按 Claude Design 定稿(归档在 docs/design/)重做 UI,并把 tmux 从 「从不自动进」修成「连接前探测 → 让用户选会话」。 设计令牌与导航地基 - Theme 拆三层:TXAccent(恒定 blurple) / TXChrome(中性阶×4 表) / TXFlavor(Catppuccin 终端) - vendor JetBrains Mono 四权重(附 OFL 许可) - AppRouter + SessionManager:多会话并存,路由与会话生命周期解耦 - SessionCanvas 常驻挂载会话 surface(摘除即丢内容,也是缩回动画的前提) 按设计稿落地的屏 - 沉浸轨道页:56pt 轨道 + 浮起标题/状态胶囊 + 侧边栏三态(遮罩不 resize、Pin 各一次) - 首页:活动会话卡(readViewportText 文本镜像)+ 主机网格 + 筛选 chips - 关闭二次确认(tmux 仅断开 / 原生窗口两套文案)、分屏菜单、pane 拖拽条 - 空状态:首次运行 / 无会话 / 搜索无命中 / 连接中·失败·已关闭 tmux 真实链路(真机查出并修掉 4 个 bug) - 全代码库从来没人发起 attach → 连接前探测 + 会话选择器(接回 / 新建 / 原生终端) - format 分隔符 tab 经 PTY 变成下划线 → 改用 |:| - controller 变化不冒泡到 session → Combine 转发(否则数据解析对了 UI 不刷新) - 当前会话名不能靠 session_attached 反推 → 改用 display-message 传输层:SSH connect 加超时(原来阻塞到系统 TCP 超时 75s+) 无头验证设施:假会话 fixture · terminalx://ui/* 驱动 · 横屏截图脚本 · 对拍走查法 TXCore 45 tests 绿(新增 5 个会话探测单测) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
93 lines
3.4 KiB
Swift
93 lines
3.4 KiB
Swift
import Foundation
|
||
|
||
/// 一条 Keychain 凭据(元数据部分,**不含机密**)。
|
||
/// 机密(密码/私钥材料/passphrase)经 `CredentialStore.secret(for:)` 按需单条取,
|
||
/// 列表 UI 只碰这里的 meta,永不为渲染而加载机密(对齐签名后 Keychain 后端的天然分层)。
|
||
struct Credential: Identifiable, Codable, Equatable {
|
||
var id: UUID = UUID()
|
||
var name: String
|
||
var username: String
|
||
var kind: Kind
|
||
var createdAt: Date = Date()
|
||
|
||
enum Kind: Codable, Equatable {
|
||
/// 用户名 + 密码。
|
||
case password
|
||
/// SSH 私钥。`publicKeyBlob` 冗余存 SSH 线格式公钥,供列表展示指纹/复制 authorized_keys 而不碰机密。
|
||
case privateKey(keyType: KeyType, source: Source, hasPassphrase: Bool, publicKeyBlob: Data)
|
||
/// Secure Enclave(Face ID)密钥——机密不出 SE,只存 tag 引用。未签名构建下建不出,UI 占位。
|
||
case secureEnclave(keyTag: String)
|
||
}
|
||
|
||
/// 私钥来源。
|
||
enum Source: String, Codable { case pasted, imported, generated }
|
||
|
||
/// 私钥算法。libssh2 无 Ed25519、有 ECDSA;"生成新的"默认 P-256。
|
||
enum KeyType: String, Codable {
|
||
case ecdsaP256, ecdsaP384, ecdsaP521, rsa, unknown
|
||
|
||
var displayName: String {
|
||
switch self {
|
||
case .ecdsaP256: "ECDSA P-256"
|
||
case .ecdsaP384: "ECDSA P-384"
|
||
case .ecdsaP521: "ECDSA P-521"
|
||
case .rsa: "RSA"
|
||
case .unknown: "SSH 私钥"
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 展示辅助
|
||
|
||
var kindLabel: String {
|
||
switch kind {
|
||
case .password: "密码"
|
||
case .privateKey(let t, _, _, _): t.displayName
|
||
case .secureEnclave: "Face ID 密钥"
|
||
}
|
||
}
|
||
|
||
var iconName: String {
|
||
switch kind {
|
||
case .password: "key.fill"
|
||
case .privateKey: "lock.doc.fill"
|
||
case .secureEnclave: "faceid"
|
||
}
|
||
}
|
||
|
||
/// SSH 线格式公钥 blob(仅私钥类凭据有)。
|
||
var publicKeyBlob: Data? {
|
||
if case .privateKey(_, _, _, let blob) = kind { return blob }
|
||
return nil
|
||
}
|
||
|
||
/// authorized_keys 行(生成/导入的私钥可复制到服务器)。
|
||
var authorizedKeysLine: String? {
|
||
guard let blob = publicKeyBlob, !blob.isEmpty else { return nil }
|
||
// 目前生成的都是 ecdsa-sha2-nistp256;导入的按 keyType 推断算法名。
|
||
let algo: String
|
||
if case .privateKey(let t, _, _, _) = kind {
|
||
switch t {
|
||
case .ecdsaP256: algo = "ecdsa-sha2-nistp256"
|
||
case .ecdsaP384: algo = "ecdsa-sha2-nistp384"
|
||
case .ecdsaP521: algo = "ecdsa-sha2-nistp521"
|
||
case .rsa: algo = "ssh-rsa"
|
||
case .unknown: return nil
|
||
}
|
||
} else { return nil }
|
||
return "\(algo) \(blob.base64EncodedString()) \(username)@terminalx"
|
||
}
|
||
}
|
||
|
||
/// 凭据机密(按需加载,自描述解释方式)。
|
||
enum CredentialSecret: Codable, Equatable {
|
||
/// 密码 → `.password` 认证。
|
||
case password(String)
|
||
/// PEM / PKCS#8 私钥文本(导入/粘贴)→ libssh2 `publickey_frommemory`(路径 B)。
|
||
case pemPrivateKey(pem: Data, passphrase: String?)
|
||
/// SecKey 外部表示(生成的软件 P-256 私钥)→ 重建 SecKey → SSHSigner(路径 A)。
|
||
case rawECPrivateKey(x963: Data)
|
||
/// 无机密(Secure Enclave:机密不出安全域)。
|
||
case none
|
||
}
|