Files
kid aa92d0e676 初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)
- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连
- M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire
- M1.5: tmux -CC 原生 tab(MVP)
- 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/
- 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口)
- 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名
- 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:20:46 +08:00

305 lines
7.8 KiB
Swift

import Foundation
import GhosttyKit
public struct TerminalDebugCategory: OptionSet, Sendable {
public let rawValue: UInt16
public init(rawValue: UInt16) {
self.rawValue = rawValue
}
public static let lifecycle = Self(rawValue: 1 << 0)
public static let metrics = Self(rawValue: 1 << 1)
public static let input = Self(rawValue: 1 << 2)
public static let output = Self(rawValue: 1 << 3)
public static let ime = Self(rawValue: 1 << 4)
public static let actions = Self(rawValue: 1 << 5)
public static let render = Self(rawValue: 1 << 6)
public static let standard: Self = [
.lifecycle,
.metrics,
.input,
.output,
.ime,
.actions,
]
public static let all: Self = [
.standard,
.render,
]
}
public enum TerminalDebugLog {
public typealias Sink = @Sendable (String) -> Void
private struct Snapshot {
let isEnabled: Bool
let categories: TerminalDebugCategory
let sink: Sink
}
private final class Store: @unchecked Sendable {
let lock = NSLock()
var isEnabled = false
var categories: TerminalDebugCategory = .standard
var sink: Sink = { message in
Swift.print(message)
}
}
private static let store = Store()
public static var isEnabled: Bool {
get {
withSnapshot { $0.isEnabled }
}
set {
updateStore { $0.isEnabled = newValue }
}
}
public static var categories: TerminalDebugCategory {
get {
withSnapshot { $0.categories }
}
set {
updateStore { $0.categories = newValue }
}
}
public static var sink: Sink {
get {
withSnapshot { $0.sink }
}
set {
updateStore { $0.sink = newValue }
}
}
public static func enable(_ categories: TerminalDebugCategory = .standard) {
updateStore {
$0.isEnabled = true
$0.categories = categories
}
}
public static func disable() {
updateStore { $0.isEnabled = false }
}
static func log(
_ category: TerminalDebugCategory,
_ message: @autoclosure () -> String
) {
let snapshot = snapshot()
guard snapshot.isEnabled else { return }
guard snapshot.categories.contains(category) else { return }
snapshot.sink(
"[GhosttyTerminal][\(timestamp())][\(label(for: category))] \(message())"
)
}
static func describe(_ string: String?, limit: Int = 96) -> String {
guard let string else { return "nil" }
return "\"\(escaped(string, limit: limit))\""
}
static func describe(_ data: Data, limit: Int = 48) -> String {
let preview = data.prefix(limit)
let text = String(decoding: preview, as: UTF8.self)
let hex = preview.map { String(format: "%02X", $0) }.joined(separator: " ")
let suffix = data.count > limit ? "..." : ""
return "bytes=\(data.count) utf8=\"\(escaped(text, limit: limit))\(suffix)\" hex=\(hex)\(suffix)"
}
static func describe(_ range: NSRange) -> String {
"{location=\(range.location), length=\(range.length)}"
}
static func describe(_ action: ghostty_input_action_e) -> String {
switch action {
case GHOSTTY_ACTION_PRESS:
"press"
case GHOSTTY_ACTION_RELEASE:
"release"
case GHOSTTY_ACTION_REPEAT:
"repeat"
default:
"unknown(\(action.rawValue))"
}
}
static func describe(_ state: ghostty_input_mouse_state_e) -> String {
switch state {
case GHOSTTY_MOUSE_PRESS:
"press"
case GHOSTTY_MOUSE_RELEASE:
"release"
default:
"unknown(\(state.rawValue))"
}
}
static func describe(_ tag: ghostty_action_tag_e) -> String {
switch tag {
case GHOSTTY_ACTION_CELL_SIZE:
"cell_size"
case GHOSTTY_ACTION_SET_TITLE:
"set_title"
case GHOSTTY_ACTION_SET_TAB_TITLE:
"set_tab_title"
case GHOSTTY_ACTION_RING_BELL:
"ring_bell"
case GHOSTTY_ACTION_RENDER:
"render"
case GHOSTTY_ACTION_CONFIG_CHANGE:
"config_change"
case GHOSTTY_ACTION_RELOAD_CONFIG:
"reload_config"
default:
"tag(\(tag.rawValue))"
}
}
private static func withSnapshot<T>(
_ body: (Snapshot) -> T
) -> T {
body(snapshot())
}
private static func snapshot() -> Snapshot {
store.lock.lock()
defer { store.lock.unlock() }
return Snapshot(
isEnabled: store.isEnabled,
categories: store.categories,
sink: store.sink
)
}
private static func updateStore(
_ body: (Store) -> Void
) {
store.lock.lock()
defer { store.lock.unlock() }
body(store)
}
private static func label(
for category: TerminalDebugCategory
) -> String {
switch category {
case .lifecycle:
"lifecycle"
case .metrics:
"metrics"
case .input:
"input"
case .output:
"output"
case .ime:
"ime"
case .actions:
"actions"
case .render:
"render"
default:
"debug"
}
}
private static func timestamp() -> String {
String(format: "%.3f", Date().timeIntervalSince1970)
}
private static func escaped(
_ string: String,
limit: Int
) -> String {
var result = ""
var emitted = 0
for scalar in string.unicodeScalars {
guard emitted < limit else {
result.append("...")
break
}
switch scalar.value {
case 0x09:
result.append("\\t")
case 0x0A:
result.append("\\n")
case 0x0D:
result.append("\\r")
case 0x1B:
result.append("\\e")
case 0x20 ..< 0x7F:
result.append(Character(scalar))
default:
if scalar.value < 0x20 || scalar.value == 0x7F {
result.append(String(format: "\\u{%02X}", Int(scalar.value)))
} else {
result.append(Character(scalar))
}
}
emitted += 1
}
return result
}
}
extension TerminalGridMetrics {
var debugSummary: String {
"cols=\(columns) rows=\(rows) pixels=\(widthPixels)x\(heightPixels) cell=\(cellWidthPixels)x\(cellHeightPixels)"
}
}
extension TerminalViewportMetrics {
var debugSummary: String {
"\(surfaceSize.debugSummary) scale=\(String(format: "%.2f", scale))"
}
}
extension TerminalSessionBackend {
var debugSummary: String {
switch self {
case .exec:
"exec"
case .inMemory:
"in-memory"
}
}
}
extension TerminalSurfaceOptions {
var debugSummary: String {
let fontSizeDescription = fontSize.map { String($0) } ?? "nil"
return "backend=\(backend.debugSummary) fontSize=\(fontSizeDescription) workingDirectory=\(workingDirectory ?? "nil") context=\(context.debugSummary)"
}
}
extension TerminalSurfaceContext {
var debugSummary: String {
switch self {
case .window:
"window"
case .split:
"split"
}
}
}
extension TerminalHardwareKeyDelivery {
var debugSummary: String {
switch self {
case let .ghostty(key):
"ghostty(\(key.rawValue))"
case let .data(data):
"data(\(TerminalDebugLog.describe(data)))"
}
}
}