初始提交: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:
130
vendor/libghostty-spm/Sources/GhosttyTerminal/Platform/AppKit/TerminalTextInputHandler@AppKit.swift
vendored
Normal file
130
vendor/libghostty-spm/Sources/GhosttyTerminal/Platform/AppKit/TerminalTextInputHandler@AppKit.swift
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// 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
|
||||
Reference in New Issue
Block a user