- 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>
105 lines
2.8 KiB
Swift
105 lines
2.8 KiB
Swift
//
|
|
// TerminalViewRepresentable.swift
|
|
// libghostty-spm
|
|
//
|
|
// Created by Lakr233 on 2026/3/16.
|
|
//
|
|
|
|
import SwiftUI
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#elseif canImport(AppKit)
|
|
import AppKit
|
|
#endif
|
|
|
|
@MainActor
|
|
struct TerminalViewRepresentable {
|
|
let context: TerminalViewState
|
|
let controller: TerminalController
|
|
let configuration: TerminalSurfaceOptions
|
|
let focusBinding: TerminalFocusBinding?
|
|
|
|
func configureView(_ view: TerminalView, initial: Bool) {
|
|
if initial {
|
|
view.delegate = context
|
|
}
|
|
|
|
if let currentController = view.controller, currentController === controller {
|
|
// Keep the current surface.
|
|
} else {
|
|
view.controller = controller
|
|
}
|
|
|
|
if !view.configuration.isEquivalent(to: configuration) {
|
|
view.configuration = configuration
|
|
}
|
|
}
|
|
|
|
static func synchronizeFocus(_ view: TerminalView, with binding: TerminalFocusBinding?) {
|
|
guard let binding else { return }
|
|
|
|
DispatchQueue.main.async { [weak view] in
|
|
#if canImport(UIKit)
|
|
guard let view, view.window != nil else { return }
|
|
if binding.isFocused {
|
|
if !view.isFirstResponder { view.becomeFirstResponder() }
|
|
} else if view.isFirstResponder {
|
|
_ = view.resignFirstResponder()
|
|
}
|
|
#elseif canImport(AppKit)
|
|
guard let view, let window = view.window else { return }
|
|
if binding.isFocused {
|
|
if window.firstResponder !== view {
|
|
window.makeFirstResponder(view)
|
|
}
|
|
} else if window.firstResponder === view {
|
|
window.makeFirstResponder(nil)
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
struct TerminalFocusBinding {
|
|
private let read: () -> Bool
|
|
private let write: (Bool) -> Void
|
|
|
|
var isFocused: Bool {
|
|
read()
|
|
}
|
|
|
|
func setFocused(_ focused: Bool) {
|
|
write(focused)
|
|
}
|
|
|
|
static func bool(_ binding: FocusState<Bool>.Binding) -> TerminalFocusBinding {
|
|
TerminalFocusBinding(
|
|
read: { binding.wrappedValue },
|
|
write: { binding.wrappedValue = $0 }
|
|
)
|
|
}
|
|
|
|
static func optional<Value: Hashable>(
|
|
_ binding: FocusState<Value?>.Binding,
|
|
equals value: Value
|
|
) -> TerminalFocusBinding {
|
|
TerminalFocusBinding(
|
|
read: { binding.wrappedValue == value },
|
|
write: { focused in
|
|
binding.wrappedValue = focused ? value : nil
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
extension TerminalFocusBinding? {
|
|
func setFocused(_ focused: Bool) {
|
|
guard let binding = self, binding.isFocused != focused else {
|
|
return
|
|
}
|
|
binding.setFocused(focused)
|
|
}
|
|
}
|