Files
terminalX/vendor/libghostty-spm/Sources/GhosttyTerminal/Platform/AppKit/TerminalTextInputHandler@AppKit.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

131 lines
3.7 KiB
Swift

//
// TerminalTextInputHandler@AppKit.swift
// libghostty-spm
//
// Created by Lakr233 on 2026/3/16.
// Reference:
// - ghostty-org/ghostty
// - macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift
// IME text accumulation intentionally follows Ghostty's AppKit flow:
// marked/preedit text is passed through as-is, without extra app-specific
// filtering, so composition behavior stays consistent with upstream.
//
#if canImport(AppKit) && !canImport(UIKit)
import AppKit
import GhosttyKit
@MainActor
final class TerminalTextInputHandler: NSObject {
private weak var view: AppTerminalView?
private var markedTextState = TerminalMarkedTextState()
private var accumulatedTexts: [String]?
var hasMarkedText: Bool {
markedTextState.hasMarkedText
}
init(view: AppTerminalView) {
self.view = view
super.init()
}
func startCollectingText() {
accumulatedTexts = []
}
func finishCollectingText() -> [String]? {
defer { accumulatedTexts = nil }
guard let texts = accumulatedTexts, !texts.isEmpty else { return nil }
return texts
}
// MARK: - Text Input
func insertText(_ string: Any) {
guard NSApp.currentEvent != nil else { return }
let text: String
if let attrStr = string as? NSAttributedString {
text = attrStr.string
} else if let str = string as? String {
text = str
} else {
return
}
unmarkText()
if accumulatedTexts != nil {
accumulatedTexts?.append(text)
} else {
view?.surface?.sendText(text)
}
}
func setMarkedText(
_ string: Any,
selectedRange: NSRange
) {
let text: String
if let attrStr = string as? NSAttributedString {
text = attrStr.string
} else if let str = string as? String {
text = str
} else {
return
}
markedTextState.setMarkedText(text, selectedRange: selectedRange)
if accumulatedTexts == nil {
syncPreedit()
}
}
func unmarkText() {
guard markedTextState.hasMarkedText else { return }
markedTextState.clear()
syncPreedit()
}
func currentSelectedRange() -> NSRange {
markedTextState.currentSelectedRange
}
func markedRange() -> NSRange {
markedTextState.markedRange
}
func attributedSubstring(
forProposedRange range: NSRange,
actualRange: NSRangePointer?
) -> NSAttributedString? {
guard markedTextState.hasMarkedText else {
return nil
}
let length = markedTextState.documentLength
let location = min(max(range.location, 0), length)
let end = min(max(range.location + range.length, location), length)
let clampedRange = NSRange(location: location, length: end - location)
actualRange?.pointee = clampedRange
guard let text = markedTextState.text(in: clampedRange) else {
return nil
}
return NSAttributedString(string: text)
}
func syncPreedit(clearIfNeeded: Bool = true) {
guard let text = markedTextState.text else {
guard clearIfNeeded else { return }
view?.surface?.preedit("")
return
}
view?.surface?.preedit(text)
}
}
#endif