初始提交: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:
254
vendor/libghostty-spm/Sources/GhosttyTerminal/Platform/UIKit/TerminalTextInputHandler@UIKit.swift
vendored
Normal file
254
vendor/libghostty-spm/Sources/GhosttyTerminal/Platform/UIKit/TerminalTextInputHandler@UIKit.swift
vendored
Normal file
@@ -0,0 +1,254 @@
|
||||
//
|
||||
// TerminalTextInputHandler@UIKit.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
import GhosttyKit
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
final class TerminalTextInputHandler {
|
||||
private weak var view: UITerminalView?
|
||||
private var markedTextState = TerminalMarkedTextState()
|
||||
|
||||
var hasMarkedText: Bool {
|
||||
markedTextState.hasMarkedText
|
||||
}
|
||||
|
||||
var documentLength: Int {
|
||||
markedTextState.documentLength
|
||||
}
|
||||
|
||||
init(view: UITerminalView) {
|
||||
self.view = view
|
||||
}
|
||||
|
||||
// MARK: - Text Input
|
||||
|
||||
func insertText(
|
||||
_ text: String,
|
||||
applyingStickyModifiers: Bool = false
|
||||
) {
|
||||
guard let view else { return }
|
||||
let shouldNotifySelectionChange = shouldNotifySelectionChange
|
||||
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"insertText text=\(TerminalDebugLog.describe(text)) marked=\(hasMarkedText)"
|
||||
)
|
||||
|
||||
view.inputDelegate?.textWillChange(view)
|
||||
if shouldNotifySelectionChange {
|
||||
view.inputDelegate?.selectionWillChange(view)
|
||||
}
|
||||
|
||||
markedTextState.clear()
|
||||
view.surface?.preedit("")
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
if applyingStickyModifiers {
|
||||
_ = view.handleStickyCommittedText(text)
|
||||
} else {
|
||||
view.surface?.sendText(text)
|
||||
}
|
||||
#else
|
||||
view.surface?.sendText(text)
|
||||
#endif
|
||||
view.refreshInputAccessoryContent()
|
||||
|
||||
if shouldNotifySelectionChange {
|
||||
view.inputDelegate?.selectionDidChange(view)
|
||||
}
|
||||
view.inputDelegate?.textDidChange(view)
|
||||
}
|
||||
|
||||
func setMarkedText(_ text: String?, selectedRange: NSRange) {
|
||||
guard let view else { return }
|
||||
let shouldNotifySelectionChange = shouldNotifySelectionChange
|
||||
|
||||
TerminalDebugLog.log(
|
||||
.ime,
|
||||
"setMarkedText text=\(TerminalDebugLog.describe(text)) selected=\(TerminalDebugLog.describe(selectedRange))"
|
||||
)
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
if let text, !text.isEmpty {
|
||||
if view.stickyModifiers.hasActiveModifiers {
|
||||
view.inputDelegate?.textWillChange(view)
|
||||
if shouldNotifySelectionChange {
|
||||
view.inputDelegate?.selectionWillChange(view)
|
||||
}
|
||||
|
||||
markedTextState.clear()
|
||||
view.surface?.preedit("")
|
||||
_ = view.handleStickyMarkedText(text)
|
||||
view.refreshInputAccessoryContent()
|
||||
|
||||
if shouldNotifySelectionChange {
|
||||
view.inputDelegate?.selectionDidChange(view)
|
||||
}
|
||||
view.inputDelegate?.textDidChange(view)
|
||||
return
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
view.inputDelegate?.textWillChange(view)
|
||||
view.inputDelegate?.selectionWillChange(view)
|
||||
|
||||
markedTextState.setMarkedText(text, selectedRange: selectedRange)
|
||||
|
||||
if let text = markedTextState.text, !text.isEmpty {
|
||||
view.surface?.preedit(text)
|
||||
} else {
|
||||
view.surface?.preedit("")
|
||||
}
|
||||
view.refreshInputAccessoryContent()
|
||||
|
||||
view.inputDelegate?.selectionDidChange(view)
|
||||
view.inputDelegate?.textDidChange(view)
|
||||
}
|
||||
|
||||
func unmarkText(
|
||||
applyingStickyModifiers: Bool = false
|
||||
) {
|
||||
guard let view else { return }
|
||||
let shouldNotifySelectionChange = shouldNotifySelectionChange
|
||||
let committedText = markedTextState.text
|
||||
|
||||
TerminalDebugLog.log(
|
||||
.ime,
|
||||
"unmarkText committed=\(TerminalDebugLog.describe(committedText))"
|
||||
)
|
||||
|
||||
view.inputDelegate?.textWillChange(view)
|
||||
if shouldNotifySelectionChange {
|
||||
view.inputDelegate?.selectionWillChange(view)
|
||||
}
|
||||
|
||||
markedTextState.clear()
|
||||
view.surface?.preedit("")
|
||||
if let committedText, !committedText.isEmpty {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
if applyingStickyModifiers {
|
||||
_ = view.handleStickyCommittedText(committedText)
|
||||
} else {
|
||||
view.surface?.sendText(committedText)
|
||||
}
|
||||
#else
|
||||
view.surface?.sendText(committedText)
|
||||
#endif
|
||||
}
|
||||
view.refreshInputAccessoryContent()
|
||||
|
||||
if shouldNotifySelectionChange {
|
||||
view.inputDelegate?.selectionDidChange(view)
|
||||
}
|
||||
view.inputDelegate?.textDidChange(view)
|
||||
}
|
||||
|
||||
func markedTextRange() -> TerminalTextRange? {
|
||||
guard markedTextState.hasMarkedText else { return nil }
|
||||
return TerminalTextRange(
|
||||
location: markedTextState.markedRange.location,
|
||||
length: markedTextState.markedRange.length
|
||||
)
|
||||
}
|
||||
|
||||
func selectedTextRange() -> TerminalTextRange {
|
||||
TerminalTextRange(
|
||||
location: markedTextState.selectedRange.location,
|
||||
length: markedTextState.selectedRange.length
|
||||
)
|
||||
}
|
||||
|
||||
func setSelectedTextRange(_ range: UITextRange?) {
|
||||
let updatedRange = if let range = range as? TerminalTextRange {
|
||||
NSRange(
|
||||
location: range.location,
|
||||
length: range.length
|
||||
)
|
||||
} else {
|
||||
NSRange(location: 0, length: 0)
|
||||
}
|
||||
let clampedRange = clampedSelectedRange(updatedRange)
|
||||
guard markedTextState.selectedRange != clampedRange else { return }
|
||||
TerminalDebugLog.log(
|
||||
.ime,
|
||||
"setSelectedTextRange range=\(TerminalDebugLog.describe(clampedRange))"
|
||||
)
|
||||
notifySelectionWillChange()
|
||||
markedTextState.setMarkedText(markedTextState.text, selectedRange: clampedRange)
|
||||
notifySelectionDidChange()
|
||||
}
|
||||
|
||||
func text(in range: TerminalTextRange) -> String? {
|
||||
markedTextState.text(in: NSRange(
|
||||
location: range.location,
|
||||
length: range.length
|
||||
))
|
||||
}
|
||||
|
||||
func deleteBackwardInMarkedText() -> Bool {
|
||||
guard let view else { return false }
|
||||
guard markedTextState.hasMarkedText else { return false }
|
||||
let shouldNotifySelectionChange = shouldNotifySelectionChange
|
||||
TerminalDebugLog.log(
|
||||
.ime,
|
||||
"deleteBackwardInMarkedText selected=\(TerminalDebugLog.describe(markedTextState.selectedRange))"
|
||||
)
|
||||
view.inputDelegate?.textWillChange(view)
|
||||
if shouldNotifySelectionChange {
|
||||
view.inputDelegate?.selectionWillChange(view)
|
||||
}
|
||||
|
||||
_ = markedTextState.deleteBackward()
|
||||
view.surface?.preedit(markedTextState.text ?? "")
|
||||
view.refreshInputAccessoryContent()
|
||||
|
||||
if shouldNotifySelectionChange {
|
||||
view.inputDelegate?.selectionDidChange(view)
|
||||
}
|
||||
view.inputDelegate?.textDidChange(view)
|
||||
return true
|
||||
}
|
||||
|
||||
func notifyGeometryDidChange(reason: String) {
|
||||
guard let view else { return }
|
||||
TerminalDebugLog.log(
|
||||
.ime,
|
||||
"notifyGeometryDidChange reason=\(reason) selected=\(TerminalDebugLog.describe(markedTextState.selectedRange)) documentLength=\(markedTextState.documentLength) marked=\(hasMarkedText)"
|
||||
)
|
||||
view.inputDelegate?.selectionWillChange(view)
|
||||
view.inputDelegate?.selectionDidChange(view)
|
||||
if view.isFirstResponder {
|
||||
view.reloadInputViews()
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldNotifySelectionChange: Bool {
|
||||
hasMarkedText
|
||||
|| markedTextState.selectedRange.location != 0
|
||||
|| markedTextState.selectedRange.length != 0
|
||||
}
|
||||
|
||||
private func clampedSelectedRange(_ range: NSRange) -> NSRange {
|
||||
let length = markedTextState.documentLength
|
||||
let location = min(max(range.location, 0), length)
|
||||
let end = min(max(range.location + range.length, location), length)
|
||||
return NSRange(location: location, length: end - location)
|
||||
}
|
||||
|
||||
private func notifySelectionWillChange() {
|
||||
if let view {
|
||||
view.inputDelegate?.selectionWillChange(view)
|
||||
}
|
||||
}
|
||||
|
||||
private func notifySelectionDidChange() {
|
||||
if let view {
|
||||
view.inputDelegate?.selectionDidChange(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user