Files
terminalX/vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/TerminalCallbackBridge.swift
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

177 lines
7.3 KiB
Swift

//
// TerminalCallbackBridge.swift
// libghostty-spm
//
// Created by Lakr233 on 2026/3/16.
//
import Foundation
import GhosttyKit
/// Dispatches C runtime callbacks to a ``TerminalSurfaceViewDelegate``.
///
/// An instance of this class is passed as the `userdata` pointer in the
/// surface config so that Ghostty callbacks can route actions back to
/// the owning view.
@MainActor
final class TerminalCallbackBridge {
weak var delegate: (any TerminalSurfaceViewDelegate)?
/// Raw surface pointer for use in C callbacks (e.g. clipboard).
nonisolated(unsafe) var rawSurface: ghostty_surface_t?
var onCellSizeChange: ((UInt32, UInt32) -> Void)?
var onRenderRequest: (() -> Void)?
init(delegate: (any TerminalSurfaceViewDelegate)? = nil) {
self.delegate = delegate
}
func handleAction(_ action: ghostty_action_s) {
switch action.tag {
case GHOSTTY_ACTION_SET_TITLE:
if let cStr = action.action.set_title.title {
let title = String(cString: cStr)
TerminalDebugLog.log(
.actions,
"callback action=set_title title=\(TerminalDebugLog.describe(title))"
)
(delegate as? any TerminalSurfaceTitleDelegate)?
.terminalDidChangeTitle(title)
}
case GHOSTTY_ACTION_CELL_SIZE:
let cellSize = action.action.cell_size
TerminalDebugLog.log(
.actions,
"callback action=cell_size width=\(cellSize.width) height=\(cellSize.height)"
)
onCellSizeChange?(cellSize.width, cellSize.height)
case GHOSTTY_ACTION_RING_BELL:
TerminalDebugLog.log(.actions, "callback action=ring_bell")
(delegate as? any TerminalSurfaceBellDelegate)?
.terminalDidRingBell()
case GHOSTTY_ACTION_RENDER:
TerminalDebugLog.log(.render, "callback action=render")
onRenderRequest?()
case GHOSTTY_ACTION_CONFIG_CHANGE:
// Colors/theme may have changed (e.g. on system appearance
// toggle). Ghostty applies the new config internally but won't
// repaint until the next frame request one so the refreshed
// theme is visible without waiting for input or layout.
TerminalDebugLog.log(.actions, "callback action=config_change")
onRenderRequest?()
case GHOSTTY_ACTION_PROGRESS_REPORT:
let report = action.action.progress_report
let state = TerminalProgressState(report.state) ?? .set
// int8_t -1 signals "no progress provided" surface as nil.
let percent: Int? = report.progress < 0 ? nil : Int(report.progress)
TerminalDebugLog.log(
.actions,
"callback action=progress_report state=\(state) percent=\(percent.map { "\($0)" } ?? "nil")"
)
(delegate as? any TerminalSurfaceProgressReportDelegate)?
.terminalDidReportProgress(state: state, percent: percent)
case GHOSTTY_ACTION_COMMAND_FINISHED:
let finished = action.action.command_finished
// int16_t -1 signals unknown exit code.
let exit: Int? = finished.exit_code < 0 ? nil : Int(finished.exit_code)
TerminalDebugLog.log(
.actions,
"callback action=command_finished exit=\(exit.map { "\($0)" } ?? "nil") duration_ns=\(finished.duration)"
)
(delegate as? any TerminalSurfaceCommandFinishedDelegate)?
.terminalDidFinishCommand(
exitCode: exit,
durationNanos: finished.duration
)
case GHOSTTY_ACTION_DESKTOP_NOTIFICATION:
let payload = action.action.desktop_notification
let title = payload.title.map { String(cString: $0) } ?? ""
let body = payload.body.map { String(cString: $0) } ?? ""
TerminalDebugLog.log(
.actions,
"callback action=desktop_notification title=\(TerminalDebugLog.describe(title)) body=\(TerminalDebugLog.describe(body))"
)
(delegate as? any TerminalSurfaceDesktopNotificationDelegate)?
.terminalDidRequestDesktopNotification(title: title, body: body)
case GHOSTTY_ACTION_OPEN_URL:
let payload = action.action.open_url
let kind = TerminalOpenURLKind(payload.kind)
let url: String = payload.url.map { ptr in
// Ghostty provides a length-prefixed string; respect the
// documented length rather than trusting a NUL terminator.
let buf = UnsafeBufferPointer(start: ptr, count: Int(payload.len))
return String(decoding: buf.map(UInt8.init), as: UTF8.self)
} ?? ""
TerminalDebugLog.log(
.actions,
"callback action=open_url kind=\(kind) url=\(TerminalDebugLog.describe(url))"
)
(delegate as? any TerminalSurfaceOpenURLDelegate)?
.terminalDidRequestOpenURL(url, kind: kind)
case GHOSTTY_ACTION_MOUSE_OVER_LINK:
let payload = action.action.mouse_over_link
let url: String? = {
guard let ptr = payload.url, payload.len > 0 else { return nil }
let buf = UnsafeBufferPointer(start: ptr, count: Int(payload.len))
return String(decoding: buf.map(UInt8.init), as: UTF8.self)
}()
TerminalDebugLog.log(
.actions,
"callback action=mouse_over_link url=\(url.map { TerminalDebugLog.describe($0) } ?? "nil")"
)
(delegate as? any TerminalSurfaceHoverLinkDelegate)?
.terminalDidUpdateHoverLink(url)
case GHOSTTY_ACTION_PWD:
let payload = action.action.pwd
if let cStr = payload.pwd {
let pwd = String(cString: cStr)
TerminalDebugLog.log(
.actions,
"callback action=pwd pwd=\(TerminalDebugLog.describe(pwd))"
)
(delegate as? any TerminalSurfacePwdDelegate)?
.terminalDidChangeWorkingDirectory(pwd)
}
case GHOSTTY_ACTION_SCROLLBAR:
let payload = action.action.scrollbar
TerminalDebugLog.log(
.actions,
"callback action=scrollbar total=\(payload.total) offset=\(payload.offset) len=\(payload.len)"
)
(delegate as? any TerminalSurfaceScrollbarDelegate)?
.terminalDidUpdateScrollbar(
TerminalScrollbar(
total: payload.total,
offset: payload.offset,
len: payload.len
)
)
default:
TerminalDebugLog.log(
.actions,
"callback action=\(TerminalDebugLog.describe(action.tag))"
)
}
}
func handleClose(processAlive: Bool) {
TerminalDebugLog.log(
.lifecycle,
"callback close processAlive=\(processAlive)"
)
(delegate as? any TerminalSurfaceCloseDelegate)?
.terminalDidClose(processAlive: processAlive)
}
}