初始提交: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:
285
vendor/libghostty-spm/Sources/GhosttyTerminal/Platform/UIKit/UITerminalView.swift
vendored
Normal file
285
vendor/libghostty-spm/Sources/GhosttyTerminal/Platform/UIKit/UITerminalView.swift
vendored
Normal file
@@ -0,0 +1,285 @@
|
||||
//
|
||||
// UITerminalView.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/16.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
import GhosttyKit
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
open class UITerminalView: UIView {
|
||||
let core = TerminalSurfaceCoordinator()
|
||||
var momentumDisplayLink: CADisplayLink?
|
||||
var momentumVelocity: CGPoint = .zero
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
static let minFontSize: Float = 4
|
||||
static let maxFontSize: Float = 64
|
||||
#endif
|
||||
var activePointerButton: ghostty_input_mouse_button_e?
|
||||
var pointerSelectionStartPoint: CGPoint?
|
||||
var lastPointerSelectionRect: CGRect?
|
||||
var pendingSelectionMenuPoint: CGPoint?
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
var indirectPointerPanOwnsTouchSequence = false
|
||||
var suppressNextIndirectPointerTouchEnd = false
|
||||
#endif
|
||||
lazy var selectionContextMenuInteraction = UIContextMenuInteraction(delegate: self)
|
||||
var hardwareKeyHandled = false
|
||||
let touchScrollMultiplier: CGFloat = 3.0
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
var currentFontSize: Float = 14
|
||||
var lastPinchScale: CGFloat = 1.0
|
||||
#endif
|
||||
lazy var inputHandler = TerminalTextInputHandler(view: self)
|
||||
weak var _inputDelegate: (any UITextInputDelegate)?
|
||||
var onFocusChange: ((Bool) -> Void)?
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
lazy var terminalInputAccessory = TerminalInputAccessoryView(terminalView: self)
|
||||
let stickyModifiers = TerminalStickyModifierState()
|
||||
var softwareKeyboardVisible = false
|
||||
var pendingKeyboardDismissOnTouchEnd = false
|
||||
var touchDidScrollDuringCurrentTouch = false
|
||||
#endif
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
open var inputAccessoryStyle: TerminalInputAccessoryStyle {
|
||||
get { terminalInputAccessory.style }
|
||||
set { terminalInputAccessory.style = newValue }
|
||||
}
|
||||
|
||||
open var inputAccessoryItems: [TerminalInputAccessoryItem] = TerminalInputAccessoryItem.defaultItems {
|
||||
didSet {
|
||||
terminalInputAccessory.rebuildContent()
|
||||
reloadInputViews()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
open weak var delegate: (any TerminalSurfaceViewDelegate)? {
|
||||
get { core.delegate }
|
||||
set { core.delegate = newValue }
|
||||
}
|
||||
|
||||
open var controller: TerminalController? {
|
||||
get { core.controller }
|
||||
set { core.controller = newValue }
|
||||
}
|
||||
|
||||
open var configuration: TerminalSurfaceOptions {
|
||||
get { core.configuration }
|
||||
set { core.configuration = newValue }
|
||||
}
|
||||
|
||||
var surface: TerminalSurface? {
|
||||
core.surface
|
||||
}
|
||||
|
||||
open var hasText: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override open var canBecomeFirstResponder: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
public required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func commonInit() {
|
||||
backgroundColor = .clear
|
||||
isOpaque = false
|
||||
isUserInteractionEnabled = true
|
||||
updateDisplayScale()
|
||||
|
||||
core.isAttached = { [weak self] in self?.window != nil }
|
||||
core.scaleFactor = { [weak self] in
|
||||
Double(self?.resolvedDisplayScale() ?? UIScreen.main.nativeScale)
|
||||
}
|
||||
core.viewSize = { [weak self] in
|
||||
guard let self else { return (0, 0) }
|
||||
return (bounds.width, bounds.height)
|
||||
}
|
||||
core.platformSetup = { [weak self] config in
|
||||
guard let self else { return }
|
||||
config.platform_tag = GHOSTTY_PLATFORM_IOS
|
||||
config.platform = ghostty_platform_u(
|
||||
ios: ghostty_platform_ios_s(
|
||||
uiview: Unmanaged.passUnretained(self).toOpaque()
|
||||
)
|
||||
)
|
||||
}
|
||||
core.onMetricsUpdate = { [weak self] in
|
||||
self?.updateSublayerFrames()
|
||||
}
|
||||
core.onCellSizeDidChange = { [weak self] in
|
||||
self?.refreshTextInputGeometry(reason: "cell-size-action")
|
||||
}
|
||||
core.onPostRender = { [weak self] in
|
||||
self?.enforceSublayerScale()
|
||||
}
|
||||
|
||||
setupApplicationLifecycleObservers()
|
||||
syncApplicationActiveState()
|
||||
setupPlatformInput()
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
setupKeyboardObservers()
|
||||
#endif
|
||||
}
|
||||
|
||||
open func selectionMenuPoint(at point: CGPoint) -> CGPoint? {
|
||||
logPointerSelectionDiagnostics(
|
||||
context: "selectionMenuPoint",
|
||||
point: point
|
||||
)
|
||||
if let rect = lastPointerSelectionRect {
|
||||
let pointIsInsidePointerSelection = rect.insetBy(dx: -4, dy: -4).contains(point)
|
||||
guard pointIsInsidePointerSelection else {
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"selection menu miss point=\(NSCoder.string(for: point)) outside pointer selection"
|
||||
)
|
||||
return nil
|
||||
}
|
||||
guard surface?.hasSelection() == true else {
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"selection menu miss point=\(NSCoder.string(for: point)) inside pointer selection without active selection"
|
||||
)
|
||||
return nil
|
||||
}
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"selection menu hit point=\(NSCoder.string(for: point)) inside pointer selection"
|
||||
)
|
||||
return point
|
||||
}
|
||||
|
||||
guard surface?.hasSelection() == true else {
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"selection menu miss point=\(NSCoder.string(for: point))"
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
guard surface?.selectionContainsQuicklookWord() == true else {
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"selection menu miss point=\(NSCoder.string(for: point)) outside quicklook word"
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"selection menu hit point=\(NSCoder.string(for: point))"
|
||||
)
|
||||
return point
|
||||
}
|
||||
|
||||
open func showSelectionCopyMenu(at point: CGPoint) {
|
||||
becomeFirstResponder()
|
||||
let menu = UIMenuController.shared
|
||||
menu.menuItems = nil
|
||||
menu.showMenu(
|
||||
from: self,
|
||||
rect: CGRect(x: point.x, y: point.y, width: 1, height: 1)
|
||||
)
|
||||
menu.update()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copySelectedTextToPasteboard() -> Bool {
|
||||
#if DEBUG
|
||||
if ProcessInfo.processInfo.arguments.contains("--ui-testing") {
|
||||
accessibilityValue = nil
|
||||
}
|
||||
#endif
|
||||
guard let text = surface?.readSelection(), !text.isEmpty else {
|
||||
return false
|
||||
}
|
||||
UIPasteboard.general.string = text
|
||||
#if DEBUG
|
||||
if ProcessInfo.processInfo.arguments.contains("--ui-testing") {
|
||||
accessibilityValue = text
|
||||
}
|
||||
#endif
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"selection copied bytes=\(text.utf8.count) lines=\(TerminalInputText.lineCount(in: text))"
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
open func selectionContextMenuConfiguration(
|
||||
at _: CGPoint
|
||||
) -> UIContextMenuConfiguration {
|
||||
UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
|
||||
UIMenu(children: self?.selectionContextMenuElements() ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
open func selectionContextMenuElements() -> [UIMenuElement] {
|
||||
let copy = UIAction(
|
||||
title: "Copy",
|
||||
image: UIImage(systemName: "doc.on.doc")
|
||||
) { [weak self] _ in
|
||||
self?.copySelectedTextToPasteboard()
|
||||
}
|
||||
return [copy]
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
func setupKeyboardObservers() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(keyboardDidShow),
|
||||
name: UIResponder.keyboardDidShowNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(keyboardDidHide),
|
||||
name: UIResponder.keyboardDidHideNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@objc func keyboardDidShow(_: Notification) {
|
||||
guard isFirstResponder else { return }
|
||||
softwareKeyboardVisible = true
|
||||
}
|
||||
|
||||
@objc func keyboardDidHide(_: Notification) {
|
||||
softwareKeyboardVisible = false
|
||||
}
|
||||
#endif
|
||||
|
||||
func refreshTextInputGeometry(reason: String) {
|
||||
guard isFirstResponder || inputHandler.hasMarkedText else { return }
|
||||
TerminalDebugLog.log(.ime, "refresh text geometry reason=\(reason)")
|
||||
inputHandler.notifyGeometryDidChange(reason: reason)
|
||||
}
|
||||
|
||||
func refreshInputAccessoryContent() {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
terminalInputAccessory.refreshContent()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user