初始提交: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>
This commit is contained in:
291
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/InMemoryTerminalSession.swift
vendored
Normal file
291
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/InMemoryTerminalSession.swift
vendored
Normal file
@@ -0,0 +1,291 @@
|
||||
//
|
||||
// InMemoryTerminalSession.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/16.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
public final class InMemoryTerminalSession: @unchecked Sendable {
|
||||
private static let slowSurfaceWriteThreshold: TimeInterval = 0.5
|
||||
|
||||
private let resizeLock = NSLock()
|
||||
private let surfaceAccess: InMemoryTerminalSurfaceAccess
|
||||
private var lastResize: InMemoryTerminalViewport?
|
||||
private let writeHandler: @Sendable (Data) -> Void
|
||||
private let resizeHandler: @Sendable (InMemoryTerminalViewport) -> Void
|
||||
|
||||
public init(
|
||||
write: @escaping @Sendable (Data) -> Void,
|
||||
resize: @escaping @Sendable (InMemoryTerminalViewport) -> Void
|
||||
) {
|
||||
writeHandler = write
|
||||
resizeHandler = resize
|
||||
surfaceAccess = InMemoryTerminalSurfaceAccess(
|
||||
write: Self.writeToSurface,
|
||||
processExit: Self.reportProcessExit
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
write: @escaping @Sendable (Data) -> Void,
|
||||
resize: @escaping @Sendable (InMemoryTerminalViewport) -> Void,
|
||||
surfaceWrite: @escaping InMemoryTerminalSurfaceAccess.Write,
|
||||
processExit: @escaping InMemoryTerminalSurfaceAccess.ProcessExit =
|
||||
InMemoryTerminalSession.reportProcessExit
|
||||
) {
|
||||
writeHandler = write
|
||||
resizeHandler = resize
|
||||
surfaceAccess = InMemoryTerminalSurfaceAccess(
|
||||
write: surfaceWrite,
|
||||
processExit: processExit
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Surface Lifecycle
|
||||
|
||||
func setSurface(_ surface: ghostty_surface_t?) {
|
||||
surfaceAccess.setSurface(surface)
|
||||
TerminalDebugLog.log(
|
||||
.lifecycle,
|
||||
"in-memory session surface=\(surface == nil ? "nil" : "set")"
|
||||
)
|
||||
}
|
||||
|
||||
func clearSurface(ifMatches expectedSurface: ghostty_surface_t?) {
|
||||
guard surfaceAccess.clearSurface(ifMatches: expectedSurface) else {
|
||||
TerminalDebugLog.log(
|
||||
.lifecycle,
|
||||
"in-memory session clear skipped expected=\(expectedSurface == nil ? "nil" : "set") current=\(surfaceAccess.currentSurface == nil ? "nil" : "set")"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
TerminalDebugLog.log(.lifecycle, "in-memory session surface=nil matched")
|
||||
}
|
||||
|
||||
var currentSurface: ghostty_surface_t? {
|
||||
surfaceAccess.currentSurface
|
||||
}
|
||||
|
||||
// MARK: - Viewport Read
|
||||
|
||||
/// Returns the active viewport as a UTF-8 string, or `nil` if no surface
|
||||
/// is attached. Lines are separated by `\n`. The `ghostty_text_s`
|
||||
/// lifecycle (allocate via `ghostty_surface_read_text`, free via
|
||||
/// `ghostty_surface_free_text`) is fully encapsulated — callers never
|
||||
/// touch the C buffer.
|
||||
///
|
||||
/// Selection grammar: `(VIEWPORT, TOP_LEFT)` to `(VIEWPORT, BOTTOM_RIGHT)`
|
||||
/// with `rectangle: false` (linear flow). This reads exactly the visible
|
||||
/// rows and ignores scrollback. Empty viewports return an empty string.
|
||||
///
|
||||
/// Thread-safe: keeps the surface alive for the duration of the read,
|
||||
/// preventing access against a surface mid-replacement.
|
||||
public func readViewportText() -> String? {
|
||||
surfaceAccess.withCurrentSurface { surface in
|
||||
let topLeft = ghostty_point_s(
|
||||
tag: GHOSTTY_POINT_VIEWPORT,
|
||||
coord: GHOSTTY_POINT_COORD_TOP_LEFT,
|
||||
x: 0,
|
||||
y: 0
|
||||
)
|
||||
let bottomRight = ghostty_point_s(
|
||||
tag: GHOSTTY_POINT_VIEWPORT,
|
||||
coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
|
||||
x: 0,
|
||||
y: 0
|
||||
)
|
||||
let selection = ghostty_selection_s(
|
||||
top_left: topLeft,
|
||||
bottom_right: bottomRight,
|
||||
rectangle: false
|
||||
)
|
||||
|
||||
var out = ghostty_text_s()
|
||||
guard ghostty_surface_read_text(surface, selection, &out) else {
|
||||
return nil
|
||||
}
|
||||
defer { ghostty_surface_free_text(surface, &out) }
|
||||
|
||||
guard let textPtr = out.text, out.text_len > 0 else {
|
||||
return ""
|
||||
}
|
||||
let bytes = UnsafeBufferPointer(start: textPtr, count: Int(out.text_len))
|
||||
.map { UInt8(bitPattern: $0) }
|
||||
return String(decoding: bytes, as: UTF8.self)
|
||||
} ?? nil
|
||||
}
|
||||
|
||||
func updateViewport(_ size: TerminalGridMetrics) {
|
||||
TerminalDebugLog.log(.metrics, "in-memory viewport update \(size.debugSummary)")
|
||||
dispatchResize(InMemoryTerminalViewport(
|
||||
columns: size.columns,
|
||||
rows: size.rows,
|
||||
widthPixels: size.widthPixels,
|
||||
heightPixels: size.heightPixels,
|
||||
cellWidthPixels: size.cellWidthPixels,
|
||||
cellHeightPixels: size.cellHeightPixels
|
||||
))
|
||||
}
|
||||
|
||||
// MARK: - Receiving Data
|
||||
|
||||
/// Enqueue data for the terminal from the host backend.
|
||||
///
|
||||
/// Writes are processed in order on a per-session serial queue so parsing
|
||||
/// cannot block the caller or the main thread.
|
||||
public func receive(_ data: Data) {
|
||||
guard surfaceAccess.enqueueWrite(data) else {
|
||||
TerminalDebugLog.log(
|
||||
.output,
|
||||
"terminal <- host dropped \(TerminalDebugLog.describe(data))"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
TerminalDebugLog.log(
|
||||
.output,
|
||||
"terminal <- host \(TerminalDebugLog.describe(data))"
|
||||
)
|
||||
}
|
||||
|
||||
/// Feed a UTF-8 string into the terminal from the host backend.
|
||||
public func receive(_ string: String) {
|
||||
guard let data = string.data(using: .utf8) else { return }
|
||||
receive(data)
|
||||
}
|
||||
|
||||
/// Inject input bytes directly into the host-side consumer.
|
||||
///
|
||||
/// This bypasses `ghostty_surface_key` translation and is intended for
|
||||
/// control sequences that the in-memory backend must interpret itself.
|
||||
public func sendInput(_ data: Data) {
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"host <- direct input \(TerminalDebugLog.describe(data))"
|
||||
)
|
||||
writeHandler(data)
|
||||
}
|
||||
|
||||
// MARK: - Process Exit
|
||||
|
||||
/// Enqueue a host-managed process exit after all previously received data.
|
||||
public func finish(exitCode: UInt32, runtimeMilliseconds: UInt64) {
|
||||
guard surfaceAccess.enqueueProcessExit(
|
||||
exitCode: exitCode,
|
||||
runtimeMilliseconds: runtimeMilliseconds
|
||||
) else {
|
||||
TerminalDebugLog.log(
|
||||
.lifecycle,
|
||||
"process exit ignored: missing surface exitCode=\(exitCode) runtimeMs=\(runtimeMilliseconds)"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
TerminalDebugLog.log(
|
||||
.lifecycle,
|
||||
"process exit exitCode=\(exitCode) runtimeMs=\(runtimeMilliseconds)"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - C Callbacks
|
||||
|
||||
static let receiveBufferCallback: ghostty_surface_receive_buffer_cb = { userdata, ptr, len in
|
||||
guard let userdata, let ptr else { return }
|
||||
let session = Unmanaged<InMemoryTerminalSession>
|
||||
.fromOpaque(userdata)
|
||||
.takeUnretainedValue()
|
||||
let data = Data(bytes: ptr, count: len)
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"host <- terminal \(TerminalDebugLog.describe(data))"
|
||||
)
|
||||
session.writeHandler(data)
|
||||
}
|
||||
|
||||
static let receiveResizeCallback: ghostty_surface_receive_resize_cb = { userdata, cols, rows, widthPx, heightPx in
|
||||
guard let userdata else { return }
|
||||
let session = Unmanaged<InMemoryTerminalSession>
|
||||
.fromOpaque(userdata)
|
||||
.takeUnretainedValue()
|
||||
TerminalDebugLog.log(
|
||||
.metrics,
|
||||
"receive resize cols=\(cols) rows=\(rows) pixels=\(widthPx)x\(heightPx)"
|
||||
)
|
||||
session.dispatchResize(InMemoryTerminalViewport(
|
||||
columns: cols,
|
||||
rows: rows,
|
||||
widthPixels: widthPx,
|
||||
heightPixels: heightPx
|
||||
))
|
||||
}
|
||||
|
||||
private func dispatchResize(_ resize: InMemoryTerminalViewport) {
|
||||
resizeLock.lock()
|
||||
let mergedResize = mergedResize(resize)
|
||||
guard mergedResize != lastResize else {
|
||||
resizeLock.unlock()
|
||||
TerminalDebugLog.log(
|
||||
.metrics,
|
||||
"resize unchanged cols=\(mergedResize.columns) rows=\(mergedResize.rows) pixels=\(mergedResize.widthPixels)x\(mergedResize.heightPixels) cell=\(mergedResize.cellWidthPixels)x\(mergedResize.cellHeightPixels)"
|
||||
)
|
||||
return
|
||||
}
|
||||
lastResize = mergedResize
|
||||
resizeLock.unlock()
|
||||
|
||||
TerminalDebugLog.log(
|
||||
.metrics,
|
||||
"resize dispatched cols=\(mergedResize.columns) rows=\(mergedResize.rows) pixels=\(mergedResize.widthPixels)x\(mergedResize.heightPixels) cell=\(mergedResize.cellWidthPixels)x\(mergedResize.cellHeightPixels)"
|
||||
)
|
||||
resizeHandler(mergedResize)
|
||||
}
|
||||
|
||||
private func mergedResize(_ resize: InMemoryTerminalViewport) -> InMemoryTerminalViewport {
|
||||
guard let lastResize else { return resize }
|
||||
|
||||
return InMemoryTerminalViewport(
|
||||
columns: resize.columns,
|
||||
rows: resize.rows,
|
||||
widthPixels: resize.widthPixels == 0 ? lastResize.widthPixels : resize.widthPixels,
|
||||
heightPixels: resize.heightPixels == 0 ? lastResize.heightPixels : resize.heightPixels,
|
||||
cellWidthPixels: resize.cellWidthPixels == 0 ? lastResize.cellWidthPixels : resize.cellWidthPixels,
|
||||
cellHeightPixels: resize.cellHeightPixels == 0 ? lastResize.cellHeightPixels : resize.cellHeightPixels
|
||||
)
|
||||
}
|
||||
|
||||
func waitForPendingOutput() {
|
||||
surfaceAccess.waitForPendingOutput()
|
||||
}
|
||||
|
||||
private static func writeToSurface(_ surface: ghostty_surface_t, _ data: Data) {
|
||||
let start = ProcessInfo.processInfo.systemUptime
|
||||
defer {
|
||||
let duration = ProcessInfo.processInfo.systemUptime - start
|
||||
if duration >= slowSurfaceWriteThreshold {
|
||||
TerminalDebugLog.log(
|
||||
.output,
|
||||
"surface write slow bytes=\(data.count) duration=\(String(format: "%.3f", duration))s"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data.withUnsafeBytes { buffer in
|
||||
guard let ptr = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
ghostty_surface_write_buffer(surface, ptr, UInt(buffer.count))
|
||||
}
|
||||
}
|
||||
|
||||
private static func reportProcessExit(
|
||||
_ surface: ghostty_surface_t,
|
||||
_ exitCode: UInt32,
|
||||
_ runtimeMilliseconds: UInt64
|
||||
) {
|
||||
ghostty_surface_process_exit(surface, exitCode, runtimeMilliseconds)
|
||||
}
|
||||
}
|
||||
141
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/InMemoryTerminalSurfaceAccess.swift
vendored
Normal file
141
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/InMemoryTerminalSurfaceAccess.swift
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
/// Serializes host output while keeping the raw surface alive for each C call.
|
||||
final class InMemoryTerminalSurfaceAccess: @unchecked Sendable {
|
||||
typealias Write = @Sendable (ghostty_surface_t, Data) -> Void
|
||||
typealias ProcessExit = @Sendable (ghostty_surface_t, UInt32, UInt64) -> Void
|
||||
|
||||
private let condition = NSCondition()
|
||||
private let outputQueue = DispatchQueue(
|
||||
label: "com.lakr233.libghostty-spm.in-memory-output",
|
||||
qos: .userInitiated
|
||||
)
|
||||
private let write: Write
|
||||
private let processExit: ProcessExit
|
||||
|
||||
private var surface: ghostty_surface_t?
|
||||
/// Invalidates work that was enqueued for a surface that has been replaced.
|
||||
private var generation: UInt64 = 0
|
||||
/// Prevents the caller from freeing a surface while a C operation uses it.
|
||||
private var activeOperations = 0
|
||||
|
||||
init(
|
||||
write: @escaping Write,
|
||||
processExit: @escaping ProcessExit
|
||||
) {
|
||||
self.write = write
|
||||
self.processExit = processExit
|
||||
}
|
||||
|
||||
func setSurface(_ surface: ghostty_surface_t?) {
|
||||
condition.lock()
|
||||
generation &+= 1
|
||||
self.surface = nil
|
||||
waitForActiveOperations()
|
||||
self.surface = surface
|
||||
condition.unlock()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func clearSurface(ifMatches expectedSurface: ghostty_surface_t?) -> Bool {
|
||||
condition.lock()
|
||||
guard surface == expectedSurface else {
|
||||
condition.unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
generation &+= 1
|
||||
surface = nil
|
||||
waitForActiveOperations()
|
||||
condition.unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
var currentSurface: ghostty_surface_t? {
|
||||
condition.lock()
|
||||
defer { condition.unlock() }
|
||||
return surface
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func enqueueWrite(_ data: Data) -> Bool {
|
||||
guard let generation = currentGeneration else { return false }
|
||||
outputQueue.async { [self] in
|
||||
withSurface(generation: generation) { surface in
|
||||
write(surface, data)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func enqueueProcessExit(
|
||||
exitCode: UInt32,
|
||||
runtimeMilliseconds: UInt64
|
||||
) -> Bool {
|
||||
guard let generation = currentGeneration else { return false }
|
||||
outputQueue.async { [self] in
|
||||
withSurface(generation: generation) { surface in
|
||||
processExit(surface, exitCode, runtimeMilliseconds)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func withCurrentSurface<Result>(
|
||||
_ operation: (ghostty_surface_t) -> Result
|
||||
) -> Result? {
|
||||
condition.lock()
|
||||
guard let surface else {
|
||||
condition.unlock()
|
||||
return nil
|
||||
}
|
||||
activeOperations += 1
|
||||
condition.unlock()
|
||||
|
||||
defer { finishOperation() }
|
||||
return operation(surface)
|
||||
}
|
||||
|
||||
func waitForPendingOutput() {
|
||||
outputQueue.sync {}
|
||||
}
|
||||
|
||||
private var currentGeneration: UInt64? {
|
||||
condition.lock()
|
||||
defer { condition.unlock() }
|
||||
return surface == nil ? nil : generation
|
||||
}
|
||||
|
||||
private func withSurface(
|
||||
generation expectedGeneration: UInt64,
|
||||
_ operation: (ghostty_surface_t) -> Void
|
||||
) {
|
||||
condition.lock()
|
||||
guard generation == expectedGeneration, let surface else {
|
||||
condition.unlock()
|
||||
return
|
||||
}
|
||||
activeOperations += 1
|
||||
condition.unlock()
|
||||
|
||||
defer { finishOperation() }
|
||||
operation(surface)
|
||||
}
|
||||
|
||||
private func finishOperation() {
|
||||
condition.lock()
|
||||
activeOperations -= 1
|
||||
if activeOperations == 0 {
|
||||
condition.broadcast()
|
||||
}
|
||||
condition.unlock()
|
||||
}
|
||||
|
||||
private func waitForActiveOperations() {
|
||||
while activeOperations > 0 {
|
||||
condition.wait()
|
||||
}
|
||||
}
|
||||
}
|
||||
31
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/InMemoryTerminalViewport.swift
vendored
Normal file
31
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/InMemoryTerminalViewport.swift
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// InMemoryTerminalViewport.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/16.
|
||||
//
|
||||
|
||||
public struct InMemoryTerminalViewport: Sendable, Equatable {
|
||||
public var columns: UInt16
|
||||
public var rows: UInt16
|
||||
public var widthPixels: UInt32
|
||||
public var heightPixels: UInt32
|
||||
public var cellWidthPixels: UInt32
|
||||
public var cellHeightPixels: UInt32
|
||||
|
||||
public init(
|
||||
columns: UInt16,
|
||||
rows: UInt16,
|
||||
widthPixels: UInt32 = 0,
|
||||
heightPixels: UInt32 = 0,
|
||||
cellWidthPixels: UInt32 = 0,
|
||||
cellHeightPixels: UInt32 = 0
|
||||
) {
|
||||
self.columns = columns
|
||||
self.rows = rows
|
||||
self.widthPixels = widthPixels
|
||||
self.heightPixels = heightPixels
|
||||
self.cellWidthPixels = cellWidthPixels
|
||||
self.cellHeightPixels = cellHeightPixels
|
||||
}
|
||||
}
|
||||
176
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/TerminalCallbackBridge.swift
vendored
Normal file
176
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/TerminalCallbackBridge.swift
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
22
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/TerminalSessionBackend.swift
vendored
Normal file
22
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/TerminalSessionBackend.swift
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// TerminalSessionBackend.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/16.
|
||||
//
|
||||
|
||||
public enum TerminalSessionBackend: Sendable {
|
||||
case exec
|
||||
case inMemory(InMemoryTerminalSession)
|
||||
|
||||
func isEquivalent(to other: TerminalSessionBackend) -> Bool {
|
||||
switch (self, other) {
|
||||
case (.exec, .exec):
|
||||
true
|
||||
case let (.inMemory(lhs), .inMemory(rhs)):
|
||||
lhs === rhs
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user