Files
terminalX/apps/TerminalX/iOS/UIPreviewFixture.swift
kid 4b5292107b feat: 业务逻辑对齐 + 首页/弹框七项减法 + 终端页四项体验修复
业务逻辑(用户定稿):
- 点主机总是新建会话(会话:主机=多:1);activate 选中断线会话自动重连
- 「直接输入」凭据保存时自动落 Keychain(去重复用),主机只存引用
- 主机编辑页可就地新建 Tailscale 连接;删无人引用的旧 HostsListView

首页/弹框减法:
- 删侧边栏「连接新主机」、无 tmux 横幅与安装引导、页头「新建连接」
- 搜索移到「全部主机」行;添加主机虚线卡与主机卡等高(骨架复刻)
- 会话卡加 ✕ + 复用终端页同款二次确认
- 会话选择器 sheet → 项目风格居中弹框(去手柄/Header/最近)

终端页修复:
- 侧边栏会话列表改创建顺序,切换不再重排抖动
- 焦点高亮上移到 pane 容器边框(四边完整,去负 padding)
- 软键盘双重让位修复:画布忽略键盘 + TXKeyboardObserver 显式让位一次
- 自动聚焦:focusTick 脉冲强制翻转 FocusState + surface 就绪补聚焦;
  vendor 补 iPadOS 触摸板 indirectPointer 的 becomeFirstResponder

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 17:49:58 +08:00

241 lines
12 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#if DEBUG
import Foundation
import UIKit
/// UI DEBUG
///
/// GUI `simctl io screenshot` 稿稿
/// ****
/// in-memory ****
///
/// `xcrun simctl launch <UDID> ai.athom.terminalx --args -txUIFixture 1`
/// `-txHost/-txUser/-txPass` CLAUDE.md
enum UIPreviewFixture {
/// 稿 `1e -` / tsnet /
/// `name` `hosts` `SavedHost.id`
struct Spec {
let name: String
let link: LinkState
let tmux: TmuxSummary?
let lines: [String]
}
/// Tailscale id tsnet fixture
static let fixtureTailnetID = UUID(uuidString: "00000000-0000-0000-0000-0000000000FE")!
/// tailnet +
static let tailnets: [TailscaleConnection] = [
TailscaleConnection(id: fixtureTailnetID, name: "athom.ts.net",
hostname: "ipad-pro", needsAuthKey: false, lastSelfIP: "100.92.14.7"),
]
/// 稿 `1e` 7 +
static let hosts: [SavedHost] = [
SavedHost(name: "mac-studio", host: "100.71.203.4", useMosh: true,
auth: .inlinePassword(username: "yz", password: ""),
tailscaleID: fixtureTailnetID, group: "生产", os: "macOS · M4 Max"),
SavedHost(name: "ubuntu-cn2", host: "100.88.9.61", useMosh: true,
auth: .inlinePassword(username: "yz", password: ""),
tailscaleID: fixtureTailnetID, group: "", os: "Ubuntu 26.04"),
SavedHost(name: "edge-sg1", host: "10.1.2.11", useMosh: false,
auth: .inlinePassword(username: "root", password: ""),
tailscaleID: fixtureTailnetID, group: "", os: "Debian 13"),
SavedHost(name: "edge-sg2", host: "10.1.2.12", useMosh: false,
auth: .inlinePassword(username: "root", password: ""),
group: "", os: "Debian 13"),
SavedHost(name: "nas-home", host: "100.104.2.18", useMosh: true,
auth: .inlinePassword(username: "admin", password: ""),
group: "家里", os: "TrueNAS"),
SavedHost(name: "pi-lab", host: "192.168.1.31", useMosh: false,
auth: .inlinePassword(username: "pi", password: ""),
group: "家里", os: "Raspberry Pi OS"),
SavedHost(name: "mosh-lab", host: "192.168.1.44", useMosh: true,
auth: .inlinePassword(username: "yz", password: ""),
group: "家里", os: "Fedora 44"),
]
static let specs: [Spec] = [
Spec(name: "mac-studio", link: .direct,
tmux: TmuxSummary(name: "dev", windows: 3, panes: 4, currentTitle: "运行测试"), lines: [
"→ terminalx git:(main) git fetch --all --prune",
"Fetching origin … done",
"→ terminalx git:(main) swiftformat --lint Sources",
"✓ 0 issues in 84 files",
"→ terminalx git:(main) swift build -c release --arch arm64",
"[142/142] Compiling TXCore",
"✓ build complete (14.2s)",
"→ terminalx git:(main) swift test --package-path packages/TXCore",
"◇ Test run started.",
" ✔ parses %output escaped octal",
" ✔ layout diff keeps pane identity",
" ✔ host-key pin survives wipe",
"◇ 50 tests passed after 0.412s",
"→ terminalx git:(main) xcodegen generate --spec project.yml",
"✅ Created TerminalX.xcodeproj",
"→ terminalx git:(main) xcodebuild -scheme TerminalX build",
"** BUILD SUCCEEDED **",
"→ terminalx git:(main) git status --short",
" M apps/TerminalX/iOS/RailSidebar.swift",
" M apps/TerminalX/iOS/HomeView.swift",
"?? docs/design/UI-DESIGN-HANDOFF.md",
"→ terminalx git:(main) scripts/tx-shot.sh /tmp/v3.png",
" pixelWidth: 2752",
" pixelHeight: 2064",
"→ terminalx git:(main) swift test --package-path packages/TXCore",
"◇ 50 tests passed after 0.398s",
"→ terminalx git:(main) tmux ls",
"dev: 3 windows (created Fri 09:12:03)",
"scratch: 1 windows (created Tue)",
"→ terminalx git:(main) tailscale status | head -3",
"100.71.203.4 mac-studio direct",
"100.88.9.61 ubuntu-cn2 relay hkg",
"→ terminalx git:(main) ",
]),
Spec(name: "ubuntu-cn2", link: .relay,
tmux: TmuxSummary(name: "build", windows: 1, panes: 1, currentTitle: "集成测试"), lines: [
"→ cn2 ~ journalctl -u build-runner -n 3 --no-pager",
"12:05:02 Started build-runner@42",
"12:06:02 build-runner@42 succeeded",
"→ cn2 ~ systemctl status build-runner --no-pager",
"● build-runner.service — active (running)",
" since Fri 09:12:03 CST; 2h 54min ago",
"→ cn2 ~ ninja -C build -j 8",
"[148/148] Linking libmosh-core.a",
"✓ ninja: no work to do.",
"→ cn2 ~ tailscale netcheck | head -5",
" * UDP: true",
" * IPv4: yes",
" * UPnP: false",
"→ cn2 ~ ./run-integration.sh --tsnet",
"▸ joining tailnet as ci-runner…",
"▸ peer enumeration: 7 nodes",
"▸ mosh relay loopback:60122 ⇄ tsnet",
"▸ handshake ok, AES-OCB session up",
" roaming: peer addr switched",
" resume ok in 1.8s, 0 bytes lost",
" suspend 90s → wake",
" resume ok in 2.4s",
"▸ 14/14 passed",
"→ cn2 ~ df -h /tank | tail -1",
"tank 187T 217T 7.87 42%",
"→ cn2 ~ ",
]),
Spec(name: "nas-home", link: .reconnecting(attempt: 3), tmux: nil, lines: [
"nas ~ $ zfs get compressratio tank",
"tank compressratio 1.34x",
"nas ~ $ docker ps --format 'table {{.Names}}'",
"NAMES STATUS",
"plex Up 6 days",
"syncthing Up 6 days",
"nas ~ $ uname -sr",
"Linux 6.12.9-truenas",
"nas ~ $ zpool status -x",
" all pools are healthy",
"nas ~ $ df -h /tank",
"Filesystem Size Used Avail Use%",
"tank 187T 217T 7.87 42%",
"nas ~ $ zfs list -t snapshot | tail -3",
"tank/media@auto-2026-07-24 1.2G",
"tank/docs@auto-2026-07-24 318M",
"tank/vm@auto-2026-07-24 2.4G",
"nas ~ $ smartctl -H /dev/sda | tail -2",
"SMART overall-health: PASSED",
"nas ~ $ systemctl is-active smbd nfs-server",
"active",
"active",
"nas ~ $ tail -2 /var/log/messages",
"12:04:31 nas kernel: wlan0 rssi -74",
"12:06:20 nas tailscaled: peer offline",
"nas ~ $ ",
]),
]
/// `-txOpenAsHost 1`
static func hostFromLaunchArgs() -> SavedHost? {
let d = UserDefaults.standard
guard let host = d.string(forKey: "txHost"), !host.isEmpty,
let user = d.string(forKey: "txUser") else { return nil }
let port = d.integer(forKey: "txPort")
return SavedHost(
name: host, host: host, port: port == 0 ? 22 : port,
useMosh: d.bool(forKey: "txMosh"),
useTmux: d.object(forKey: "txTmux") == nil ? true : d.bool(forKey: "txTmux"),
tmuxSession: d.string(forKey: "txTmuxSession"),
auth: .inlinePassword(username: user, password: d.string(forKey: "txPass") ?? ""))
}
/// `-txUIFixture 1`
static var isEnabled: Bool {
UserDefaults.standard.bool(forKey: "txUIFixture")
}
/// `xcrun simctl openurl <UDID> "terminalx://ui/<>"`
///
/// GUIidb companion
/// URL `AppRouter`/`SessionManager`
///
/// `home` · `minimize` · `close`() · `close-now`()
/// · `pick/<>` · `pick-new/<>` · `pick-native`
/// · `dismiss` · `session/<>`1 · `sidebar/toggle|pin`
/// `resize/<cols>` pane pane tmux
@MainActor
static func handle(uiCommand path: [String], manager: SessionManager, router: AppRouter) -> Bool {
switch path.first {
case "home":
router.goHome()
case "minimize":
if let id = router.route.sessionID { manager.minimize(id); router.goHome() }
case "close":
//
guard router.route.sessionID != nil else { return false }
router.present(.closeConfirm)
case "close-now":
//
if let id = router.route.sessionID { router.goHome(); manager.close(id) }
case "dismiss":
router.dismissModal()
case "pick":
// attach `ui/pick/<name>`
guard let s = manager.sessionAwaitingChoice ?? router.route.sessionID.flatMap(manager.session),
let name = path.dropFirst().first else { return false }
s.attachTmuxSession(name)
case "pick-new":
guard let s = manager.sessionAwaitingChoice ?? router.route.sessionID.flatMap(manager.session)
else { return false }
s.createTmuxSession(path.dropFirst().first ?? "main")
case "pick-native":
// route
guard let s = manager.sessionAwaitingChoice ?? router.route.sessionID.flatMap(manager.session)
else { return false }
s.useNativeTerminal()
case "session":
guard let n = path.dropFirst().first.flatMap(Int.init),
let s = manager.session(atShortcutIndex: n - 1) else { return false }
manager.activate(s)
router.enter(s.id)
case "sidebar":
// pin toggle
router.toggleSidebar()
default:
return false
}
NSLog("TXUI cmd=\(path.joined(separator: "/")) route=\(router.route) sidebar=\(router.sidebar)")
return true
}
/// `-txLandscape 1`
///
/// GUI iPad Pro 13 1366×1024`simctl`
/// iOS 16+ `requestGeometryUpdate` app `simctl io screenshot`
/// DEBUG
@MainActor
static func forceLandscapeIfRequested() {
guard UserDefaults.standard.bool(forKey: "txLandscape") else { return }
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
scene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight)) { error in
NSLog("TXUI landscape request failed: \(error.localizedDescription)")
}
}
}
#endif