初始提交: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:
kid
2026-07-24 10:20:46 +08:00
commit aa92d0e676
2761 changed files with 803505 additions and 0 deletions

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