Files
terminalX/apps/TerminalX/iOS/UIPreviewFixture.swift
kid 5707d70dff feat: 侧边栏两态重构(统一视图身份)+ 第四轮触控化走查
侧边栏(真机验证):
- 砍掉悬浮态,只留收起⇄固定展开;TerminalSidebar 单一视图身份,
  内容恒按 288pt 排版、收起=左侧裁切(clipped 宽度动画),
  图标列两态同尺寸同位置,明细淡入淡出,终端随宽度盖上/让开
- 开合入口收敛:Logo 位(收起态=»展开键/展开态=Logo)+ 头部«;
  删底部开合键与 Pin;LeadingHitShape 修 clipped 不裁触摸
- 修 GridBox 从不通知 UI:onGridChange→objectWillChange,
  标题胶囊格子数随开合实时刷新

触控化走查(上一轮,真机部署验证):
- 窗口按钮 13pt 色点→22pt 圆角方块常显字形;动作条改可点按钮
- TXTech 拼写/配色规范(SSH/mosh/tmux 官方写法+品牌色映射)
- 首页会话卡固定 3 列 4:3;去掉未实现的 ⌘K 提示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-26 00:55:53 +08:00

269 lines
14 KiB
Swift
Raw 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 ~ $ ",
]),
]
/// attached + detached稿 `3b`
static func tmuxSessions(current: String) -> [TmuxSessionInfo] {
// timeIntervalSinceNow
[
TmuxSessionInfo(name: current, windows: 3, isAttached: true, lastActivity: nil),
TmuxSessionInfo(name: "scratch", windows: 1, isAttached: false,
lastActivity: Date(timeIntervalSinceNow: -3 * 86400)),
TmuxSessionInfo(name: "deploy", windows: 2, isAttached: false,
lastActivity: Date(timeIntervalSinceNow: -86400)),
]
}
/// `-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`() · `split/v|h`
/// · `split-now/v|h`() · `resize/<cols>`(resize-pane)
/// · `pick/<>` · `pick-new/<>` · `pick-native`
/// · `dismiss` · `session/<>`1 · `sidebar/toggle|pin`
@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 "split":
guard router.route.sessionID != nil else { return false }
router.present(.splitMenu(vertical: path.dropFirst().first != "h"))
case "dismiss":
router.dismissModal()
case "split-now":
// split-window
guard let id = router.route.sessionID, let s = manager.session(id),
let tmux = s.tmuxController else { return false }
tmux.splitWindow(vertical: path.dropFirst().first != "h")
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 "resize":
// resize-pane DragGesture`ui/resize/<cols>`
guard let id = router.route.sessionID, let s = manager.session(id),
let tmux = s.tmuxController,
let win = tmux.windows.first(where: { $0.id == tmux.activeWindowID }),
let pane = win.activePane,
let cols = path.dropFirst().first.flatMap(Int.init) else { return false }
tmux.resizePane(pane, cols: cols)
case "session":
guard let n = path.dropFirst().first.flatMap(Int.init),
let s = manager.session(atShortcutIndex: n - 1) else { return false }
manager.focus(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