初始提交: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:
158
vendor/libghostty-spm/Tests/GhosttyKitTest/InMemoryTerminalSessionOutputQueueTests.swift
vendored
Normal file
158
vendor/libghostty-spm/Tests/GhosttyKitTest/InMemoryTerminalSessionOutputQueueTests.swift
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
@testable import GhosttyTerminal
|
||||
import Darwin
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
import Testing
|
||||
|
||||
struct InMemoryTerminalSessionOutputQueueTests {
|
||||
@Test
|
||||
func `receive returns before surface write completes`() {
|
||||
let writeStarted = DispatchSemaphore(value: 0)
|
||||
let allowWriteToFinish = DispatchSemaphore(value: 0)
|
||||
let session = makeSession { _, _ in
|
||||
writeStarted.signal()
|
||||
allowWriteToFinish.wait()
|
||||
}
|
||||
session.setSurface(testSurface(1))
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
|
||||
allowWriteToFinish.signal()
|
||||
}
|
||||
|
||||
let start = ProcessInfo.processInfo.systemUptime
|
||||
session.receive(Data("hello".utf8))
|
||||
let elapsed = ProcessInfo.processInfo.systemUptime - start
|
||||
|
||||
#expect(elapsed < 0.2)
|
||||
#expect(writeStarted.wait(timeout: .now() + 1) == .success)
|
||||
allowWriteToFinish.signal()
|
||||
session.waitForPendingOutput()
|
||||
}
|
||||
|
||||
@Test
|
||||
func `writes and process exit preserve enqueue order`() {
|
||||
let events = LockedValues<String>()
|
||||
let session = InMemoryTerminalSession(
|
||||
write: { _ in },
|
||||
resize: { _ in },
|
||||
surfaceWrite: { _, data in
|
||||
events.append(String(decoding: data, as: UTF8.self))
|
||||
},
|
||||
processExit: { _, exitCode, runtimeMilliseconds in
|
||||
events.append("exit:\(exitCode):\(runtimeMilliseconds)")
|
||||
}
|
||||
)
|
||||
session.setSurface(testSurface(2))
|
||||
|
||||
session.receive("first")
|
||||
session.receive("second")
|
||||
session.finish(exitCode: 7, runtimeMilliseconds: 42)
|
||||
session.waitForPendingOutput()
|
||||
|
||||
#expect(events.values == ["first", "second", "exit:7:42"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `surface teardown waits for active write and drops queued stale writes`() {
|
||||
let firstWriteStarted = DispatchSemaphore(value: 0)
|
||||
let allowFirstWriteToFinish = DispatchSemaphore(value: 0)
|
||||
let clearFinished = DispatchSemaphore(value: 0)
|
||||
let writes = LockedValues<String>()
|
||||
let surface = SendableSurface(testSurface(3))
|
||||
let session = makeSession { _, data in
|
||||
let value = String(decoding: data, as: UTF8.self)
|
||||
writes.append(value)
|
||||
if value == "first" {
|
||||
firstWriteStarted.signal()
|
||||
allowFirstWriteToFinish.wait()
|
||||
}
|
||||
}
|
||||
session.setSurface(surface.rawValue)
|
||||
session.receive("first")
|
||||
session.receive("stale")
|
||||
#expect(firstWriteStarted.wait(timeout: .now() + 1) == .success)
|
||||
|
||||
DispatchQueue.global().async {
|
||||
session.clearSurface(ifMatches: surface.rawValue)
|
||||
clearFinished.signal()
|
||||
}
|
||||
|
||||
let clearDeadline = ProcessInfo.processInfo.systemUptime + 1
|
||||
while session.currentSurface != nil,
|
||||
ProcessInfo.processInfo.systemUptime < clearDeadline
|
||||
{
|
||||
sched_yield()
|
||||
}
|
||||
#expect(session.currentSurface == nil)
|
||||
|
||||
allowFirstWriteToFinish.signal()
|
||||
#expect(clearFinished.wait(timeout: .now() + 1) == .success)
|
||||
session.waitForPendingOutput()
|
||||
|
||||
#expect(writes.values == ["first"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `blocked session does not block another session`() {
|
||||
let firstWriteStarted = DispatchSemaphore(value: 0)
|
||||
let allowFirstWriteToFinish = DispatchSemaphore(value: 0)
|
||||
let secondWriteFinished = DispatchSemaphore(value: 0)
|
||||
let firstSession = makeSession { _, _ in
|
||||
firstWriteStarted.signal()
|
||||
allowFirstWriteToFinish.wait()
|
||||
}
|
||||
let secondSession = makeSession { _, _ in
|
||||
secondWriteFinished.signal()
|
||||
}
|
||||
firstSession.setSurface(testSurface(4))
|
||||
secondSession.setSurface(testSurface(5))
|
||||
|
||||
firstSession.receive("blocked")
|
||||
#expect(firstWriteStarted.wait(timeout: .now() + 1) == .success)
|
||||
secondSession.receive("independent")
|
||||
|
||||
#expect(secondWriteFinished.wait(timeout: .now() + 1) == .success)
|
||||
allowFirstWriteToFinish.signal()
|
||||
firstSession.waitForPendingOutput()
|
||||
secondSession.waitForPendingOutput()
|
||||
}
|
||||
}
|
||||
|
||||
private func makeSession(
|
||||
surfaceWrite: @escaping InMemoryTerminalSurfaceAccess.Write
|
||||
) -> InMemoryTerminalSession {
|
||||
InMemoryTerminalSession(
|
||||
write: { _ in },
|
||||
resize: { _ in },
|
||||
surfaceWrite: surfaceWrite
|
||||
)
|
||||
}
|
||||
|
||||
private func testSurface(_ address: Int) -> ghostty_surface_t {
|
||||
UnsafeMutableRawPointer(bitPattern: address)!
|
||||
}
|
||||
|
||||
private struct SendableSurface: @unchecked Sendable {
|
||||
let rawValue: ghostty_surface_t
|
||||
|
||||
init(_ rawValue: ghostty_surface_t) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
}
|
||||
|
||||
private final class LockedValues<Value: Sendable>: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var storage: [Value] = []
|
||||
|
||||
var values: [Value] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return storage
|
||||
}
|
||||
|
||||
func append(_ value: Value) {
|
||||
lock.lock()
|
||||
storage.append(value)
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
25
vendor/libghostty-spm/Tests/GhosttyKitTest/InMemoryTerminalSessionViewportTests.swift
vendored
Normal file
25
vendor/libghostty-spm/Tests/GhosttyKitTest/InMemoryTerminalSessionViewportTests.swift
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
@testable import GhosttyTerminal
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
struct InMemoryTerminalSessionViewportTests {
|
||||
/// `readViewportText()` MUST return `nil` (not crash) when no surface is
|
||||
/// attached. This is the canonical pre-surface / post-surface-teardown
|
||||
/// state — consumers may call `readViewportText` from any thread that
|
||||
/// holds a reference, and the contract is "nil means no surface."
|
||||
@Test
|
||||
func `read viewport text returns nil before surface attached`() {
|
||||
let session = InMemoryTerminalSession(write: { _ in }, resize: { _ in })
|
||||
#expect(session.readViewportText() == nil)
|
||||
}
|
||||
|
||||
/// After clearing the surface, the read MUST go back to returning `nil`.
|
||||
/// Together with the test above this pins the surface-presence semantics
|
||||
/// of the public API.
|
||||
@Test
|
||||
func `read viewport text returns nil after surface cleared`() {
|
||||
let session = InMemoryTerminalSession(write: { _ in }, resize: { _ in })
|
||||
session.clearSurface(ifMatches: nil)
|
||||
#expect(session.readViewportText() == nil)
|
||||
}
|
||||
}
|
||||
446
vendor/libghostty-spm/Tests/GhosttyKitTest/ShellCraftKitTests.swift
vendored
Normal file
446
vendor/libghostty-spm/Tests/GhosttyKitTest/ShellCraftKitTests.swift
vendored
Normal file
@@ -0,0 +1,446 @@
|
||||
import Foundation
|
||||
import GhosttyTerminal
|
||||
@testable import ShellCraftKit
|
||||
import Testing
|
||||
|
||||
struct ShellCraftKitTests {
|
||||
@Test
|
||||
func `styled prompt uses visible column width`() {
|
||||
let shell = ShellDefinition(
|
||||
prompt: "\u{1B}[38;5;110mcolor\u{1B}[0m > ",
|
||||
welcomeMessage: ""
|
||||
) {}
|
||||
|
||||
#expect(shell.promptDisplayWidth == 8)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `terminal display width counts wide characters`() {
|
||||
#expect("abc".terminalDisplayWidth == 3)
|
||||
#expect("你好".terminalDisplayWidth == 4)
|
||||
#expect("a你b好".terminalDisplayWidth == 6)
|
||||
#expect("\u{1B}[31m红色\u{1B}[0m".terminalDisplayWidth == 4)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `cursor column uses display width instead of character count`() {
|
||||
#expect(
|
||||
terminalCursorColumn(
|
||||
promptDisplayWidth: 8,
|
||||
input: "测试",
|
||||
cursorPosition: 2
|
||||
) == 13
|
||||
)
|
||||
|
||||
#expect(
|
||||
terminalCursorColumn(
|
||||
promptDisplayWidth: 8,
|
||||
input: "a测b",
|
||||
cursorPosition: 2
|
||||
) == 12
|
||||
)
|
||||
|
||||
#expect(
|
||||
terminalCursorColumn(
|
||||
promptDisplayWidth: 8,
|
||||
input: "你好吗",
|
||||
cursorPosition: 1
|
||||
) == 11
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `rendered input state tracks wrapped lines and cursor placement`() {
|
||||
let state = terminalRenderedInputState(
|
||||
promptDisplayWidth: 18,
|
||||
input: "hello world",
|
||||
cursorPosition: 11,
|
||||
terminalColumns: 20
|
||||
)
|
||||
|
||||
#expect(state.totalLineCount == 2)
|
||||
#expect(state.cursorLineOffset == 1)
|
||||
#expect(state.cursorColumn == 10)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `rendered input state handles prompt only wrapping`() {
|
||||
let state = terminalRenderedInputState(
|
||||
promptDisplayWidth: 18,
|
||||
input: "",
|
||||
cursorPosition: 0,
|
||||
terminalColumns: 10
|
||||
)
|
||||
|
||||
#expect(state.totalLineCount == 2)
|
||||
#expect(state.cursorLineOffset == 1)
|
||||
#expect(state.cursorColumn == 9)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `wrapped terminal line count handles exact boundary`() {
|
||||
#expect(wrappedTerminalLineCount(displayWidth: 20, terminalColumns: 20) == 1)
|
||||
#expect(wrappedTerminalLineCount(displayWidth: 21, terminalColumns: 20) == 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `rendered input state keeps cursor on boundary without trailing content`() {
|
||||
let state = terminalRenderedInputState(
|
||||
promptDisplayWidth: 18,
|
||||
input: "ab",
|
||||
cursorPosition: 2,
|
||||
terminalColumns: 20
|
||||
)
|
||||
|
||||
#expect(state.totalLineCount == 1)
|
||||
#expect(state.cursorLineOffset == 0)
|
||||
#expect(state.cursorColumn == 20)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `rendered input state wraps boundary cursor when trailing content exists`() {
|
||||
let state = terminalRenderedInputState(
|
||||
promptDisplayWidth: 18,
|
||||
input: "abc",
|
||||
cursorPosition: 2,
|
||||
terminalColumns: 20
|
||||
)
|
||||
|
||||
#expect(state.totalLineCount == 2)
|
||||
#expect(state.cursorLineOffset == 1)
|
||||
#expect(state.cursorColumn == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `incremental append is allowed for tail insertion`() {
|
||||
#expect(
|
||||
canIncrementallyAppendInput(
|
||||
previousInput: "hello",
|
||||
previousCursorPosition: 5,
|
||||
insertedText: " world"
|
||||
)
|
||||
)
|
||||
#expect(
|
||||
canIncrementallyAppendInput(
|
||||
previousInput: "ni",
|
||||
previousCursorPosition: 2,
|
||||
insertedText: "你好"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `incremental append falls back for mid line or control input`() {
|
||||
#expect(
|
||||
!canIncrementallyAppendInput(
|
||||
previousInput: "hello",
|
||||
previousCursorPosition: 2,
|
||||
insertedText: "X"
|
||||
)
|
||||
)
|
||||
#expect(
|
||||
!canIncrementallyAppendInput(
|
||||
previousInput: "hello",
|
||||
previousCursorPosition: 5,
|
||||
insertedText: "\t"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `tab expansion uses visible cursor column`() {
|
||||
#expect(
|
||||
terminalExpandedTabText(
|
||||
promptDisplayWidth: 2,
|
||||
input: "abc",
|
||||
cursorPosition: 3,
|
||||
terminalColumns: 80
|
||||
) == " "
|
||||
)
|
||||
#expect(
|
||||
terminalExpandedTabText(
|
||||
promptDisplayWidth: 7,
|
||||
input: "",
|
||||
cursorPosition: 0,
|
||||
terminalColumns: 80
|
||||
) == " "
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `tab expansion respects wrapped cursor column`() {
|
||||
#expect(
|
||||
terminalExpandedTabText(
|
||||
promptDisplayWidth: 18,
|
||||
input: "ab",
|
||||
cursorPosition: 2,
|
||||
terminalColumns: 20
|
||||
) == String(repeating: " ", count: 5)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `meta editing action recognizes word sequences`() {
|
||||
#expect(terminalMetaEditingAction(for: 0x7F) == .deleteBackwardWord)
|
||||
#expect(terminalMetaEditingAction(for: 0x08) == .deleteBackwardWord)
|
||||
#expect(terminalMetaEditingAction(for: 0x62) == .moveBackwardWord)
|
||||
#expect(terminalMetaEditingAction(for: 0x66) == .moveForwardWord)
|
||||
#expect(terminalMetaEditingAction(for: 0x64) == .deleteForwardWord)
|
||||
#expect(terminalMetaEditingAction(for: 0x42) == nil)
|
||||
#expect(terminalMetaEditingAction(for: 0x78) == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `csi editing action recognizes modified arrow word sequences`() {
|
||||
#expect(
|
||||
terminalCSIEditingAction(params: Data("1;3".utf8), finalByte: 0x44)
|
||||
== .moveCursorBackwardWord
|
||||
)
|
||||
#expect(
|
||||
terminalCSIEditingAction(params: Data("1;3".utf8), finalByte: 0x43)
|
||||
== .moveCursorForwardWord
|
||||
)
|
||||
#expect(
|
||||
terminalCSIEditingAction(params: Data(), finalByte: 0x44)
|
||||
== .moveCursorLeft
|
||||
)
|
||||
#expect(
|
||||
terminalCSIEditingAction(params: Data(), finalByte: 0x43)
|
||||
== .moveCursorRight
|
||||
)
|
||||
#expect(
|
||||
terminalCSIEditingAction(params: Data("3".utf8), finalByte: 0x7E)
|
||||
== .deleteForward
|
||||
)
|
||||
#expect(
|
||||
terminalCSIEditingAction(params: Data("1;4".utf8), finalByte: 0x44)
|
||||
== .moveCursorBackwardWord
|
||||
)
|
||||
#expect(
|
||||
terminalCSIEditingAction(params: Data("3;3".utf8), finalByte: 0x7E)
|
||||
== .deleteForwardWord
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `csi modifier detection matches alt suffix`() {
|
||||
#expect(terminalCSIHasAltModifier(Data("1;3".utf8)))
|
||||
#expect(terminalCSIHasAltModifier(Data("1;4".utf8)))
|
||||
#expect(!terminalCSIHasAltModifier(Data("1;5".utf8)))
|
||||
#expect(!terminalCSIHasAltModifier(Data("3".utf8)))
|
||||
#expect(!terminalCSIHasAltModifier(Data()))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `word boundaries treat punctuation as separators for meta motion`() {
|
||||
#expect(terminalPreviousWordBoundary(in: "alpha beta", from: 10) == 6)
|
||||
#expect(terminalPreviousWordBoundary(in: "alpha beta ", from: 12) == 6)
|
||||
#expect(terminalNextWordBoundary(in: "alpha beta", from: 0) == 5)
|
||||
#expect(terminalNextWordBoundary(in: "alpha beta", from: 5) == 12)
|
||||
#expect(terminalPreviousWordBoundary(in: "foo-bar", from: 7) == 4)
|
||||
#expect(terminalNextWordBoundary(in: "foo-bar", from: 0) == 3)
|
||||
#expect(terminalPreviousWordBoundary(in: "héllo wörld", from: 11) == 6)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `shell word boundaries remain whitespace delimited for control W`() {
|
||||
#expect(terminalPreviousShellWordBoundary(in: "foo-bar baz", from: 11) == 8)
|
||||
#expect(terminalPreviousShellWordBoundary(in: "alpha beta ", from: 12) == 6)
|
||||
#expect(terminalNextShellWordBoundary(in: "alpha beta", from: 5) == 12)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `delete backward word removes previous word and trailing spaces`() {
|
||||
let result = terminalDeleteBackwardWord(
|
||||
input: "alpha beta ",
|
||||
cursorPosition: 12
|
||||
)
|
||||
|
||||
#expect(result.input == "alpha ")
|
||||
#expect(result.cursorPosition == 6)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `delete forward word removes next word and leading spaces`() {
|
||||
let result = terminalDeleteForwardWord(
|
||||
input: "alpha beta gamma",
|
||||
cursorPosition: 5
|
||||
)
|
||||
|
||||
#expect(result.input == "alpha gamma")
|
||||
#expect(result.cursorPosition == 5)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `delete backward shell word removes previous whitespace delimited token`() {
|
||||
let result = terminalDeleteBackwardShellWord(
|
||||
input: "foo-bar baz ",
|
||||
cursorPosition: 13
|
||||
)
|
||||
|
||||
#expect(result.input == "foo-bar ")
|
||||
#expect(result.cursorPosition == 8)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `sandbox shell supports exit and styled fallback`() {
|
||||
let viewport = InMemoryTerminalViewport(
|
||||
columns: 80,
|
||||
rows: 24,
|
||||
widthPixels: 0,
|
||||
heightPixels: 0
|
||||
)
|
||||
|
||||
switch defaultSandboxShell.processCommand(
|
||||
"exit",
|
||||
username: "tester",
|
||||
terminalSize: viewport
|
||||
) {
|
||||
case .exit:
|
||||
break
|
||||
|
||||
default:
|
||||
Issue.record("expected sandbox shell exit command to terminate the session")
|
||||
}
|
||||
|
||||
if case let .output(message) = defaultSandboxShell.processCommand(
|
||||
"missing-command",
|
||||
username: "tester",
|
||||
terminalSize: viewport
|
||||
) {
|
||||
#expect(message.contains("\u{1B}["))
|
||||
#expect(message.contains("missing-command"))
|
||||
} else {
|
||||
Issue.record("expected fallback command result to produce output")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - decodeUTF8Incrementally
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental decodes complete ASCII`() {
|
||||
let (text, leftover) = decodeUTF8Incrementally(Data("hello".utf8))
|
||||
#expect(text == "hello")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental decodes complete chinese`() {
|
||||
let (text, leftover) = decodeUTF8Incrementally(Data("你好".utf8))
|
||||
#expect(text == "你好")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental retains incomplete three byte sequence`() {
|
||||
// "你" is E4 BD A0 — send only first 2 bytes
|
||||
let partial = Data([0xE4, 0xBD])
|
||||
let (text1, leftover1) = decodeUTF8Incrementally(partial)
|
||||
#expect(text1 == "")
|
||||
#expect(leftover1 == partial)
|
||||
|
||||
// Now complete the sequence
|
||||
let full = leftover1 + Data([0xA0])
|
||||
let (text2, leftover2) = decodeUTF8Incrementally(full)
|
||||
#expect(text2 == "你")
|
||||
#expect(leftover2.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental retains incomplete four byte sequence`() {
|
||||
// 😀 is F0 9F 98 80 — send only first 3 bytes
|
||||
let partial = Data([0xF0, 0x9F, 0x98])
|
||||
let (text1, leftover1) = decodeUTF8Incrementally(partial)
|
||||
#expect(text1 == "")
|
||||
#expect(leftover1 == partial)
|
||||
|
||||
let full = leftover1 + Data([0x80])
|
||||
let (text2, leftover2) = decodeUTF8Incrementally(full)
|
||||
#expect(text2 == "😀")
|
||||
#expect(leftover2.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental skips illegal lead byte FF`() {
|
||||
let (text, leftover) = decodeUTF8Incrementally(Data([0xFF]))
|
||||
#expect(text == "")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental skips overlong lead C 0 C 1`() {
|
||||
// 0xC0 and 0xC1 are overlong, should be skipped
|
||||
let input = Data([0xC0, 0x41, 0xC1, 0x42])
|
||||
let (text, leftover) = decodeUTF8Incrementally(input)
|
||||
#expect(text == "AB")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental skips lead F 5 plus`() {
|
||||
let input = Data([0xF5, 0x41, 0xF6, 0x42, 0xF7, 0x43])
|
||||
let (text, leftover) = decodeUTF8Incrementally(input)
|
||||
#expect(text == "ABC")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental skips only lead byte on invalid combination`() {
|
||||
// 0xE4 expects 2 continuation bytes, but next bytes are ASCII
|
||||
let input = Data([0xE4, 0x41, 0x42])
|
||||
let (text, leftover) = decodeUTF8Incrementally(input)
|
||||
#expect(text == "AB")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental preserves valid text after illegal byte`() {
|
||||
let input = Data([0xFF, 0x68, 0x65, 0x6C, 0x6C, 0x6F]) // 0xFF + "hello"
|
||||
let (text, leftover) = decodeUTF8Incrementally(input)
|
||||
#expect(text == "hello")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental handles empty data`() {
|
||||
let (text, leftover) = decodeUTF8Incrementally(Data())
|
||||
#expect(text == "")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental handles mixed valid and incomplete`() {
|
||||
// "ab" + incomplete 3-byte lead
|
||||
let input = Data([0x61, 0x62, 0xE4, 0xBD])
|
||||
let (text, leftover) = decodeUTF8Incrementally(input)
|
||||
#expect(text == "ab")
|
||||
#expect(leftover == Data([0xE4, 0xBD]))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental decomposed unicode`() {
|
||||
// e (0x65) + combining acute accent U+0301 (0xCC 0x81)
|
||||
let input = Data([0x65, 0xCC, 0x81])
|
||||
let (text, leftover) = decodeUTF8Incrementally(input)
|
||||
#expect(text == "e\u{0301}")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental skips lead with non continuation tail`() {
|
||||
// 0xE4 is 3-byte lead, but 0x41 is ASCII — not a valid continuation
|
||||
let input = Data([0xE4, 0x41])
|
||||
let (text, leftover) = decodeUTF8Incrementally(input)
|
||||
#expect(text == "A")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `utf 8 incremental skips four byte lead with invalid tail`() {
|
||||
// 0xF0 is 4-byte lead, but 0x41/0x42 are ASCII
|
||||
let input = Data([0xF0, 0x41, 0x42])
|
||||
let (text, leftover) = decodeUTF8Incrementally(input)
|
||||
#expect(text == "AB")
|
||||
#expect(leftover.isEmpty)
|
||||
}
|
||||
}
|
||||
43
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalDebugLogTests.swift
vendored
Normal file
43
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalDebugLogTests.swift
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
@testable import GhosttyTerminal
|
||||
import Testing
|
||||
|
||||
@Suite(.serialized)
|
||||
struct TerminalDebugLogTests {
|
||||
@Test
|
||||
func `logging is disabled by default`() {
|
||||
withRestoredDebugLogState {
|
||||
#expect(!TerminalDebugLog.isEnabled)
|
||||
#expect(TerminalDebugLog.categories == .standard)
|
||||
#expect(!TerminalDebugLog.categories.contains(.render))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `logging can be toggled and reconfigured`() {
|
||||
withRestoredDebugLogState {
|
||||
TerminalDebugLog.disable()
|
||||
#expect(!TerminalDebugLog.isEnabled)
|
||||
|
||||
TerminalDebugLog.enable(.all)
|
||||
TerminalDebugLog.sink = { _ in }
|
||||
|
||||
#expect(TerminalDebugLog.isEnabled)
|
||||
#expect(TerminalDebugLog.categories == .all)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func withRestoredDebugLogState(
|
||||
_ body: () -> Void
|
||||
) {
|
||||
let originalEnabled = TerminalDebugLog.isEnabled
|
||||
let originalCategories = TerminalDebugLog.categories
|
||||
let originalSink = TerminalDebugLog.sink
|
||||
defer {
|
||||
TerminalDebugLog.isEnabled = originalEnabled
|
||||
TerminalDebugLog.categories = originalCategories
|
||||
TerminalDebugLog.sink = originalSink
|
||||
}
|
||||
|
||||
body()
|
||||
}
|
||||
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]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
37
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalInputTextTests.swift
vendored
Normal file
37
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalInputTextTests.swift
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
@testable import GhosttyTerminal
|
||||
import Testing
|
||||
|
||||
struct TerminalInputTextTests {
|
||||
@Test
|
||||
func `filters apple private use function keys from text path`() {
|
||||
#expect(TerminalInputText.filteredFunctionKeyText("\u{F702}") == nil)
|
||||
#expect(TerminalInputText.filteredFunctionKeyText("\u{F703}") == nil)
|
||||
#expect(TerminalInputText.filteredFunctionKeyText("UIKeyInputLeftArrow") == nil)
|
||||
#expect(TerminalInputText.filteredFunctionKeyText("UIKeyInputUpArrow") == nil)
|
||||
#expect(TerminalInputText.filteredFunctionKeyText("a") == "a")
|
||||
#expect(TerminalInputText.filteredFunctionKeyText("你好") == "你好")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `recognizes private use function key scalars`() {
|
||||
#expect(TerminalInputText.isPrivateUseFunctionKey("\u{F702}"))
|
||||
#expect(TerminalInputText.isPrivateUseFunctionKey("\u{F703}"))
|
||||
#expect(!TerminalInputText.isPrivateUseFunctionKey("a"))
|
||||
#expect(!TerminalInputText.isPrivateUseFunctionKey("你"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `recognizes UI kit named function keys`() {
|
||||
#expect(TerminalInputText.isUIKitNamedFunctionKey("UIKeyInputLeftArrow"))
|
||||
#expect(TerminalInputText.isUIKitNamedFunctionKey("UIKeyInputDownArrow"))
|
||||
#expect(!TerminalInputText.isUIKitNamedFunctionKey("a"))
|
||||
#expect(!TerminalInputText.isUIKitNamedFunctionKey("你好"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `counts paste lines for diagnostics only`() {
|
||||
#expect(TerminalInputText.lineCount(in: "") == 0)
|
||||
#expect(TerminalInputText.lineCount(in: "echo ok") == 0)
|
||||
#expect(TerminalInputText.lineCount(in: "line 1\nline 2\nline 3") == 2)
|
||||
}
|
||||
}
|
||||
94
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalLifecycleTests.swift
vendored
Normal file
94
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalLifecycleTests.swift
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
@testable import GhosttyTerminal
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
struct TerminalLifecycleTests {
|
||||
@Test
|
||||
func `failed surface creation does not retain bridge`() {
|
||||
let controller = TerminalController()
|
||||
let bridge = TerminalCallbackBridge()
|
||||
|
||||
let surface = controller.createSurface(
|
||||
bridge: bridge,
|
||||
configuration: .init()
|
||||
) { _ in }
|
||||
|
||||
#expect(surface == nil)
|
||||
#expect(controller.retainedBridgeCount == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `switching controllers removes bridge from old controller`() {
|
||||
let oldController = TerminalController()
|
||||
let newController = TerminalController()
|
||||
let coordinator = TerminalSurfaceCoordinator()
|
||||
|
||||
coordinator.isAttached = { false }
|
||||
oldController.retain(coordinator.bridge)
|
||||
#expect(oldController.retainedBridgeCount == 1)
|
||||
|
||||
coordinator.controller = oldController
|
||||
#expect(oldController.retainedBridgeCount == 0)
|
||||
|
||||
oldController.retain(coordinator.bridge)
|
||||
#expect(oldController.retainedBridgeCount == 1)
|
||||
|
||||
coordinator.controller = newController
|
||||
|
||||
#expect(oldController.retainedBridgeCount == 0)
|
||||
#expect(newController.retainedBridgeCount == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `free surface removes retained bridge`() {
|
||||
let controller = TerminalController()
|
||||
let coordinator = TerminalSurfaceCoordinator()
|
||||
|
||||
coordinator.isAttached = { false }
|
||||
coordinator.controller = controller
|
||||
|
||||
controller.retain(coordinator.bridge)
|
||||
#expect(controller.retainedBridgeCount == 1)
|
||||
|
||||
coordinator.freeSurface()
|
||||
|
||||
#expect(controller.retainedBridgeCount == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `suspended wakeup does not schedule render`() {
|
||||
let controller = TerminalController()
|
||||
var wakeups = 0
|
||||
|
||||
controller.shouldProcessWakeup = { false }
|
||||
controller.onWakeup = {
|
||||
wakeups += 1
|
||||
}
|
||||
|
||||
controller.handleWakeup()
|
||||
|
||||
#expect(wakeups == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `application active state controls immediate ticks`() async {
|
||||
let coordinator = TerminalSurfaceCoordinator()
|
||||
var renders = 0
|
||||
|
||||
coordinator.isAttached = { true }
|
||||
coordinator.onPostRender = {
|
||||
renders += 1
|
||||
}
|
||||
|
||||
coordinator.setApplicationActive(false)
|
||||
coordinator.requestImmediateTick()
|
||||
await Task.yield()
|
||||
|
||||
#expect(renders == 0)
|
||||
|
||||
coordinator.setApplicationActive(true)
|
||||
await Task.yield()
|
||||
|
||||
#expect(renders == 1)
|
||||
}
|
||||
}
|
||||
48
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalMarkedTextStateTests.swift
vendored
Normal file
48
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalMarkedTextStateTests.swift
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import Foundation
|
||||
@testable import GhosttyTerminal
|
||||
import Testing
|
||||
|
||||
struct TerminalMarkedTextStateTests {
|
||||
@Test
|
||||
func `delete backward removes selection before caret`() {
|
||||
var state = TerminalMarkedTextState()
|
||||
state.setMarkedText("shufa", selectedRange: NSRange(location: 5, length: 0))
|
||||
let deleted = state.deleteBackward()
|
||||
|
||||
#expect(deleted)
|
||||
#expect(state.text == "shuf")
|
||||
#expect(state.selectedRange == NSRange(location: 4, length: 0))
|
||||
#expect(state.currentSelectedRange == NSRange(location: 4, length: 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `delete backward clears single marked character`() {
|
||||
var state = TerminalMarkedTextState()
|
||||
state.setMarkedText("字", selectedRange: NSRange(location: 1, length: 0))
|
||||
let deleted = state.deleteBackward()
|
||||
|
||||
#expect(deleted)
|
||||
#expect(state.text == nil)
|
||||
#expect(state.markedRange == NSRange(location: NSNotFound, length: 0))
|
||||
#expect(state.currentSelectedRange == NSRange(location: NSNotFound, length: 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `set marked text clamps selection into document`() {
|
||||
var state = TerminalMarkedTextState()
|
||||
state.setMarkedText("abcd", selectedRange: NSRange(location: 99, length: 3))
|
||||
|
||||
#expect(state.selectedRange == NSRange(location: 4, length: 0))
|
||||
#expect(state.documentLength == 4)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `text in range returns substring and empty caret slice`() {
|
||||
var state = TerminalMarkedTextState()
|
||||
state.setMarkedText("中文abc", selectedRange: NSRange(location: 2, length: 0))
|
||||
|
||||
#expect(state.text(in: NSRange(location: 0, length: 2)) == "中文")
|
||||
#expect(state.text(in: NSRange(location: 2, length: 0)) == "")
|
||||
#expect(state.text(in: NSRange(location: 99, length: 1)) == nil)
|
||||
}
|
||||
}
|
||||
25
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalNavigationAPITests.swift
vendored
Normal file
25
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalNavigationAPITests.swift
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
@testable import GhosttyTerminal
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
struct TerminalNavigationAPITests {
|
||||
@Test
|
||||
func `view state navigation requires an attached surface`() {
|
||||
let state = TerminalViewState()
|
||||
|
||||
#expect(!state.performBindingAction("scroll_to_top"))
|
||||
#expect(!state.jumpToPrompt(by: -1))
|
||||
#expect(!state.scrollToRow(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `platform view exposes navigation actions`() {
|
||||
let invokeActions: (TerminalView) -> Void = { view in
|
||||
_ = view.performBindingAction("scroll_to_top")
|
||||
_ = view.jumpToPrompt(by: 1)
|
||||
_ = view.scrollToRow(42)
|
||||
}
|
||||
|
||||
_ = invokeActions
|
||||
}
|
||||
}
|
||||
221
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalSelectionAnchorTests.swift
vendored
Normal file
221
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalSelectionAnchorTests.swift
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
import Foundation
|
||||
@testable import GhosttyTerminal
|
||||
import Testing
|
||||
|
||||
// All test cases use host-point units. cellWidthPoints = 10, cellHeightPoints = 20
|
||||
// unless otherwise noted. The function's contract is "pointX / cellWidthPoints
|
||||
// = expected cell column" — that identity holds regardless of physical device
|
||||
// scale, so callers passing host points consistently get correct results on
|
||||
// @1x/2x/3x devices alike. A caller passing surface pixels without dividing
|
||||
// by displayScale would compute the wrong expectedColumn — that's a caller
|
||||
// bug, not a function bug. See plan §1 and §10审 for the unit contract.
|
||||
|
||||
struct TerminalSelectionAnchorTests {
|
||||
@Test
|
||||
func `single line ASCII`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "hello world",
|
||||
word: "world",
|
||||
pointX: 60, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 6, length: 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `multi line locates row`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "aaa\nbbb\nworld",
|
||||
word: "world",
|
||||
pointX: 0, pointY: 40,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 8, length: 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `same word across rows only picks target row`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "foo\nfoo\nfoo",
|
||||
word: "foo",
|
||||
pointX: 0, pointY: 20,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 4, length: 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `empty rows preserved`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "\n\nhello",
|
||||
word: "hello",
|
||||
pointX: 0, pointY: 40,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 2, length: 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `word not found returns nil`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc",
|
||||
word: "xyz",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `row out of bounds returns nil`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc",
|
||||
word: "abc",
|
||||
pointX: 0, pointY: 100,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `emoji surrogate pair`() {
|
||||
let text = "hi 👋"
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: text,
|
||||
word: "👋",
|
||||
pointX: 30, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
let nsText = text as NSString
|
||||
#expect(range != nil)
|
||||
if let r = range {
|
||||
#expect(r == NSRange(location: 3, length: 2))
|
||||
#expect(NSMaxRange(r) <= nsText.length)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `cjk full width`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "你好 world",
|
||||
word: "你好",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 0, length: 2))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `zero cell dimensions`() {
|
||||
let r1 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: 0, cellHeightPoints: 20
|
||||
)
|
||||
let r2 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 0
|
||||
)
|
||||
#expect(r1 == nil)
|
||||
#expect(r2 == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `empty word`() {
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `negative coordinates`() {
|
||||
let r1 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: -1, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
let r2 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: 0, pointY: -1,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(r1 == nil)
|
||||
#expect(r2 == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `substring disambiguation by point X`() {
|
||||
// `catalog cat` long-pressed at the end `cat` (column 8) — must pick
|
||||
// the standalone `cat` at location 8, not the prefix inside `catalog`
|
||||
// at location 0. literals at {0, 8}, expectedColumn=8 → 8.
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "catalog cat",
|
||||
word: "cat",
|
||||
pointX: 80, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 8, length: 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `triple repeat picked by point X`() {
|
||||
// literals at {0, 4, 8}, expectedColumn=8 → 8.
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "cat cat cat",
|
||||
word: "cat",
|
||||
pointX: 80, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 8, length: 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `non word characters in token`() {
|
||||
// literals at {4, 15}, expectedColumn=15 → 15.
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "see /usr/local /usr/local",
|
||||
word: "/usr/local",
|
||||
pointX: 150, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 15, length: 10))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `non word prefix token`() {
|
||||
// literals at {1, 6}, expectedColumn=6 → 6.
|
||||
let range = TerminalSelectionAnchor.resolveRange(
|
||||
in: "x/foo /foo",
|
||||
word: "/foo",
|
||||
pointX: 60, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
#expect(range == NSRange(location: 6, length: 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `nan and infinity guarded`() {
|
||||
let r1 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: .nan, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
let r2 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: .infinity, pointY: 0,
|
||||
cellWidthPoints: 10, cellHeightPoints: 20
|
||||
)
|
||||
let r3 = TerminalSelectionAnchor.resolveRange(
|
||||
in: "abc", word: "abc",
|
||||
pointX: 0, pointY: 0,
|
||||
cellWidthPoints: .nan, cellHeightPoints: 20
|
||||
)
|
||||
#expect(r1 == nil)
|
||||
#expect(r2 == nil)
|
||||
#expect(r3 == nil)
|
||||
}
|
||||
}
|
||||
23
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalSurfaceOptionsTests.swift
vendored
Normal file
23
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalSurfaceOptionsTests.swift
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
@testable import GhosttyTerminal
|
||||
import Testing
|
||||
|
||||
struct TerminalSurfaceOptionsTests {
|
||||
@Test
|
||||
func `envVars default to empty`() {
|
||||
#expect(TerminalSurfaceOptions().envVars.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `isEquivalent considers envVars`() {
|
||||
let base = TerminalSurfaceOptions(workingDirectory: "/tmp", envVars: ["A": "1"])
|
||||
#expect(base.isEquivalent(to: base))
|
||||
|
||||
var changedValue = base
|
||||
changedValue.envVars = ["A": "2"]
|
||||
#expect(!base.isEquivalent(to: changedValue))
|
||||
|
||||
var extraEntry = base
|
||||
extraEntry.envVars = ["A": "1", "B": "2"]
|
||||
#expect(!base.isEquivalent(to: extraEntry))
|
||||
}
|
||||
}
|
||||
44
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalSurfaceViewFocusAPITests.swift
vendored
Normal file
44
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalSurfaceViewFocusAPITests.swift
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
@testable import GhosttyTerminal
|
||||
import SwiftUI
|
||||
import Testing
|
||||
|
||||
@Suite("TerminalSurfaceViewFocusAPI")
|
||||
struct TerminalSurfaceViewFocusAPITests {
|
||||
@Test
|
||||
@MainActor
|
||||
func `bool focus modifier compiles`() {
|
||||
_ = BoolFocusSmokeView()
|
||||
}
|
||||
|
||||
@Test
|
||||
@MainActor
|
||||
func `optional focus modifier compiles`() {
|
||||
_ = OptionalFocusSmokeView()
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13.0, iOS 15.0, macCatalyst 15.0, *)
|
||||
private struct BoolFocusSmokeView: View {
|
||||
@StateObject private var state = TerminalViewState()
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
var body: some View {
|
||||
TerminalSurfaceView(context: state)
|
||||
.terminalFocused($isFocused)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13.0, iOS 15.0, macCatalyst 15.0, *)
|
||||
private struct OptionalFocusSmokeView: View {
|
||||
enum Pane: Hashable {
|
||||
case primary
|
||||
}
|
||||
|
||||
@StateObject private var state = TerminalViewState()
|
||||
@FocusState private var focusedPane: Pane?
|
||||
|
||||
var body: some View {
|
||||
TerminalSurfaceView(context: state)
|
||||
.terminalFocusOnAppear($focusedPane, equals: .primary)
|
||||
}
|
||||
}
|
||||
220
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalThemeConfigurationTests.swift
vendored
Normal file
220
vendor/libghostty-spm/Tests/GhosttyKitTest/TerminalThemeConfigurationTests.swift
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
@testable import GhosttyTerminal
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
struct TerminalThemeConfigurationTests {
|
||||
@Test
|
||||
func `command builder preserves insertion order`() {
|
||||
let configuration = TerminalConfiguration {
|
||||
$0.withFontSize(13)
|
||||
$0.withCursorStyle(.bar)
|
||||
$0.withCursorStyleBlink(false)
|
||||
$0.withBackground("#101010")
|
||||
}
|
||||
|
||||
#expect(
|
||||
configuration.rendered
|
||||
== """
|
||||
font-size = 13
|
||||
cursor-style = bar
|
||||
cursor-style-blink = false
|
||||
background = #101010
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `rendered config composes base behavior and theme`() {
|
||||
let state = TerminalViewState(
|
||||
configSource: .generated(
|
||||
"""
|
||||
font-size = 14
|
||||
cursor-style = block
|
||||
"""
|
||||
),
|
||||
theme: .init(
|
||||
light: TerminalConfiguration()
|
||||
.backgroundOpacity(0.82)
|
||||
.background("#111111"),
|
||||
dark: TerminalConfiguration()
|
||||
.backgroundOpacity(0.62)
|
||||
.background("#000000")
|
||||
),
|
||||
terminalConfiguration: TerminalConfiguration()
|
||||
.cursorStyleBlink(true)
|
||||
)
|
||||
|
||||
#expect(state.renderedConfig.contains("font-size = 14"))
|
||||
#expect(state.renderedConfig.contains("cursor-style = block"))
|
||||
#expect(state.renderedConfig.contains("cursor-style-blink = true"))
|
||||
#expect(state.renderedConfig.contains("background-opacity = 0.82"))
|
||||
#expect(state.renderedConfig.contains("background = #111111"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `valid configuration update preserves controller identity`() {
|
||||
let state = TerminalViewState(
|
||||
terminalConfiguration: TerminalConfiguration()
|
||||
.fontSize(14)
|
||||
)
|
||||
let controller = state.controller
|
||||
|
||||
let didApply = state.setTerminalConfiguration(
|
||||
TerminalConfiguration()
|
||||
.fontSize(16)
|
||||
.cursorStyle(.underline)
|
||||
)
|
||||
|
||||
#expect(didApply)
|
||||
#expect(state.controller === controller)
|
||||
#expect(state.terminalConfiguration == TerminalConfiguration()
|
||||
.fontSize(16)
|
||||
.cursorStyle(.underline))
|
||||
#expect(state.renderedConfig.contains("font-size = 16"))
|
||||
#expect(state.renderedConfig.contains("cursor-style = underline"))
|
||||
let fontSizeLines = state.renderedConfig
|
||||
.split(separator: "\n")
|
||||
.filter { $0.hasPrefix("font-size = ") }
|
||||
#expect(fontSizeLines.last == "font-size = 16")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `adopting dark mode switches rendered theme variant`() {
|
||||
let state = TerminalViewState(
|
||||
theme: .init(
|
||||
light: TerminalConfiguration()
|
||||
.backgroundOpacity(0.91),
|
||||
dark: TerminalConfiguration()
|
||||
.backgroundOpacity(0.47)
|
||||
)
|
||||
)
|
||||
var notificationCount = 0
|
||||
var colorSchemeAtNotification: TerminalColorScheme?
|
||||
let cancellable = state.objectWillChange.sink {
|
||||
notificationCount += 1
|
||||
colorSchemeAtNotification = state.effectiveColorScheme
|
||||
}
|
||||
defer { cancellable.cancel() }
|
||||
let controller = state.controller
|
||||
|
||||
#expect(state.effectiveColorScheme == .light)
|
||||
#expect(state.renderedConfig.contains("background-opacity = 0.91"))
|
||||
|
||||
state.adopt(colorScheme: .dark)
|
||||
|
||||
#expect(notificationCount == 1)
|
||||
#expect(colorSchemeAtNotification == .light)
|
||||
#expect(state.effectiveColorScheme == .dark)
|
||||
#expect(state.renderedConfig.contains("background-opacity = 0.47"))
|
||||
#expect(!state.renderedConfig.contains("background-opacity = 0.91"))
|
||||
#expect(state.controller === controller)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `invalid dark theme rolls back color scheme and rendered config`() {
|
||||
let state = TerminalViewState(
|
||||
theme: .init(
|
||||
light: TerminalConfiguration()
|
||||
.backgroundOpacity(0.91),
|
||||
dark: TerminalConfiguration()
|
||||
.custom("not-a-real-ghostty-option", "true")
|
||||
)
|
||||
)
|
||||
var notificationCount = 0
|
||||
let cancellable = state.objectWillChange.sink {
|
||||
notificationCount += 1
|
||||
}
|
||||
defer { cancellable.cancel() }
|
||||
let previousRenderedConfig = state.renderedConfig
|
||||
|
||||
state.adopt(colorScheme: .dark)
|
||||
|
||||
// Controller rolls back on config failure, so color scheme stays light
|
||||
#expect(notificationCount == 0)
|
||||
#expect(state.effectiveColorScheme == .light)
|
||||
#expect(state.renderedConfig == previousRenderedConfig)
|
||||
#expect(state.renderedConfig.contains("background-opacity = 0.91"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `adopting current color scheme does not notify`() {
|
||||
let state = TerminalViewState()
|
||||
var notificationCount = 0
|
||||
let cancellable = state.objectWillChange.sink {
|
||||
notificationCount += 1
|
||||
}
|
||||
defer { cancellable.cancel() }
|
||||
|
||||
#expect(state.effectiveColorScheme == .light)
|
||||
state.adopt(colorScheme: .light)
|
||||
|
||||
#expect(notificationCount == 0)
|
||||
#expect(state.effectiveColorScheme == .light)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `shared controller accepts mutations`() {
|
||||
let controller = TerminalController()
|
||||
let state = TerminalViewState(controller: controller)
|
||||
|
||||
// With the single-source-of-truth architecture, mutations go
|
||||
// directly to the controller and succeed.
|
||||
let didSetTheme = state.setTheme(
|
||||
.init(
|
||||
light: TerminalConfiguration()
|
||||
.backgroundOpacity(0.5)
|
||||
)
|
||||
)
|
||||
|
||||
#expect(didSetTheme)
|
||||
#expect(state.controller === controller)
|
||||
#expect(state.renderedConfig.contains("background-opacity = 0.5"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `no op theme update returns false`() {
|
||||
let theme = TerminalTheme(
|
||||
light: TerminalConfiguration()
|
||||
.backgroundOpacity(0.7)
|
||||
)
|
||||
let state = TerminalViewState(theme: theme)
|
||||
var notificationCount = 0
|
||||
let cancellable = state.objectWillChange.sink {
|
||||
notificationCount += 1
|
||||
}
|
||||
defer { cancellable.cancel() }
|
||||
let previousRenderedConfig = state.renderedConfig
|
||||
|
||||
let didApply = state.setTheme(theme)
|
||||
|
||||
#expect(!didApply)
|
||||
#expect(notificationCount == 0)
|
||||
#expect(state.renderedConfig == previousRenderedConfig)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `invalid configuration does not replace rendered config`() {
|
||||
let state = TerminalViewState(
|
||||
terminalConfiguration: TerminalConfiguration()
|
||||
.fontSize(14)
|
||||
)
|
||||
var notificationCount = 0
|
||||
let cancellable = state.objectWillChange.sink {
|
||||
notificationCount += 1
|
||||
}
|
||||
defer { cancellable.cancel() }
|
||||
let previousRenderedConfig = state.renderedConfig
|
||||
|
||||
let didApply = state.setTerminalConfiguration(
|
||||
TerminalConfiguration()
|
||||
.custom("not-a-real-ghostty-option", "true")
|
||||
)
|
||||
|
||||
#expect(!didApply)
|
||||
#expect(notificationCount == 0)
|
||||
#expect(state.renderedConfig == previousRenderedConfig)
|
||||
#expect(state.terminalConfiguration == TerminalConfiguration().fontSize(14))
|
||||
}
|
||||
}
|
||||
31
vendor/libghostty-spm/Tests/GhosttyKitTest/Test.swift
vendored
Normal file
31
vendor/libghostty-spm/Tests/GhosttyKitTest/Test.swift
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
import GhosttyKit
|
||||
import XCTest
|
||||
|
||||
class GhosttyKitTest: XCTestCase {
|
||||
func testAppLifecycle() {
|
||||
XCTAssertEqual(ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv), GHOSTTY_SUCCESS)
|
||||
|
||||
guard let config = ghostty_config_new() else {
|
||||
return XCTFail("ghostty_config_new returned nil")
|
||||
}
|
||||
defer { ghostty_config_free(config) }
|
||||
|
||||
ghostty_config_finalize(config)
|
||||
|
||||
var runtime = ghostty_runtime_config_s(
|
||||
userdata: nil,
|
||||
supports_selection_clipboard: false,
|
||||
wakeup_cb: nil,
|
||||
action_cb: nil,
|
||||
read_clipboard_cb: nil,
|
||||
confirm_read_clipboard_cb: nil,
|
||||
write_clipboard_cb: nil,
|
||||
close_surface_cb: nil
|
||||
)
|
||||
|
||||
guard let app = ghostty_app_new(&runtime, config) else {
|
||||
return XCTFail("ghostty_app_new returned nil")
|
||||
}
|
||||
ghostty_app_free(app)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user