初始提交: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:
372
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalHardwareKeyRouterTests.swift
vendored
Normal file
372
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalHardwareKeyRouterTests.swift
vendored
Normal file
@@ -0,0 +1,372 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
@testable import GhosttyTerminal
|
||||
import Testing
|
||||
|
||||
struct TerminalHardwareKeyRouterTests {
|
||||
@Test
|
||||
func `routes UI kit arrow keys directly for in memory backends`() {
|
||||
let session = InMemoryTerminalSession(write: { _ in }, resize: { _ in })
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x50,
|
||||
backend: .inMemory(session)
|
||||
) == .data(Data("\u{1B}[D".utf8))
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x52,
|
||||
backend: .inMemory(session)
|
||||
) == .data(Data("\u{1B}[A".utf8))
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x2A,
|
||||
backend: .inMemory(session)
|
||||
) == .data(Data([0x7F]))
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x2B,
|
||||
backend: .inMemory(session)
|
||||
) == .data(Data([0x09]))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `routes UI kit keys to ghostty for exec backends`() {
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x50,
|
||||
backend: .exec
|
||||
) == .ghostty(GHOSTTY_KEY_ARROW_LEFT)
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x04,
|
||||
backend: .exec
|
||||
) == .ghostty(GHOSTTY_KEY_A)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `routes modified UI kit arrow keys to ghostty for in memory backends`() {
|
||||
let session = InMemoryTerminalSession(write: { _ in }, resize: { _ in })
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x50,
|
||||
backend: .inMemory(session),
|
||||
modifiers: .alt
|
||||
) == .ghostty(GHOSTTY_KEY_ARROW_LEFT)
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x4F,
|
||||
backend: .inMemory(session),
|
||||
modifiers: .ctrl
|
||||
) == .ghostty(GHOSTTY_KEY_ARROW_RIGHT)
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x29,
|
||||
backend: .inMemory(session),
|
||||
modifiers: .super_
|
||||
) == .ghostty(GHOSTTY_KEY_ESCAPE)
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x2B,
|
||||
backend: .inMemory(session),
|
||||
modifiers: .shift
|
||||
) == .ghostty(GHOSTTY_KEY_TAB)
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeUIKit(
|
||||
usage: 0x2A,
|
||||
backend: .inMemory(session),
|
||||
modifiers: .alt
|
||||
) == .ghostty(GHOSTTY_KEY_BACKSPACE)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `routes app kit arrow keys directly for in memory backends`() {
|
||||
let session = InMemoryTerminalSession(write: { _ in }, resize: { _ in })
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeAppKit(
|
||||
keyCode: 0x7B,
|
||||
backend: .inMemory(session)
|
||||
) == .data(Data("\u{1B}[D".utf8))
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeAppKit(
|
||||
keyCode: 0x75,
|
||||
backend: .inMemory(session)
|
||||
) == .data(Data("\u{1B}[3~".utf8))
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeAppKit(
|
||||
keyCode: 0x30,
|
||||
backend: .inMemory(session)
|
||||
) == .data(Data([0x09]))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `routes app kit keys to ghostty for exec backends`() {
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeAppKit(
|
||||
keyCode: 0x7B,
|
||||
backend: .exec
|
||||
) == .ghostty(GHOSTTY_KEY_ARROW_LEFT)
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeAppKit(
|
||||
keyCode: 0x33,
|
||||
backend: .exec
|
||||
) == .ghostty(GHOSTTY_KEY_BACKSPACE)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `app kit interpreted commands are replayed as key events`() {
|
||||
#expect(
|
||||
TerminalKeyEventHandler.shouldReplayInterpretedCommand(
|
||||
#selector(NSResponder.insertTab(_:))
|
||||
)
|
||||
)
|
||||
#expect(
|
||||
TerminalKeyEventHandler.shouldReplayInterpretedCommand(
|
||||
NSSelectorFromString("insertBacktab:")
|
||||
)
|
||||
)
|
||||
#expect(
|
||||
TerminalKeyEventHandler.shouldReplayInterpretedCommand(
|
||||
#selector(NSResponder.moveUp(_:))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// Quote HID 0x34 must translate to AppKit keycode 0x27, not fall
|
||||
/// through to `0` (which is AppKit's keycode for the `A` key) nor to
|
||||
/// `GHOSTTY_KEY_QUOTE.rawValue` (which happens to equal AppKit's
|
||||
/// keycode for Tab — the original bug).
|
||||
@Test
|
||||
func `app kit key code for UI kit translates quote to mac keycode`() {
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x34) == 0x27
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `app kit key code for UI kit translates common keys`() {
|
||||
// Letter A: HID 0x04 → AppKit 0x00
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x04) == 0x00
|
||||
)
|
||||
// Tab: HID 0x2B → AppKit 0x30
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x2B) == 0x30
|
||||
)
|
||||
// Enter: HID 0x28 → AppKit 0x24
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x28) == 0x24
|
||||
)
|
||||
// ArrowUp: HID 0x52 → AppKit 0x7E
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x52) == 0x7E
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `app kit key code for ghostty keys translates common keys`() {
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_A) == 0x00
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_TAB) == 0x30
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_ESCAPE) == 0x35
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_ARROW_LEFT) == 0x7B
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `app kit key code for ghostty keys translates higher function and volume keys`() {
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_F17) == 0x40
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_F18) == 0x4F
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_F19) == 0x50
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_F20) == 0x5A
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_AUDIO_VOLUME_UP) == 0x48
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_AUDIO_VOLUME_DOWN) == 0x49
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_AUDIO_VOLUME_MUTE) == 0x4A
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `app kit key code for ghostty keys returns sentinel for keys absent from mac`() {
|
||||
let sentinel = TerminalHardwareKeyRouter.unidentifiedAppKitKeyCode
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_CONTEXT_MENU) == sentinel
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_INSERT) == sentinel
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_CUT) == sentinel
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCode(for: GHOSTTY_KEY_INTL_BACKSLASH) == sentinel
|
||||
)
|
||||
}
|
||||
|
||||
/// HID usages that have no AppKit counterpart must not collapse to `0`
|
||||
/// (AppKit's keycode for `A`). They must return the sentinel so
|
||||
/// libghostty's native-keycode lookup resolves them to `.unidentified`.
|
||||
@Test
|
||||
func `app kit key code for UI kit returns sentinel for keys absent from mac`() {
|
||||
let sentinel = TerminalHardwareKeyRouter.unidentifiedAppKitKeyCode
|
||||
// CUT, COPY, PASTE, CONTEXT_MENU, INSERT, PRINT_SCREEN, SCROLL_LOCK,
|
||||
// PAUSE and the higher function keys past F20 are in uiKitMap but
|
||||
// have no AppKit virtual keycode.
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x7B) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x7C) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x7D) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x65) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x49) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x46) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x47) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x48) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x70) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x71) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x72) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x73) == sentinel)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `app kit key code for UI kit translates divergent and higher function keys`() {
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x53) == 0x47
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x6C) == 0x40
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x6D) == 0x4F
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x6E) == 0x50
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x6F) == 0x5A
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `app kit key code for UI kit translates volume keys`() {
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x7F) == 0x4A
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x80) == 0x48
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x81) == 0x49
|
||||
)
|
||||
}
|
||||
|
||||
/// The pinned Ghostty keycode table resolves the international backslash
|
||||
/// HID usage (`0x64`) to AppKit's ISO section keycode (`0x0A`).
|
||||
@Test
|
||||
func `app kit key code for UI kit translates intl backslash key`() {
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x64) == 0x0A
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x32)
|
||||
== TerminalHardwareKeyRouter.unidentifiedAppKitKeyCode
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `route app kit recognizes higher function and volume keys`() {
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeAppKit(
|
||||
keyCode: 0x40,
|
||||
backend: .exec
|
||||
) == .ghostty(GHOSTTY_KEY_F17)
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeAppKit(
|
||||
keyCode: 0x5A,
|
||||
backend: .exec
|
||||
) == .ghostty(GHOSTTY_KEY_F20)
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeAppKit(
|
||||
keyCode: 0x48,
|
||||
backend: .exec
|
||||
) == .ghostty(GHOSTTY_KEY_AUDIO_VOLUME_UP)
|
||||
)
|
||||
#expect(
|
||||
TerminalHardwareKeyRouter.routeAppKit(
|
||||
keyCode: 0x4A,
|
||||
backend: .exec
|
||||
) == .ghostty(GHOSTTY_KEY_AUDIO_VOLUME_MUTE)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `app kit key code for UI kit returns sentinel for unknown HID`() {
|
||||
// HID usages not in uiKitMap at all.
|
||||
let sentinel = TerminalHardwareKeyRouter.unidentifiedAppKitKeyCode
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0xFFFE) == sentinel)
|
||||
#expect(TerminalHardwareKeyRouter.appKitKeyCodeForUIKit(usage: 0x0001) == sentinel)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `app kit direct input requires no modifiers`() {
|
||||
#expect(
|
||||
TerminalKeyEventHandler.shouldUseDirectInput(
|
||||
modifierFlags: []
|
||||
)
|
||||
)
|
||||
#expect(
|
||||
!TerminalKeyEventHandler.shouldUseDirectInput(
|
||||
modifierFlags: [.shift]
|
||||
)
|
||||
)
|
||||
#expect(
|
||||
!TerminalKeyEventHandler.shouldUseDirectInput(
|
||||
modifierFlags: [.control]
|
||||
)
|
||||
)
|
||||
#expect(
|
||||
!TerminalKeyEventHandler.shouldUseDirectInput(
|
||||
modifierFlags: [.option]
|
||||
)
|
||||
)
|
||||
#expect(
|
||||
!TerminalKeyEventHandler.shouldUseDirectInput(
|
||||
modifierFlags: [.command]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user