Files
terminalX/vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalHardwareKeyRouterTests.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

373 lines
13 KiB
Swift

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]
)
)
}
}