From 28e9cfc20795cfee5fffa8ff0cae7d9fe7302dac Mon Sep 17 00:00:00 2001 From: kid Date: Sat, 25 Jul 2026 22:59:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20UI=20=E5=95=86=E4=B8=9A=E5=8C=96?= =?UTF-8?q?=E6=94=B9=E9=80=A0=20+=20=E8=BF=9E=E6=8E=A5=E5=89=8D=20tmux=20?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E9=80=89=E6=8B=A9=EF=BC=88=E7=9C=9F=E6=9C=BA?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 Claude Design 定稿(归档在 docs/design/)重做 UI,并把 tmux 从 「从不自动进」修成「连接前探测 → 让用户选会话」。 设计令牌与导航地基 - Theme 拆三层:TXAccent(恒定 blurple) / TXChrome(中性阶×4 表) / TXFlavor(Catppuccin 终端) - vendor JetBrains Mono 四权重(附 OFL 许可) - AppRouter + SessionManager:多会话并存,路由与会话生命周期解耦 - SessionCanvas 常驻挂载会话 surface(摘除即丢内容,也是缩回动画的前提) 按设计稿落地的屏 - 沉浸轨道页:56pt 轨道 + 浮起标题/状态胶囊 + 侧边栏三态(遮罩不 resize、Pin 各一次) - 首页:活动会话卡(readViewportText 文本镜像)+ 主机网格 + 筛选 chips - 关闭二次确认(tmux 仅断开 / 原生窗口两套文案)、分屏菜单、pane 拖拽条 - 空状态:首次运行 / 无会话 / 搜索无命中 / 连接中·失败·已关闭 tmux 真实链路(真机查出并修掉 4 个 bug) - 全代码库从来没人发起 attach → 连接前探测 + 会话选择器(接回 / 新建 / 原生终端) - format 分隔符 tab 经 PTY 变成下划线 → 改用 |:| - controller 变化不冒泡到 session → Combine 转发(否则数据解析对了 UI 不刷新) - 当前会话名不能靠 session_attached 反推 → 改用 display-message 传输层:SSH connect 加超时(原来阻塞到系统 TCP 超时 75s+) 无头验证设施:假会话 fixture · terminalx://ui/* 驱动 · 横屏截图脚本 · 对拍走查法 TXCore 45 tests 绿(新增 5 个会话探测单测) Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 21 +- .../Resources/Fonts/JetBrainsMono-Bold.ttf | Bin 0 -> 277828 bytes .../Resources/Fonts/JetBrainsMono-Medium.ttf | Bin 0 -> 273860 bytes .../Resources/Fonts/JetBrainsMono-Regular.ttf | Bin 0 -> 273900 bytes .../Fonts/JetBrainsMono-SemiBold.ttf | Bin 0 -> 277092 bytes apps/TerminalX/Resources/Fonts/OFL.txt | 93 + apps/TerminalX/Resources/Fonts/README.md | 2 + apps/TerminalX/TerminalX-Info.plist | 10 +- apps/TerminalX/iOS/AppRouter.swift | 97 + apps/TerminalX/iOS/ContentView.swift | 964 +++++---- apps/TerminalX/iOS/Credential.swift | 92 + apps/TerminalX/iOS/CredentialAuth.swift | 84 + apps/TerminalX/iOS/CredentialStore.swift | 78 + apps/TerminalX/iOS/HomeView.swift | 632 ++++++ apps/TerminalX/iOS/HostStore.swift | 102 +- apps/TerminalX/iOS/HostsView.swift | 350 +++ apps/TerminalX/iOS/KeychainKnownHosts.swift | 30 + apps/TerminalX/iOS/KeychainView.swift | 358 +++ apps/TerminalX/iOS/RailSidebar.swift | 529 +++++ apps/TerminalX/iOS/SSHTerminalModel.swift | 655 ------ apps/TerminalX/iOS/SessionManager.swift | 193 ++ apps/TerminalX/iOS/SessionPickerSheet.swift | 206 ++ apps/TerminalX/iOS/SettingsView.swift | 173 ++ apps/TerminalX/iOS/SigningKeyProvider.swift | 34 + apps/TerminalX/iOS/TXParts.swift | 301 +++ apps/TerminalX/iOS/TailscaleStore.swift | 80 + apps/TerminalX/iOS/TailscaleView.swift | 206 ++ apps/TerminalX/iOS/TerminalChrome.swift | 406 ++++ apps/TerminalX/iOS/TerminalDialogs.swift | 279 +++ apps/TerminalX/iOS/TerminalSession.swift | 1097 ++++++++++ apps/TerminalX/iOS/Theme.swift | 335 ++- apps/TerminalX/iOS/TmuxController.swift | 131 +- apps/TerminalX/iOS/TsnetRegistry.swift | 103 + apps/TerminalX/iOS/UIComponents.swift | 56 + apps/TerminalX/iOS/UIPreviewFixture.swift | 267 +++ apps/TerminalX/project.yml | 13 +- docs/HANDOFF.md | 173 ++ docs/design/UI-DESIGN-HANDOFF.md | 241 +++ docs/design/render-screens.py | 80 + docs/design/support.js | 1911 +++++++++++++++++ docs/design/terminalX-iPadOS.dc.html | 810 +++++++ .../TXCore/Tmux/TmuxSessionProbe.swift | 102 + .../TXCoreTests/TmuxSessionProbeTests.swift | 69 + .../Sources/TXTransport/SSHSession.swift | 32 +- .../Sources/TXTransport/Transport.swift | 7 +- scripts/tx-shot.sh | 39 + 46 files changed, 10237 insertions(+), 1204 deletions(-) create mode 100644 apps/TerminalX/Resources/Fonts/JetBrainsMono-Bold.ttf create mode 100644 apps/TerminalX/Resources/Fonts/JetBrainsMono-Medium.ttf create mode 100644 apps/TerminalX/Resources/Fonts/JetBrainsMono-Regular.ttf create mode 100644 apps/TerminalX/Resources/Fonts/JetBrainsMono-SemiBold.ttf create mode 100644 apps/TerminalX/Resources/Fonts/OFL.txt create mode 100644 apps/TerminalX/Resources/Fonts/README.md create mode 100644 apps/TerminalX/iOS/AppRouter.swift create mode 100644 apps/TerminalX/iOS/Credential.swift create mode 100644 apps/TerminalX/iOS/CredentialAuth.swift create mode 100644 apps/TerminalX/iOS/CredentialStore.swift create mode 100644 apps/TerminalX/iOS/HomeView.swift create mode 100644 apps/TerminalX/iOS/HostsView.swift create mode 100644 apps/TerminalX/iOS/KeychainView.swift create mode 100644 apps/TerminalX/iOS/RailSidebar.swift delete mode 100644 apps/TerminalX/iOS/SSHTerminalModel.swift create mode 100644 apps/TerminalX/iOS/SessionManager.swift create mode 100644 apps/TerminalX/iOS/SessionPickerSheet.swift create mode 100644 apps/TerminalX/iOS/SettingsView.swift create mode 100644 apps/TerminalX/iOS/TXParts.swift create mode 100644 apps/TerminalX/iOS/TailscaleStore.swift create mode 100644 apps/TerminalX/iOS/TailscaleView.swift create mode 100644 apps/TerminalX/iOS/TerminalChrome.swift create mode 100644 apps/TerminalX/iOS/TerminalDialogs.swift create mode 100644 apps/TerminalX/iOS/TerminalSession.swift create mode 100644 apps/TerminalX/iOS/TsnetRegistry.swift create mode 100644 apps/TerminalX/iOS/UIComponents.swift create mode 100644 apps/TerminalX/iOS/UIPreviewFixture.swift create mode 100644 docs/design/UI-DESIGN-HANDOFF.md create mode 100644 docs/design/render-screens.py create mode 100644 docs/design/support.js create mode 100644 docs/design/terminalX-iPadOS.dc.html create mode 100644 packages/TXCore/Sources/TXCore/Tmux/TmuxSessionProbe.swift create mode 100644 packages/TXCore/Tests/TXCoreTests/TmuxSessionProbeTests.swift create mode 100755 scripts/tx-shot.sh diff --git a/CLAUDE.md b/CLAUDE.md index 5b3ad27..e55f02a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,15 +58,25 @@ sleep N; xcrun simctl io $UDID screenshot /tmp/x.png # 然后用 Read 工具 - **mosh 直连(LAN)**:`-txHost H -txPort 22 -txUser U -txPass PW -txMosh 1`(登录后跑 mosh-server→解析 MOSH CONNECT→mosh_main 直连 UDP) - **mosh over tsnet**:额外加 `-txTsnetKey `(-txHost 填 tailnet IP;relay=loopback↔tsnet UDP) - 自定义 mosh-server 命令:`-txMoshServerCmd "…"`(默认 `mosh-server new -s -c 256 -l LANG=en_US.UTF-8 -l LC_ALL=en_US.UTF-8`) +- **tmux 自动进入**:默认开(`SavedHost.useTmux`)。`-txTmux 0` 关闭(回归 raw 终端路径)、`-txTmuxSession <名>` 指定会话名(默认 `main`)。 - **M4 publickey 认证**:`-txPubkeyAuth 1`(不给 -txPass;SigningKeyProvider 造 P-256 key,日志打 authorized_keys 行,加到服务器后重连免密登录) - **M4 host key mismatch 测试**:`-txHostKeyPinOverrideBase64 `(播种假 pin → 真 host key 不符 → 弹告警) +**UI 改造期无头验证(推荐走脚本)**:`scripts/tx-shot.sh [-- <启动参数>]` 一步完成装/启/截图/修正方向。 +- **`-txLandscape 1`**:app 侧 `requestGeometryUpdate` 转横屏(设计基准 iPad Pro 13″ 1366×1024)。无 GUI 时模拟器帧缓冲恒竖屏,故脚本截完 `sips -r 90` 转回来(内容已横向渲染)。 +- **`-txUIFixture 1`**:造 3 条**假会话**(mac-studio/ubuntu-cn2/nas-home,含 direct/relay/重连三种状态)灌示意输出进 in-memory 终端,**不建任何传输连接**。设计稿的首页会话卡/轨道多头像/状态徽标都要多会话才有内容,用真机连接验证太慢。见 `UIPreviewFixture.swift`(DEBUG only),与 `-txHost` 互斥。 +- **`terminalx://ui/<动作>`**(`xcrun simctl openurl`):无头驱动导航与浮层——`home` / `minimize` / `close`(弹确认) / `close-now`(直接关) / `split/v|h`(分屏菜单) / `dismiss` / `session/` / `sidebar/toggle|pin`。比坐标点击稳(不依赖 idb companion,不随布局失效)。模态状态放在 `AppRouter.modal` 而非视图 `@State`,快捷键与 URL 才能共用同一入口。 +- **对拍走查法(还原度靠这个,别靠目测)**:截图 → 用 python+PIL 把设计稿 PNG 与截图统一缩放到 1366×1024 → 裁同区域并排 → 再对元素列做垂直投影扫描量化 y 坐标。缩略图里 `#1E1E2E` 与 `#11121C` 目测分不出,必须采样像素。 +- **坑:`statusBarHidden` 不能挂在 `TerminalScreen`**(它常驻挂载,会连首页一起隐藏)→ 挂 `RootView` 按 `router.route` 判断。 +- 设计稿本体在 `docs/design/`:`UI-DESIGN-HANDOFF.md`(token/规格/交互全表) + `terminalX-iPadOS.dc.html`(13 屏画板) + `render-screens.py`(拆屏,配 headless Chrome 逐屏截 PNG 对拍)。 + **M3 挂起→恢复无头验证**:退后台 `simctl openurl $UDID https://apple.com`;冻结 `kill -STOP $(pgrep -x TerminalX)`;解冻 `kill -CONT`;前台 `simctl openurl $UDID "terminalx://resume"`(URL scheme 已注册)。日志 `log stream | grep -E 'MOSHDBG|TXM3'` 看 `wake pulse`→`TXM3 recovered` 时间差判 <3s。**坑:模拟器看门狗会杀掉被 STOP 冻结过久(>~30s)的 app;且模拟器不复现 socket defunct/WG 过期,深挂起需真机锁屏补测。** ## 仓库结构 - `packages/TXCore` — 零依赖纯逻辑:tmux 解析器/layout/`SessionMachine`(会话状态机+退避重连)/`TmuxControlSequence`(DCS 检测)。 - `packages/TXTransport` — `SSHSession`(libssh2, 支持外部 fd)/`MoshSession`(mosh_main 桥)/`Transport` 协议;C shim: `CSSH`+`CSSHCore`(libssh2)、`CMosh`+`MoshCore`(mosh)。 -- `apps/TerminalX/iOS` — `SSHTerminalModel`(连接编排+tsnet+tmux 路由+mosh 编排)/`ContentView`(HomeView 主机管理页+TerminalScreen 终端 chrome+设置)/`TmuxController`(tmux 网关)/`TsnetProbe`(M1 自检)/`Theme`(TXPalette+TXThemeManager 设计令牌,颜色封装供主题系统)/`HostStore`(主机列表 JSON 持久化)。UI 对标 Moshi(深蓝黑+终端绿+Catppuccin),见 `docs/HANDOFF.md §10`。 +- `apps/TerminalX/iOS` — `TerminalSession`(**一实例=一条会话**:连接编排+tsnet+tmux 路由+mosh 编排+viewport 快照)/`SessionManager`(多会话持有+最小化/关闭语义+生命周期广播)/`AppRouter`(AppRoute .home|.terminal(id) + SidebarState 三态)/`ContentView`(RootView+**SessionCanvas 会话常驻挂载**+TerminalScreen 沉浸轨道页+TmuxStage)/`HomeView`(首页会话卡+主机网格)/`RailSidebar`(56pt 轨道+288pt 面板三态)/`TerminalChrome`(浮起标题/状态胶囊+提示条)/`TXParts`(设计令牌小件+终端文本镜像)/`TmuxController`(tmux 网关)/`TsnetProbe`(M1 自检)/`Theme`(TXAccent 恒定 blurple + TXChrome 中性阶×4 + TXFlavor 终端配色)/`HostStore`(主机列表 JSON 持久化)/`UIPreviewFixture`(DEBUG 无头 UI 验证脚手架)。 +- UI 已从 Demo 转入**商业化改造**:设计定稿见 `docs/design/UI-DESIGN-HANDOFF.md`(Nocturne chrome + Catppuccin 终端 + 沉浸轨道),路线图与进度见 `docs/HANDOFF.md §12`。 - `vendor/` — 所有非 Swift 依赖本地化:`libghostty-spm`(GhosttyKit)/`MSDisplayLink`/`libssh2`/`mbedtls`/`tsnet-bridge`(Go)/`mosh`(blinksh/ios 分支)/`protobuf`(3.21.12);`build/mosh/`(手写 config.h+CMakeLists)。 - `artifacts/` — 预编译 xcframework(**有意提交**,非 gitignore):`CSSHCore`/`TsnetBridge`/`MoshCore`。 @@ -79,8 +89,17 @@ sleep N; xcrun simctl io $UDID screenshot /tmp/x.png # 然后用 Read 工具 - **Swift6 `SendNonSendable` pass 可能崩溃**(`pthread_create`+`Unmanaged` 上下文+C 入口的组合):改用 `Foundation.Thread`+线程内 `pthread_self()` 规避(见 `MoshSession`)。 - GhosttyKit 的 `InMemoryTerminalSession.receive` 在 surface 未 attach 时**丢弃**写入 → 用 `OutputGate` 缓冲到 `surfaceSize != nil` 再 flush。 - tmux `-CC attach` **不推 %window-add** → 必须主动 `list-windows` 枚举。 +- **tmux format 的字段分隔符不能用 tab**:真机实测 tab 经 PTY 送到 tmux 后**变成下划线**,`list-windows`/`list-sessions` 整行分不开(`count=1`)→ title/cwd/会话名全丢、UI 显示兜底名「窗口 N」。改用 `TmuxController.fieldSep = "|:|"`(可打印、不被 PTY 处理、`#` 会被 tmux format 当特殊字符所以不能用)。**排查提醒**:`os_log` 会把控制字符显示成 `_`,光看日志分不出「真下划线」还是「被显示替换的 tab」——要打印 `parts.count` 才能确诊。 +- **tmux 当前会话名要用 `display-message -p "#{session_name}"`**,不能拿 `list-sessions` 的 `session_attached` 反推:服务器上别的客户端 attach 着其它会话时会猜错(真机把用户自己的 `demo` 当成了当前会话)。 +- **轨道/侧边栏显示 `#{window_index}` 而非 window_id**:`@2` 是内部 id,用户看到的编号是 index(0 起,删过 window 后两者会差很远)。 +- **`TerminalSession` 必须转发 `TmuxController.objectWillChange`**:`tmuxSummary` 等是读 controller 的 computed property,而轨道/胶囊/侧边栏只 `@ObservedObject` 订阅 session。没有这条转发,数据解析全对但 UI 不刷新(真机表现为轨道空、第三格只有「tmux」)。同一个坑的另一面见下条。 +- **连接流程 = 点主机 → 留在首页探测 → 首页弹选择器 → 选完才进终端**(用户定稿,别改回"自动 attach"或"先进终端再弹框"):SSH 连上 → 在**普通 shell** 里跑 `TmuxSessionProbe.command` → 选择器给三种连接方式(接回已有会话 / 新建会话 / **原生终端**,不用"跳过"这种模糊措辞)。`SessionManager.enterRequest` 决定何时进终端;`isProbingTmux` **必须在 `.connected` 当刻置位**,否则连接建立与探测开始(延迟 2.5s)之间的空窗会让 `evaluateEnter` 提前把人送进终端页。**没装 tmux 时不弹框**,直接原生终端 + 安装引导横幅。探测的三个坑:① 命令会被 PTY **回显**,标记必须写成相邻字符串拼接(`"__TX""_SESSIONS_BEGIN"`)否则回显行被当成结果起点;② 「没装 tmux」与「装了但没会话」的 stderr 都被 `2>/dev/null` 吞掉、标记之间同样空,只能靠 `tmux -V` 的版本行分开(两者 UI 完全不同);③ 探测命令与输出会脏屏 → 命令末尾接 `printf '\033[H\033[2J\033[3J'` 清屏(刚连上时清掉的只有 motd)。 +- **tmux 不会自己进**:`TmuxRouter` 只被动检测字节流里的 DCS 前导,**必须由 app 主动发 `tmux -CC new -A -s <名>`**(登录后 2.5s 等提示符就绪,沿用 mosh 引导已验证的延迟)。`-A` = 同名会话存在就接回。4s 内没进 control mode 判定无 tmux → 降级客户端窗口 + 行内安装引导横幅(设计 2c),**不弹窗不强推**。曾经漏了这一步 → 真机连上永远显示「无 tmux」。 - **tmux 多 pane 尺寸走"路线 b"**:app 按全屏格子发 `refresh-client -C WxH` 让 tmux 布局,pane surface 尺寸严格取 layout rect(%output 按 tmux pane 宽高排版,二者不一致必换行/清屏错乱);pane surface 的 resize 回调**只断言不反向驱动 tmux**(防反馈环)。diff 用 full layout、渲染用 visible layout(zoom 免费);SwiftUI identity 只用 paneID(布局变不重建 surface)。新 pane 在 `capture-pane` %end 前丢弃其 %output(tmux 单线程串行保证快照不缺不重)。 - **tmux 网关字节必须单消费者保序**:多个 `Task{@MainActor}` 到主线程顺序无保证,多 pane 高吞吐会打碎 % 协议 → 用 `TmuxByteChannel`(AsyncStream) 单消费者。 +- **导航不得销毁会话**:终端**不能**走 `fullScreenCover`——① surface 一旦从视图树摘除,ghostty 释放 grid,内容全丢;② matchedGeometry(最小化缩回首页卡)要求两端同层级,独立 presentation 层做不到。故 `SessionCanvas` 把所有会话 ZStack 常驻挂载、只切 opacity + `allowsHitTesting`(沿用 tmux 切 tab 已验证的手法),画布恒全屏 → 切路由不触发 PTY resize。 +- **首页缩略卡不许再开 surface**:一个 ghostty surface 只有一个 IOSurfaceLayer,同处两地/离屏 Metal 渲染是已修过的泄漏雷区。缩略卡走 `TerminalSession.captureSnapshot()`(`readViewportText()` 抓尾部若干行)+ `TXTerminalMirror` 文本渲染(底对齐),代价只是丢 ANSI 颜色。 +- **嵌套 ObservableObject 不冒泡**:`SessionManager` 的 `@Published sessions` 不会因某条会话内部 `@Published` 变化而发布。凡显示会话状态的行/卡片都要抽成独立视图用 `@ObservedObject var session:` 直接订阅(见 `SessionSidebarRow`/`SessionStatusRow`)。 - SourceKit "No such module" 是索引噪声,**以 `swift build`/`xcodebuild` 为准**。 - 密钥/密码**绝不写进仓库**;测试主机凭据由用户即时提供。 - 提交/推送 git 只在用户明确要求时(main 分支已有 6 次提交,未 push、无 remote)。 diff --git a/apps/TerminalX/Resources/Fonts/JetBrainsMono-Bold.ttf b/apps/TerminalX/Resources/Fonts/JetBrainsMono-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8c93043de6454ad2d5575f0751150c6551d9c588 GIT binary patch literal 277828 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

^Q2@p_nJptvCyhdNtfUDv9XvP^J$Tq^Yf}I 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 s3n!(`lCJDArN|NmBd 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

Zh7cd=34FaGWy`SDvi)66*nbC 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 RXt+-ctVzYaTEQjd{{RH8A$|Y= literal 0 HcmV?d00001 diff --git a/apps/TerminalX/Resources/Fonts/JetBrainsMono-Medium.ttf b/apps/TerminalX/Resources/Fonts/JetBrainsMono-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..97671156df256e850498054fdebcd41d74a65d6b GIT binary patch literal 273860 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%0AmA2ptU z`zE>nAom61RSJES+%Lg>(f9@2?Hnrke}!+GO8G0ueTdu#kfUPb4#01bdk){Qmpl)G zUO@b3$UVn#rG#VB4(AV%`vh6z7`ca(Wr+TQgvC7UWC}4?y7(RxV!;JnA8)qe6hId7 zpH%!}yi6D0UXyMP++UD{nNoidW*2!E%>;i!>LcO=Ur+EynWE^DGyX2hMPv|9wX_EK zVHr^-OG<=fiHQ9&RQnC_TrKI^Bb4qm8LB->Fh)CYjutX6w@@mR$elv&NOG?sId74i zVU*TT17#sTLX?rRbi{2WZ8U|BBuz#dQcpoVVOcK7Dar*dNB_gJgdp%LYvRn{~ zS|q}hJ6qNbQ7P+*2+KNyP}CwZi|8?SCI4jv$7L-742rf|>Z7eTehA8Z3LQ%>z7GXT z9myO^GRMj~DVEEU63Z#HgmkVVi~WxBl`rk1y-Dy)$)kNt|1|i+#2+RJizzSHk~@WR zG?H?34e<<>^0l`JE~C_kQmk^)^KL4URg~h}`VS!aM&m5pk>m=7e+J4#(yf>@kz*#T zK_xy|)+

78rT{G~g#GM^93Ud$g=sb>@T)-N>tCtE|-mU6PsjY>muYf187 zNm`uXIKfYnTTAXp((^+kr;Ox}B=;LcnMm$PDupn?<;tbFVUigpI7ajs!DT8IyRuiQ z)%U=avj%}v7SbI_b{I?WSmGQ@C4w;n`OJ!{ElP3b>V42yT^C3AfrB3%ACqmzUXD6XadI*5rA(rOP@j zFZH*kkvoIj+2qb6w}adsa+i?19KR>(wD$D2ch0l!=?UyjdicQDB9X6 zfBs`_?!_gZ)>e7ZowZ#&hwtZ<;2Sw1d(m&@zQca;DtcZFQlt|_is*95pzT+>~%U2U#z*Lv3y*9y4nU8`Jc;cjwm&6@4n z;o9Y{b?tKG^O=(#zeo@I&dp{NWqLF7GfOhdGeeo1GOIIdGpA=x$eaRqdgkoRHqbU@ zc4schT#>mdb8Y5&U|TbHNbNIsW$uG}F!M;}(aht3PG+8hdoD}Ma%Fk5^0P|770Rm4 znw?dfH39CFtm$xPXSHQ@XRXRwlC=Wvs;sqH>$5gxZOz(|wJU31*1@b(Sx2&tW*yHu z3FuVTxoj=lmF>;W&o0R>hvuQ|YIkjRZT1AXQ?jRnGCR8s?vm^ku+pmRwQ$#GZ-Tou zdk5TI+555&W*-4`Jo{w!sqAy^U2e_oa(mtR?h>~wn~)n4-4onX+|%8&-EHn}_Y(IC z_bT^VaISZ6a&L9-0JP72(0#;x)P3B2(tXN(&ZBu;9&y3*_{x1DU$w8+H^Dc>H{Cbe*XHZ?E%B}Jt@5q)t@my6 zZFNu5H1|4nHqiaDJR9gKmuCY#ee!IeXP-P9=*^O61HIGb*+B0Wc{b21-z$Z{su$c{ z>C#a9X5 zOfFUjpvisYex2M+YWDoG4E8kk9()UeZo5#t{PXqNjwjc`zTzzMNyBg z1Z|^lzT!-#q7iiiVwhSQMu2S1i!V^-;mebS__AXOzGha2bDpE= zljP+v>Ax0ytat&wVJknz+=o+@x8h9F z3hj37)A%mr=Wrr$t+ozd6I`!tz}djB?0%I7yMSO`ECd%-|;ZgXya1`GYuE9yCaiR`i3vR-9f+vb=#1wo7cpAO{JY9SY z-}{}7Fa5UQE5B{{x^D-*=-VxN@EzYp_=fLNoRV5DR*022n|P;Kh41v6O3n<)I$bJ#i zOBf%@{xGJ?ISSJEl{3zAeIpf)6Brbpe}u#3`CiFi!uSR9JdQqY74G4DyG_PdtN7V( zvwu#}y>BQSt&QyB#Y|GB<2}PT+sW(jbA0bR9DZK$`w)lp)7f{@WxHgrQT!gi3eUcS znneR@8dsivEK+djZI`Z^2{Jb*?&vWeG z$Ki`OzC8Oc`Ij=jjPd0P&wgIvSyLE4#CQ|)&t`tMa~|uPw@HQPZBgNQTbUnkwv?U| ze*)tZIeZf1lNm=JNc>Y7NB>Cp4UA7`drE!w`X+n47gwJ{&-?df2=lf%0i?@_p)%h%82 zgQtbV=PBRiYvX1jC*ciockNk z48~`2IyWgljr(suw=aJ=%jNdu4>2B7{C?KUKbG-YmYdIV1B@3k&U$)E80UJK=9I_# z^C>&BULJ1u9&Y!Z3YCuUIVIP33!Qb(U;{bqTh=Rl`{yu3mN!zn~nD& z7ry13LEo&;#xEp2csKIlw_!P2E`HyXrw!Kf@t!mk??}V6;rNwP8Ghq*nf#3%epTs6 zGXK9NUHV_>6nG!4{u^1r|DmkH|DmiQ|3g_t|3g{D|3g_N|3g`qTuc@S3Y26wZ8-p8M%p^xy} z8b*4*X50<>T5T@e)y4+t>Mpo<=;?5m>rY8ny8-SJtR4tokKE_*_2?0}9lA`bLx(25 zc{=(B-#n}>2;XdDlXT_pe|+03uTtjD7ed<=z6xWpbg^b3e8aR>xJAbO(j|TJHS|M1zbKF{NysvK@HX^{{y5wt zI(Dsmn*2?%cdxz^?in5ZrMDY>hQOOI_BHX|i4=3DNY{J%-QNQqkA6br#NI_u=ADFo zMPSTFujP+vu%5S8`xM+JZ96C_;p41ybXW3COqe`a)I)Zvt7xLS+@v6Ll1gc z$uDQk`52+)Y&$R!{!8%RfjsDxHwX9;$ln0}AaKlhNUo&sMLg)^*#rKe@E-%-4Sy&2 z``~W_e-r$zz&qiig!Ag*qa^%o@R4TzWcZNnhrCOWj%PjK4o&yZ^y62jpz8Kvl%8PIsu`_{I#HsL+Gs(W2~kRI*z%V?vIeXFTlQt z?`azJB53D9Dhcy%RJ%unTIXe=B0`@?urce?QT%cj|js)7@+GHzNd(@gV849{L?aDeJy9 zpna2QcW8S4z3v5|K?}(H3glG-TjHJpd83K867ojv$xJqD@4ez{As zPl0xZXj4J+1Jix5whwj&UoC2<-#>V}OdL!M{plt`AowW?KEkyeqXwRb_y_1FfqwkP>p8;(RXs5Ggf_4Yd?gVW$ z(w&t1AZRN<%aeKYK)| zY}}5xuyf{;99Y&(cD65rE(<_w!`Q2PXG=aCBZ~V)(5C0WF7D@uHVL$CDD#fY37~Bv z+HlbBMJ;dB~~no($x{9V6OxC`*(?j-GJ<_FFG$Mz5*| zZaZ@kXm=0|BL>!)(Z^)Ap+&%c(61ac*v~gTa|URy5)C6h@lDP^9u%L^4;t)}(dL70 z+0rgErGDA_L7Scpo60`HoucPc+H#V1^6s7K&SlxZp@XmyKL!%kpR>KGg2)`_oUtMO#ANaI1>s zz)tE0(4<|)5+AIYeQ)|fxc5jt10$m3NZ*yc4z#tP-9UWz0$Y;~J+i5M4UD$Lu{s^H zvR6^uuY$G)*q!OXW#0yQ7;6KS+%F9!{6*!UWyu@t5AcKUSC zx`}TK_!fb0LUt!;9pJm4_)djFNfPgaWP+$9O?O3o!7HbpN-|< z)3a~Vbo@U303h@UdZs=F_5PS-`^&{Q8kLcrxehTzZdd!P5_r6N>tBJk?^pf;pp0S|I8&wY5RYczmy4N!@ zJs0$QL9df3L;gLQ;VH!Kxe59z;y)~B%XrT=vJSXTd0{*69Ymi<{3|uX^|Jen>p19( zK%d5P`aplq{i;jq-=^f)m5|d7`X28**FMl&m_7mYIiNr6+Tq><`VB-6ko;+&Z**;P zqh@+15Zz1kCeZJ4t#xk%eGJhXh+Yl)GS>>(6L?1wy@}`*pm(^s-OE5PBKj3XF9dz2 zYqlFD?DZ1;Mxwhl!#&eA$u$LZyh)=}9+31Bmu7kwVMlm1ETvX8}8ju(jfDu?{!&=-v~WY3sJjPPa2O-CEnWerK59%rACpLEfK85|i+pq(8J1AJE6JNyM>6gU4*i&7PwE}_BL0E3( z<89rLG%se!X_M*QT@&9X%6?Gj^o^#8?UIrfqw)ia|EO=E$WeDX$C4D(G2~g8yFiDJ zkSdaed9bWe=4RA$hpKxQcz1)|jR!t>Yls&u(nO2&&OkkOs7O$*EU8^2@g>QJd5(OP zHj09L;oVD7(3hZ|W@wLNpY};@m-dv{AigR#iu=UAs5_cATh&<(>d z4a=~NG{a@28yQBXc{!|jzvdC&6kEkZg7%}ux5akx9X%a4JWR$t5BKTl;jkj;(SNHS z*MDbZ8QF&0@EACAXZVdABi9&Y$-)TC(>|N#6;W{F-gBh z@74Qo!;+3Y)EwL?^9XLZcog?rJT9IPPvUNir^IgYU9m@ePwW-n7yHBy+t9Khx$8xJvfbU+&|j4+;`Hy-#^B8hrh}H zO3p<8R{w0@YTqe8`eLk}m8Mwi>C~DdzJeX)Ph(H{GuTy5*z$v{f;X))+yL|ioH@GN zUT1&NzQ_KOeXspxoIv`Dy}{mS--jEWHrZddH`@=`58B_vsicSOZT7e9hwX3M+wJez zkJx+c@7a6p@7w$AAK3ftr_pYPYL^+cM&18h?u*)oaIw>K_K$@dEf@p8xch&e#v$!; zssWo-9Y}4h;u~U%ctAY(pOyVZEU)U5td&lklC|?1)X$BwhWZa^S5TiNd$MP64&hn! zXP*>P^f&as>ZkO->8JI->u2;g^|Sh0u4ZunK}u%pL(_E&?}5l+grZ%>$t$rEB5+9WYjC;D(5T^$)-^rE8+M0vtil zM_~ee9&pU?;Np(8K^Xt!eh1zYqt&&IO=Jbm!dWBHfjxkKTDRshDF z-@g{Pq`^mP5`N4}2HgXHGoFX>Jb`B)o`L1D=j&4xSD?i}0+#vlD`HWy4G;2O z0v!VZJf(P0>H*|et_B5Ao&n@D(1~X$o;&fNyaLE$0C58-r@($Zhw&hPffIO6<2kQZ znSy!nhvFHHXAGVuJX7)9ga`9M)bMkL9JwU)g8myoX6~-s1Hn1LZa~lEJ|FB0t_1W# z-gCh_gZBV>G0z=bAAAtd(cD*q+k(3QoydD8xF>iB(8=60!NWn^`;>b&?+|XB!fd4Y zosz7Q!V=7UN+P9ON}qr~rgUHF^RTpTG*S!DjVAa;@L=%8k_gd9Qw!6L(FSH!xliOR z$FF`Q|4i#K;5&2oA~lIOQ9II&E7_mm;AnkD?&iFkf~bKq9j0Glj6*sP7M(4IwLp)d zH5s{g=Gj5iR^XRWyp?&U@vAc6W>DhAyNeH%#x$dJTpc^A(c;2GCl_(8LkK^5_e6xVknLgxb zF@+l=sISnCkc^jeeo%dIGReJ6(ZT;X=%@1n!SRX?KAEp8ly4Sr`ZvNif)zt17Z0U$ z3zR-)A$lGoXYZiX!4jee3Ew?vD1Lb><=ja46FK{XsJ*byP+1<}-xdt8oJ)c4&UprR z7D)adO8e#PAGBH4M8c77&fc6u!Cc}WB*O>YG-xGm)sXTANxgEOK>2!!KSuF)4w{3z zzhwLvg>TQ@hF^b5{0fTihaEB)m+?2`JSf*(5k3a>$jI58vmL)LRrr;_x8*z$G#PII zzBAVjq6L6IB+tiFyqTasnClOqrvNU`%}Y72;hr;zk0AX1+?|0lil52@>B`ne@zGnM zRh)i@4%XzSYX4D z7lyn7`l~}u4-X8l7(NE@nPA)SalIxf)9@3+&z9PnS(;HA81eSVEX^z$ zS2U@#q%?$^-Xi|z0xuO!0Tc@^&)HBk4WZME=9LaDjR0Cuw4}7UbOJ($21fh06fFa7 zMbVnl@uiY)UD2k}8KnyVZ7JGUw6hewPoSdciGaW80HEAJiT~cBg9v@D=%vy*r9FU- z6}?`%q;wT-mh%SOIU|csfzOM3w?-D70p$0`0!2mV!1vB2n@YP&S3u`Gi?z~wOVM+c zK3r^;?kRl^e7b)^U`MeFkQvzNKT(Vv1#Ca!7kdHa`kxQnU!03REWfz4bX)0eKozC? z0$HU;0FA!nVCjpccyB6>6xWu%UW%LUipLjEEz`^VfNr?NU6x-~4k_hMxsDfgZDL zV%dxeyjvG9FWFT#2R-P~k`rYMfG;avUDgACZSnnOOW|)R-dVO1{;o?#mLY!e-jY>i z>);@GW2Fur0%#o7v-D=5jueLYhOW|kc*dtI~2 zUN4_nzM$fsiY=OTSzg&2C3eNuid~vj;_|I5m|U{F>=fu5M`acCT(+%Z2l!8n(njql zm{;)_;FA@*%Fb3CtT+nzT*b@9uU5QX@s4JV&_;Mi-9Ku_sQsY7QT$Tz$q~6D@D^Hj zYD9V2`4QD4CTLc|K+5=qaT<$VCjZZ1;$gvV$W+%*Rq>43>v982VvC3VK7<3jH2KS0tb@MgwMxdc}z{ zydRd`fpQioN)WL z3cL1Ty{0R^_u1cfawCtclIs#gBwo4q`#h4HFdmUch*x9cQIj-dJjR2<=*{HZz4luBwf5R;?X~tk z=k`LXJICO5`-INPNKNeAzw?mJ!;qTVc|_-o&SQ|8*?AmpywAeE>L85RyN>R2hcEA0 z*S(_mfZj&yg4Pn((L1AaY43?W>wBkm&TL)Ly4H2HR`jgfVR84-JFIG-+CIO%#P#bP z-t$%Oc+_@s=j7H>*RQptbwm5bo_`NNqCLO881**XWp1gT;t!Ry$9d%wo_ueF6=7vQ zD&LlA++iN%@>~7r5-rV(Xk3rvNwU@MSY9G}8ar?M7cO1Sy6L1@ZIp^B^J%_`PG4?!YbZetrP>w<7Dy#xqWYD52~V zVX305bb zO*-kF$G3RrW7Oy8=zo53z68(vbL`^qUEbds-SYe`pvIOuQ!jdA=R zI&#DB4(i~i`NKhdq(|%!H{nkPFFay1v;7?0fIk~|;OF8N{5;%)|GR&O-|WohWjLos zWaicEjqI)LAK5>%PvPr+nSGsoldTUP(Vh;z&4#$lG1EArShGncToEvWDZ=ApKye-H& zd%-obx3l-)W#B!CurORK=Q53OURW;C>t?t<+=&xWM&Fy^qHt^YSy&_KSz$rATF!i% z;r4KUctFmCn~gp5t@7a#)z8Bv;mUArxK85bd6=6YmLHxUng0ak`-KtV#&A!#7iYVS z*!K?;!)4*}a1G+)BXCB@6VQ#q>~Ma#ARm&C&nM*j3yw`IO{zFoe3zC+%d?~J$V#v-!5H{$5~ z=KJRdd-wYb7uTR5<@U+34kU9sA_+!(GnH&%Eu4l8djx0`TgcYObLA>zYq zm6p%JCLM#{m*HRLmf~N4<#+;?i(_r#tKB2P_bGmg>xZ{z4nm&}g*EFhe)&cs6+;`N z8lzm7jCE5P>lQNB?PaW^WvqQN)^W|JF|v^we{wS*PqZdrDE@DiKsf)Z;Mxg{kCyI+A|pDWPJ1j2b_1&7nb3UqY>;WWL1yD@=i1t550oso{?n-fQIC@$U_APWXKkq@blN zCchsbog;dANFjnqy8R`s@per!(&NJ(%-6U*o}_O{KHu-xdRoX~K7BVnkw%maX*~G@ zd4T<5+M@UH%1HB&&}(|Fq}i_0ueJW{C-qRS?9@Q(7hjKO2gt{h?P8B0Poy>9`ZKyL zi+LwK<|)!>TKS-9^ol?1^FxyMuq~w5vYbheFLD@7ct+C9DeDv(;g!@Qo&ej`vVE*Q z^wsGRXJx+fygElr$NVX579|nS#fmHQ8HLyUeFM{xUgpB;+^LeUyppzbTp}ep*ILgW zGVhum4SUHSw!4e)U(>QLv;3Jdr<%W0U|W(FAIH+-0mXD5I1tl&bL{z}kml3!-{5&_ z{urL;z?;ePvvQmcApc>>|Fz^lBKc3^xibGfo-6XF@VpUTO@`CR$0h#>$;X}v`M+a6 zdf&j_U^dPzuLFNz0}2`FD$>$t?7C#GCEB2}r1|vSuo2}NmUYNn=RQE~Rd{GzrQibb zT{S#3rLAH{o}D|OhKKj8c##q3epJIRjdQp@cMsU*1<66iCtSQ=G}cQ+@D;c@3GQqwM~mvPXT~Fr-6K{< zNI2#Eg$Zt<;Y70hB7?Y(b+~P%PBRe)?q!*@&(-oX8+{$*faP z8j(^Tv|?F#vdVncWV;!uC+=+2VpVq`wBlMPPfIP`Tr0C|Hz%M&K%qMiO>6yX;VfNk zmpcZnlhJ#mL<_ODB5h^K4eguK^Q4wE4>MoV%$JC#)T9t3#KuXPuBq*i;6C(`I!>|wlv61m3{3bp=PtM;{S z@$veNSs2|rJG_~vIz-nBB*+chAm`)_a%RJL4q;#9>HKX-L%ZA4dHCr5BEj?N{2hR= z5tvWs;kAoi3_Pz99^o_vX-c%tn2bU+3;U!xN;L*y$ht^I;4V$1gY*rB3Rv|9>7xX* zEw+y=JYu}vd02LtNuDaxYao{AMG955?4E+Nb?Nr9`N7(d8Pak{+kA1=^W?iL*2%Q` zA{EVT-jA#+{&gx{VYsnStg*nfKss*iIa-5^um#nR)qeij`l;ITFQ}7tAw%RYdOv|_ z^M6Rilkd1Lc?PDC`bO2=bKp+8;wwcDNybiZ$^0d18Q%U-k4z!n`QQ&{S$npXLL%AD zO)AG;=mm^Dt~{A@j9nx~+GDzaVx4nMI(P@8SRYIaRB|w`#{i73NW)cRN+hgUm8rZ% zf7P_5=o{x&%7|E}?Z#uTRm;HJ0AHGG3$FupLTf|~5e0vvlh(4IjV%ftDY%LTRBzXL$b|H`lzH+btLV_cbfHldih4BG0f+4_sM6dDLx&16Ysg&It>y2wh@_px=H4r3OS z*E}f}N^6g|XAB1olHla#Of1LK1wG$D##bETh(sl4$|UN@>W;$z5~+*M9wOm zT4$!E6>?l#5sh2gIhFWmfw;;#H}H@LSL~&(p-!z;Ee*!%oV~YD(dUcHQl=9iVcNP# z154os2Q6`>)UdJ?I6)fRcdF5xyaCNA8_>+&fM!ma15Ti3czV_!kw@mV3L0|F@vU3w zo7zR^wnHBEIV~db+t=tP6!hHfIk%6|(-IP%Sfej|MT-^i42Sh|MoC}u9r0H8aaKRADCv){(O($pMNS?5?ymGlG#=>}*6433=!K*Bo2^Pu zStI?eHTu^gz1U&rURQdtz@&epM*n6(uXge+qo-9S{o5(M$ISzqFWu`8FnU^Q(EEWk zdT4N^H@|O~(u?&bJw_o?$bFgGaE`B`klJ)>Pkp>@YmXM5{o5?oZ~Rn`NpE2ie`3U& zte>Xk#kLcFdfHz^(1pKZ(TSfQ%ZpZbZc!|6@FfZt>#c}Anf1H7pcfy`-BWAt!B}2x zg!@&|p3=Xkc=Si%_s!p+HMd>A@kiJ4=S6)A|Er{r8dB-4zAqxZ!8qNl$~PkP$`5Kv z@WW$0Vpm45FWP0ni|!+S*IIdMQPR>L5kD>+pWn^!u-$^+J>qq~yu{Yomce`0>W}N6 z9u4cq`Y-yYy9jNcY2v3^d$i+(r^oiJyzY_JdK1rGGQ9=HAB^8plicz`pZG(0#how1 z6IP3h^741I0gF8%e814E%_sb>!m?Xnds7Urw|5<4Us7MeKaI>3^?UY!?a`*Q{3%9H zJ5Kma!}HcF;kgFWUK3tmFl{v9l?DIIHjA_X0lG(omidGpHO?vS8^xI&`qoypT0u~h zwfMUsP;@{>Og@SDj^~hWj&QWlHA0$7MU*gI~XJWA;7P-D&%jF z-^|BVKpFASD=czG`g?0I&U2#PGKo(5hm%>3PoGE+;@so zxp3!5#1ZnHkM zi>@e7JIfVk`k>C2f^V!t>8M3G=O|-ne{chnWgl(tlcNCQvC)`>5NqDmpfmmRT`aJkjTo-hSUD7HlD^YvXwYK=yTQJCe6xyQVRbC zm(F>N?J+JDeN(2NL7UQlq$zdU0&g`i&B&moP5(>2p2jPUNN}X*E-cb>N3Y|DZ7XI> zElZp)w(Q(9#swXPbI)3jD2>Gbp}-4g{7wa4;hhDJJRPeLddISi;p%>~T=&)vz8jTF zlJbt!m~GiJqkmGGu-QHHy^|=L~O8lw8nxLQDJ?&`)v#cXpF>R>^Nx e(zGt5CpEN~L4-NtS%SlTT0kWh_i6=}oc{qb=iD;@ literal 0 HcmV?d00001 diff --git a/apps/TerminalX/Resources/Fonts/JetBrainsMono-Regular.ttf b/apps/TerminalX/Resources/Fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..dff66cc50702c75abd025dcf49f62a4dcc2d72de GIT binary patch literal 273900 zcmc${4V+a|`~QEfz1KQT&r?l_$#LeKnMzF=sZ2F8W_nU#Buo!9QxhSC5JCt^2yurH zLg?m{5aRA8gb+dqJt1@xLX_tJzRo%)!_EEueZSw|@AV(Ax6j(^@mlLzd+oLNIcFzE zM4ItGE7|?~^zE0k*4-fCeK{gQhyE2u95wEXTl-5mzf>e~bN{1`E^Ghc%hM!$b&bgA zoFk6fuS@f84X28j+0;@s=G5v5!}BXY5ZSw{NZH9_&Y2i%SJ`wgk<-W{^G}#?;;Eze z3ojNKHCLobqZ6yonm~Lu?KMKPP8@&U3FG(3yHI5McnMxE88^1NCbwgi*58Ko?&FA% zZuO4kcqqsD<4&D8Y3Ppwn{vEeB)4Sz>0_$1y5`l0oL8aI|CNy1<(}(m)a5b6A5?jT20}~pL^7f2!2h+ zYoXL%z5O3}%Ht$~7Xcs!1aWNwqCEO0^B}j>IHFox`Ea|3jkGtsDm< z{{s!uX{t5<7br{DF$%5x52#(*rs_%mO*)ZZ$@J?|!e@hOh8)5JK*#yd_)oU|Z_*O9 zr)VARL2cCbD?s~N7q#!t$doyd^gr@vefgip>HO1i`Xg$0rXT-Fk0xJT_T>1FWcsV) zQU3pseW+W}vHL6k9ZOn2I2d%yhW;Y!2aX)`mg>Z*>jru|p^u^sKpA%8(;tm^82x<1sYyLJb@{S#8YkJH{Csk_dr zJ$g>m<-h8erq^lze;w97s~vg{JpxaH_FdO9QMbxqSYGUM8ru&#B3K=YKO(J<(kYFtXY5!O1jj>92y&C=@-=XI)VaXK7DkAz{6 z@u&806zI87*KV~>?N=Kz*Tan689TN9aySOG{VUVxc;tfCsWb)kqvla`ZBTT~GPY~J zOuDwMtMg86)Hq%5 zG@P*|({HskgQjQDIx_WXyK0A01UfHtZ1w`JSI18Ks`&<{Rom3MGRHbjI=3{BmQ|Z| z{$|oMs`FaY^jPcEx^@2NfX+p=N$XHDYmKUsiOZyGJ!-2SYuQY?o?{x;eyd%Xb57gQ zu;$fxwSRE>I8(lq;~`K68rFR33+=DYL57T+%c=a#%f_(V-=N`YM?_lhH{hs*SsE5An zU_5uV0&Ld0ceWsWf@f!(yqgHq?#?ERk@~y_GCt6?K*v<)hx$tW@H_NwnYiOX&&Q6C zS)*wumDUASpXfa31Ui>AujbS9dvCY{v>t6o728v3?a^A$HSErG*r95t&IjsB%}K5E zUib`j4*d;uJ<#^FKAl5qhpLuIqB?)IZq1{r`BXEowx#OUHBQqMolDr9ir2m>T0Rrj z{-`b5UyWBAGI1K8iPQMHXj-P6wx@m5aaUi|%~v<9`Lu4$qnhd4kAzFoWb9Jgwf{Pv zy8fzt!$JL~W1Z=9M%7M*Ig#2o>6p%i%r#3@ZFwKkcBjUaxtFR-Rom41)ILRzwaj8r zdowm_d|m2}h1#k4wVcMM`?$LgnR4Z%={(Xgp`R)LXc}#3_&<>v6TP0QZ&h`S)HaQXYtbyUk}{O`)vInJc*)=XWhdY=5@ zxONM5fyQUTnd7=~e-%#UT|s%MTRs)f`uWFr(YQfAzHVCGaH{N{={%ah;Lr1GnY!ib zhW}_w-D9;OW8d%M^*qz|(&>MiN9{`IN!gVN*Xe&IJyUnx`Z95UrFF+Sljo29{9XKC z*jl%qy5myUmXw`3xB8^{59fbs?&zABLD##?+M#n_*P1%>G*N*p%NQ^^rY4uoqxJ#-Ur%2Q^>6OIv=#Y!|KFo zx}IlmfnJv~*9je?%(X|?YHeTZX%D)-#nR~7{XA${MZ=l){-CM#peyNrMsMO3t(S7C zyhjqIEj}ZttbG|C;8@qtI@g_)e}?!nnRYXCE|vBJX%SF6Gxh2i{T0JWJJjb*jh)Vk z#eiRS>Hex7do9kNUq3+E8~*wUyaO+C%(YR&yY45zgUy4Ng34d$M=Jd8 zAM$D4^nC~8uXXS0KzObvvzM&9m(}o&yU5oFeH5*RtKg0Fv6d%Z*?B5;eovVFKwUKd zj@Q^@mXrQ9VfwjiHu=xR&WkwyjAQNZvwz5=ZKUhcvYD{%!FTol?RW=cuW6~asawOT zei5(xS8Y?-bu4Mj!CgH#-cFc)r_N!WtMoH9H&xXadVbt1va102L}=GQmrd?ZFKtTOSDQ;6YM0hg$1dLo+W7C-r04T`Ekj*ee}_8gaqJK5$8lYLmrgx; zUYrD7I1kyo{BHd`n&YFnT@a1u&q4_KKHRi<>N7h%4yAzz$F|AWd=NOFkNN7!d}%EE zNe{Wm>~AhMGtCX=HglIrn%B)L^Ojj{-Zg8?2WGAL+{N2+OSdBG;AK`hKaC!SR9@mUKCCbXM{7uyTS*;WcYIUM);m>WcRWO+tZfY zBkV{!%HC*iv-jAA_6hs6ecyg!zpy_v_%eEV^p5DyF%xST%Z}y5TE*hA{8-yqQLJmM zXYAluzu1J>d9m|jlVexK7Q`NoEsi}C`$t}o*Ep|5UaP#l^7hV)=jG>h&MVEkI`5Xe zJM!k{-II5J-h+96&wDoS<-FB-ALf0M_i5hdyf5R8;w|DW<9o%U@q&2Ac#n8Vym!1j z-Zy?|d{F$b_|W*V@v-r<;*;Z7#czq<8($n>5`QMXJpN+*mH6xNRq+k+Zxb%jFwr`( zPoiz2eWGLH{KVCXn-YIdyq?&VpO>FsFstCsg1ZVHDp*NE4D*=BYIt%EKpPd$mJUc^&t zQl649vfEPyVfSz%o|=lMt_`mb?+G6a7lyBd?^t7-*j$@$d)YqrNPE1UX>YZ6+lTGr z_9?r@uC<%&Has;2Pt8qxss)~Eo%U3hSdUmqtPh@=nDW$=*h7EtRLi_Q>v(E<-pst) z@zmXU_vSs2_S7mo^)a6M98XES37%?=rxNjY@vgf))ql6AM#WE#pC7+8er^1=l&7AK zKZmDQq&@X@B1mN6sWyrHl&88UuEJBd;i(<@lAkD$f}0BF7Ccz6sNnU2)p+XjI-Z)l z+f%3EsX6Ulz*C>K`yu72EAf=WQ*+lP>v}4Dlyh+&{!0A?-D_F>^sn}S+M{Zx)K0Cv zxb~u2R)*jHcD=XjR!-Xa;XlIL(d@9XriCYjHQ~tc=x}J*KloY>VvSf2%Xa;P|1Zy! z{&gwq=DN($zw7qiBI~~p+4%IvMH>fg?6)zG@E#lMZQ?Wfjh!|Bh8s5Au<@T8Pu@_o zF?VBhW6O<=H-_uyuD^BD!<4*v{j5#1*Wa}1=8b&!AeH;$4Igb-%YScdczFYsY@oLr z?%Xhc<91D5zk;I(%6#3>eElQquUUTsp*`2%w*Hd!Kcp&1|8M=!^?gz$ZX|c3_1Wu- ze8Pq|RX04dVM!{LtHy>lK80^L?D0waPhxuX@fRO|^zo-3zxMIVpM3wxcc0{c(&*#c zKECziTRy%(WNqzQY+2g^?*6dT2lL*4{rz3<|MI~tZS&ji;nvva!mq=Bxu5l04(2|1 zE*Kb&w|qrGmrfg4tD)ci!tLQN;m&ZEwU*VBJXt=?=d%rL^WXBOQ<{*gwTAWoHf{@T zk?lu_E1tc=&asQ^V$P$b_F22!zGzq2SL`bL0cpyH-)eJQ5%tf_hFjfS4gdLHCZ|tz zQ{1I)*6(w=Y1DCTI(@pko1+JT?~*vYoQ_ZK4lQs?+$-*Nx7K~9agiW`Pb2A`Mz6_MBLW!FoONLJUZahV$bU9&1QE^CPA-_w7K37-u+hbMt2#8c~T(7T-QpaT#k?< z`BnDfHE?%A-PjdT9)7d{`p8ZHlCbN_Hp zM5eeU=EZQV%?m5tlacA}X}b^CN*B4z7P-g6E|IIl(7eQX9uM=la_%q9`SMHy*@N#L zwUX8nl`hg&_T~CKQ2Ize=`V2^C8tQWjFCxllHEhjky&z`TrW4s19E{GD6h-2@}j&g zE9EU&Eg#6&vOzwTuS}LP#u_Idn>^FmBurb=%M{wyriU46hM1$xG3E@@+ngmivWi{8 zKcuC+DQ#qp>?0p?%~>mZ^F%r!pRk8mCtYQ$w3CfoiMB{L`BvJ?Cf25(rI-992g?rW zEx$;a?2<#|S2^4?lEJ2d^fiGTW*W)>PPz)yREC&lrnw9?IdY8ILyk7BMD$SlU zg6~ruXZDeiCTjMU<4sH^ngeCD$(OTCcRA0Luv0lirkHZM*p$hormtLX2FO&?UoJEK zWQI9Ht~Q6ubaNP2$#HVK87ni*QF5m_QRbK$xx<_w^UQd8(3~j`$s%*UEH;znQFDPA zB-5n3oN9{XX1>Q%UtZuFOWWmaQ_S`DN7+-}k#kK?xxySO*O&@<#GD&BF)}uCW#p2` zw8)IeRgtNYlOv}@PK%rwIV&Y7#UJ zng=a{mO-mv5B3Fn1_gZEs(sKd*f;19bP75Lh3x8zgC0Rn&@;$p->`qsHRu*}5B3X+ z*hB0U!9nJiptso(9L!Fm)chJ8Vs-^(><`NW3Hk&k=o9Dw#E#}=X(F#kQ+ZVy$qE+WWqjl8IrgQ` zOO`C>J7)is{pDYBfP5nd%6HO1K9fS(ES=;F=`3GLNBLX^nfh|5sV66!PBPx?CpD&> zoM85q6HNyhXFAGQ(_T(9`^yA#fSh5v%9*B{oNl_vjb?=0WR8=+nNf0!sg_&KXt~Xd zk=dq7{%+2e1!kf=Y|fFSnG{xsqr>CEG2y7NDm*@{am(Cu;Q?-`dnP=I-ENPtBs@5b zh26sL!Qb8UuGa0c=h`#uS@vvuo;}}AvS-?f_8fbGz0l6ESJ~^`?+!*}g| z;g@!_EwCNKufh$sVYtyAXB&i{+QM*+?PObpKik8?)wXlEHvHJ`Z^wjh+EUv){3QH> zD^kqXw+Gw3!q06}JKRR?-r)|eR&TM>&bEzhGutHmDcoX9*lizVN3i2=VRP&uw#+&k z34gUm+hgr9wvyd=g`HqewWrzf_H=uaJ;k1EkFrDTQ1*UQ>qt+-Gi+8|&t}>)Z@?wY%P3 z=}vX0xI5iA_cwQiyV1?$I(56d!QJdma_6}--L-DIo8%sFv)z1mhP%aGH_! z%$?{ia2L6$?gY2az2jc8U2PY8fIZN5v)#iV!)@UYcAy<#4-LNyzYVvB@7sR1ukGW$ zaa-MY?tAyO`^D{W@3}SZ1GmV%;2w3KxUV9{edJzt+ucv@4fmE??cR28x-ITk_r6=< z-gP_Ohg>&5c0WYK{p?n`7u~;HXP4!Axt6Z0YvQ`Orfz@N&~x| z<|3}AJK7!La$FC0lsn8dcfDP;JKX-{j<>(Mp{}(%$W^()_6K*I-Qk9~JzPh(mn*eD z+wWbu+uJ!;>~h^9?npPt^>Ic1MPM<(#2dqx2HSU?s65buiMA9a_wE< z_I07%Y_~XLzq6k^vESOyT%|kIe&vpJ1MHWs#*MJwxG`?H{o2;rt@cxQjO%Z|aHHKY z`!5%Fc`o7dT|3v-6}bIecXxp6;Oe_<*VtKCgFAKCTxE&Hzh*lysy@I(8yeUH1tjdnHn9_zUG z_<%c*<@N>kPXDwoaqqayK4+f_2Zx7;gTlkYBiS!M9zGc^4xb1g3zx92@OMC^-%sd1 zj8!K^xD1VX!e`MuPuLlad%}~^geUBY=6k{yP@WVhwis>eagU+xJmD#5d)Sw~d^y^| z6Y>Orl`Dm9iSmtLCDimzo{(=6v%;kaDbF4tMaW$MS6*RfBf)A^ZQ|)msP&b2tnSJ* zAJ{$7-X6;n4zAECLan3J!=6dFT6?%abM+Q>O~O7(*mrnzZ2G3re(N~)2krZSG^OaF zX|(*nG)JI=(j18nPIDA`7#t46K=Ub8pu0ome9$uN6WO^5`#8@f=+HD*q51?wxfYHA z9lyIk{jPQ255viZt4nm(#RGUrDnss=idz z*DKQuMAer{A9x+!fPSzljrQ@)G^5bB(wu_6ou(RHon{RBPMS&RyJ>U|zvtmTNw}Bs zoP)lfW)}KEn(NRHVJ%z_AEnVb^>LaD&`&(vYl*B&qy1Q)Mt!m&jgH;MG-~^%G&&xi zrqTX=mPY&Zc^b8AbDFQwFVbk;U#8JIzN*6(!eFw{e|d~Swfr~4TXbt0?bEj&lZSri zF`d!xJx1r%Hjh#N{@^ja&>uaf5dF!Mdgig6Fqj_b&mJ=p{l#O3pgUkEd5%VZ^_Vlz zT^`dLt@UtEst;;XaCa)&pH!ciFWgh6;Eq*H8a)TJpQ*mKM4dbtC+K{?F zv`2G2ItRJK_P<&>2-QA-`pQH-x}V?qn5{?fUUnlJd7 zd-W8$k1z!u^{vjK)I2%_)xLrHpZl{Ex)0#K&7))7!K3>H?&Cb_6XslM-t|R0rO~;- z+)d5j!_fWGsNZzFK=&!!2YNWvszEq4pni z&%(10kNT~@NB1>68}X=b4^6W-I?$tg9G-o6VyKQa=pKP*C>|XH^$qAAhi5Dv^|Ov0 z=>9_Y5Gi#0bnNt8KMy@BjgF0u9q1lG&;JzKZyh_3DX5N%qW#vf0J#_)mPW^BxJNEU zbzBr35A8F^<*1H}qGO=_1euDCN~7ac<&n$K>NGl*+E0)f=$JIx?;4L>jgC#DV{?K> zrlTjO(eWGSk-6weX*BL+kKB%)l1A$r?~$44scCflPV>l}=;>*+oe3V9gPxH_Z9LN> zccAJUMQv2Sg6^|;#^KR6&++KK$()-;>zL%xJ(TYMQhUHf==pWHfH3GDh^J}4aUo&Q zeUedsE1Fk*sp!1B&co9;;WvdTcn&A#22Z#Gy)8{kRL2o?|7-3_Q-nU^k(<$E8vJcu z^XM-U&FdcB@L`O`~)3GmqYfna@3`YvGrK zLHF9`t2D*vHjm!JnIAlQT{SyBJarWFt4H@ZW|t?mN3SKUP&R1okt4`Akf~PzY=4!Ci6JfpuFTzXgX)i}# z_C%O(0pq4bu0-GWL@q%;@I>%?@S!KdxfU>%)e0PfPQjkwxal2 zKg-i|(KhrL=9X>b(evIm@tAdJQ;(k0HXB+{b_JRPEeX>P+Y0s~{3eS3^_hXecAF3Q z-mFL4ddw!YoyTlMnHP3n@_dSRfR2Q}Knp!)Gup{xzCk;~e&pGP7Qq3GmD=7Fx)EkR z+V0SkF!tGAP)=Cw>H~cVW0O7$Qvz+jzb8<;2Ed`jW0M^Sg9vN;gFS)Tb(lxj4|_Nq zK_2F`Wj-l^j?EBHfF1TIPoU#B)T8T=J=zoOj~)Y+l+}I=gW-hfryb$Z^~sL(=-Osk z-<3eec$6n#?CeaBuKo6UPtXj#!DD_xZ}bE@Hn)1rcJww+P=em=F+ZdCc!Gn_hdt&O z^bt?c8%=u54s@X>I2e80qwBhT!V{FDPkD5Ww@-V5L(nxIUGMGto}dh6T~u`4x2%my zfM4y$9!>|bpLhcGv(7UJ^t{oz1p&UdpLqfbSe?@l=v>fw20@7G{DVN}#Wqi1(H}f~ zrXvl$^ys=4o#N5wG|@{ux@U=A>d|LB(aSu#--%B3=rf+^I3ypJ7BF z^zf;aL?80VtLOrcK2eVT-J{P!qRaB-B>I>~pUXsLj9(^VeUFOke(@|{?^qE9d`vCf^I;wVqKA(uHtstGz7d-l`BKo37 zpLs{sAE3`UqB?e<&xWJw577JXsQLi(S#ngz9rS)Zy3(W1Mx(EJ^!_jUx<|&NZ+P_n zFS^PjHRzijz2}U+<6y=RJU^~h}WTaT$ibx`n$Xrn)P^j;xl#KYR3kC1yM!^F7wk6Efdp*`AOwiM4`RV>#N(yv$>*qc0zRV?ZWk4N|Mgv_^C%;T1!c^=0(6N`I7{2t5qguT$Vo{;$* zEArSLD1MER*J)q6dcu=XP3wsb%+**497MPn?d=IQ?qE--+Q$>BU45Y+`I$?x2_D@$ z#~AC_dBjK1^F6wEh)wo{%%Rv6xQgq-CiEe|SK*5&zEO0q7hCMnePK+;9dvIP`-dlF z9P(&S3GPN4dxHB>{GzyjqAfiE^CS-+Deid`|K#DD;1Lu*D8WKB?r~3{jIE;gE_t0j zZaG@&(S1YSbdRe=ulBfI=uD427rowNIXCle@Yu7^8$FhJn>Pz?V)`J&tuE?-P&v9A#}$>{L{bNw?Rb ztQ(5kAc8AW*s-Yb=$<|vcy#|74?Q7e;?`qFqt2uI<~Vav(LHgzo+tbY&GLlWR(+4| z<>L)J;YL)C!5)V;^60)l-oz8qhj>$u?zQ9Do^TD?%%l76cyo{L@#D-*CHxs>ek$Qd zXlsw|i{pEGLdG@DJXLhh9*=tTo*i;u5dM_B~ z99F`g(6c<@7W90N)p6w%h ziq-MB&0|kO=Xk7+>+K$^<9&xm@9pAudi0(zKG&o7dGWhEdQTX?+oSiB@q0X0eLl}) zbuQfNu_vO7J$m0BU*ZXOqECD5ICQB;p9{pF@z{FkbMQQU3DD)9@I&-PkKSL#S9rp= z(U(2p_vkAgy(f;p?g>9YS9!v9=mw8|`#1izC)|X7;|aH;TRrv!^jlB3p0AT&lj1ZS zdK_>Vi!z6Zl?n+fe$ig!nPR`KE-7bE1JKtUw!jLVS^E?QvhAZ9I;CB=+{W*HF%% z#6HyjDw^-n`{P7`$9;yj^*H*KXzy{Kq8&YMBg*=txUuMg9*6%E#U6JZ%KD?Y8E8+B zyBh7~aoXP!kGm2*$m5uw3DzgYor1DnD2}<7DD}8;=pi1*oKBQ^+!biK$K8nb@wl02 zUyr*K?dNgK>jeH*oQ}f)kK26aq3SKJPMMrrYYek~UjgIl)$sy%8LDN#sl949xC>DAF}RCRogd()qUsxP z+E?`-xOM1sk9!Bb+T&hAZ}PayQS~3_v%Q4c!92BHQ0))c15rI6<`V9P>R1DBY?Ord zpYaQ~q1rdF15x!WzOn;QokI|QhtBte-=YtA!mX%|DTME%4|(*tVPb*D_C^2hv3=0j zJ?=|%2kfN3-=M$3F2b~xFCOM*_&wBl+y`jH;})UyJnjWF%i|tJ>wDZM zXakS?3T^0#XgiHO?jy9Z$Gwa;@wn}1Q;+)z&GxuA&}JU@7TVn7R--LE?rk*3x{h#^ghEZ;$&Jje6V<=sq5XN%CVJ z_cNO3ajVdT$GwQ+0LA@l_tzN3btcRhDUNY1z$c39h2j^*(We6Zqd4Yd0X|Y3<5xiA zienxY;3LH`mIYcDxcyOlq&R$1fR7Z{1=VA4_@^L^>qHnIDVA|4&^Ev_w+pmhaM}(& zQd}X5j}*&XD!@mIKEEu$M~YJ$@sZ+sqS_yDN2B;iaYvx|NO77MA1ST}ijNd`6pD`& zcNmI~6sK+CE5-Fj@t5MNQT(O2!%_UDSmsy({!-lWDE?9`^Q0i|aYIpjr#Q7G-{THK z3p}n0ZR>G^(RLnvUR%)K<(0 z&|`2~cYlxFg%0qz3iMEq({kD#IOait_6?lQLG>Rv^_$KuZ~?0G2Auj(=P0-k)q25h zMs>WvZb8+r;0&ts73_DY&L`04{slVk0k=rOF&_IZdaOr3D=4V+IMrbur~Mf2v0tGh zJWk8$c!AS?>zo7oC93lqTn(z@4o=HedF(f6wa1M?M|+&MqsL&sMzuWHT6C<(ZbeV< z*iX?DJx=>H&g0Z4CwcU2Yn*vplWf$>Zvy=XqQ~Ss7DIR+_dI?NpJa0v>^4JH_=^pz4dbP*SM`w8K{pd9wdmnnO$KH!x z=dtt9nI8KPdOh4oTTh^~JoX;+Cb*OM1?XJ3i|`6m?Vy}}1zqH^Z=#QS>^taUkA5Cj z@R-N0M<4gtw@_^l?7Qd^kNp^Z(qlKEPkHS7=+hqiA-dFK-$tMD*!R$9J@ymyA0E3A zUFNZ?(SLgE8uU4jU57sJv76B49{U0Mf=55+D|itY1FPvTd#u){V*&Oh^i_{ldslj_ zw*R`vYMXC(toBcBV$7`e<1>#{TRw-ch}Sy4@mTHmj~+|^+Hx)^kt+0bPox^X(&O+` z+qrNT=LLRj$2?Qq!)Qy7dj#FX zq=d|)c0a&RwEH)aeOq~KIXci2evL9d`%WYO(Yt>FBH=E=4kE7sXkNZ|+9b>re zZpInEuVakXJxcf`z8*W;V|${MQ9|ZJ?FdhJD0-YHr2X1oJ?=-odX}9+f;+{$Qw%+L zHa-{a1-%L1gO)-c!t>F7DI|Cp9S9Y~FG7dFDTJ9zNhypa<><=3!(y>POQgw>gOZV> zhF2!L<|f+@t2!Yz{piYMP*DA3mSoA8F^SQ+d3i}0mXz{DA8L~Fsy&hb zvA(gSEvTkX^~x(Mll2m1$*e>f9-v57r(`5G+>Au5W?{Y2Wid_E?&f;)_5Vp%)ptzN zb;u*JJT^TxowgPhxdKM$$jYjU-0C62Dig!Qs#{9DgeQ{B4B5zm+CUMikg+b6aSySDoQ!=}dF2rKVrsV^* zX9Vz2GFy*^aFm_$S2HTgP7O+oF&Km4$!6tMvFTN@WHW}XQ?hyCprMruZB3tH`N_s( z6O%e6TNDmDvU1Q-J~B6t_!g=7oWh0Dy!`0Oh0U9nCrx!(vROwx<(OJ!3!CcyviV=q zw89r*K}F?4Jr5bKvgvr3X0towB`B5&Gaocvi*P1u>M(lJAN%_g`+H~og$oyov`8?> zHvDT1Z)0nT|jXX{LH% zPFBa{)g5!=Oz&3ov1P|j$vp}e8r9Z}x9XmS3q#et3Kv?{+`@%UwN2r|i0a;j3+t&y z3m0ao?o+t1zG}xpM#GPPGE&7zCt{tG<~TiDIwcG1B(~a}c!p2xR41|R?!+^FVysY- zO*{T&Kk3wCzN^~Ly8X+ee=+Qe(?8V&{Zq}Sf2sxaPqi)mQ*B58RNK=()qUxoY6tqK z+PN@Rk~)$0D~weoTUW(c^h}k`iE1?2S?6p~VRFBY$^AGV_vcjT&y4-k6EaaBD>d_8^teVJhO z`BQ5(URrS_=dVxiMDfC|rlt0^JL5qI{k zl2!$=&awWwBrvpxPM_XC(Vr!#k_#WJE?1;tV_N1g!aZ5LS|#@&n`JpINM&2tNXn88 z%R7#p-Z>GAl}xA3Ucb*4>+I`GMiOP2+_7Ypt|_HQRz7NDE|&YKZR_?LR;DX$0~T%C z$AUz^s$>Lj|NeC4%rYJQRmqlD)o}80QDw2S<<+@_tA?>Y{IN_mrm_Af`c)U_CTO7_ z{X|l&P~V?4f+4yjM_5E~hGW8U1^I&|)L5-oD>WkI|LK+Vx3-upC7H2^VS#I#9*aZ? z!*tN@q+|mYlUS@@qQ5q(UhJJoOLfMNbW(;^c8-;BL09VoA4#RVU6G6waC9h1yZ0{M ziT^(HsuF2$vGR9H9-L_{HTr3HRp|!jk6q5VxRmvyvyNiF?@j6ce>TDXC$m_` zTi3CYL~(9jo%xbCENy3h7T}&8Govzqqh1~J)Mq*_>E7%ffgLf0Bl39t|9=c{Fg0=Fz~hh1^R>IiX5IT-yl^D}2=WxZ#9++z8EQ zG8l!P;U z@DPG$>Y!`3WEn?iajhk&c_#Xx<~ci+M-w#9ITSu)XC z$uPfSQs#o*Nw@)AF@)`;xt434zvY{-Q0kR&e|^aQc}_a$L20AIdBH~athQOL>$wK$ zs3F0?pm(HmWFMD`YE<@Yi)GE0HC|SKS)`Qbk`2k$tn67S-Tj}&Jt-mA^o99m+L7EX zOsmx6nm!BL>+zB-@e$HzSnk4h8ue7x6fw4R+L)oKoFwR9X`_l@Ku{cMAKA;fCLNZT z+RKyn8ZP909(6Sl>BGJ7f9r3!43`StXZr#7$8EXy9vp1qO{zt;+iM4t@>|*mby7D4 zo&QsMSU+99zx3AuFuiIoFr~HUaFjX@dz*{Ga&unV&Kh(zY~k#+&<09?rypiKOochX zp2(~O@&)7z$QN{l!9X3sWS9jDc)~CVX2N`)ahUC_9-1XUxh&eKPaE}Vqdslar;YlwQGY66OMPsqzY;dVPQFo|1BK8BMv63y!B&w* zRM?2)M%(xzcO%G$5*P~Of%+P+5otpEO|Y%Wa##!GZ%Y2A;5g*d)@d4~&G_kc1VmPNcbo8lcV=r9e9^Xs5+AksNHx!Oom{uoPCq7Lk^eX_*ft zK)Wqzx8+or1B-z+TTO%+K-pH5ZAIBul-+}}d$bX0Ex_@f9Pi2TUXx+7NbX43Dbj}H zHnU&>Ea%@hoCGsrKHrGl1mxK#2S|?%2GU~N_@-VXSON4cUJCRlPJiOlU@k0yRX|&* zPuHiSb6_#71nSmLc2l2ta-7d`KF6s~7bwFCAZ;ENpDYjd(!vKhW3Ds`(oq1*tjow_a$$KSv)nH2H4vXdplxp$E_lTSrCI_ zsDN=W1;|%OzC!X9ZccrHT{^W!i(mj$@#Xb(uw7)oY>^^tD8h#Qv1fnU+n@e*$>EFj zh0q5^igYF1O@K1pw~8EyO$RQ3<**jE@sgBAK)zzi6jP>nBFun!uoPCq7LguSq$hcM z=EHJc?9yJO1UpJpOQ8}bz%-c43tmV&C~HCwl`&aQ?@r{ z4_*Q5V7o|Z1&o80unDmFkik#`qWwce+73Q&7AikVDeXzAp5e$GTb^mPNDXN+;}A#(a0emF4&WX-zqXiU?dQC3GG}m9w>9^P{7v9$Ul|xQ%RpnotKY; zog!B>f_&%-Ns%jOh)m0Y1tM2b&sCFQ9ni*f%1mD^a&-|<=hd52KUS9+tx@7;tOVM; zrUoVgbzMUn*G_6PrzTG)nWG>;k#NU+#;{coQqW!x^!V+F6gx)g|sPmqAu#_LX zSHc9Kt$QhVZw*X_&9GhMKJ31aeD|${wXlVk3{^lCQ11RIylf~2ML_%WXTn@a^5UU< z=n8#cC{WJ>)bjxKJU~4U>=bzr8y_P5p^<=X56ysiK;DN|1NjzMXafs?_`eSjc{m5K z@e#t0Y!gW?=Osk6zpw%*vxqtu4F&9Y6n(UYAL18639J@*Y&>k`Wkl0}dLE~*Pt1oU zyo9I-Ho;C_LX-oPdy@K|90?O)2F&BdM00ov5%nyc%FBncfpK_-a?fnz1w_REgX0tn zM3&9w$7$I5PuhB}5|;6@p_#m3s5MOH1w)NsE>QMG>V2sVtl_0XlX#g>g~%(^^(uK^ zod8>Tc~A{63@U*oBCn&bqpL*Z&3qv67J1%Y&bD_bF9Mn-@-Ff3!I~l<{(ba=DZJc= z^tH78QG1b(vG0={K3U5KY+hdo*tCIs8?bBRRG{8X^I<11>mmL#Z2D{xKU~WK%50{d zFR1Se!e3UvCSJxfkFEDOUcNH`wu^jCnXikX5+=cHSORNcJ3q>64aHCilVCP1fi-Tl^^iam+!G{+XRsx$o~U< z`H{RoZsEl}*#A=@42JPA0~Wvvpv-oGX}la}059;N>@WGy2da3954xiUsBgy{k)71B zb1E-%!OmTz)y{?`KwY()dDbw2=LmcXX)1wF7=wJ^ZN1@0SRuypsibQJ(O zz&uzkJO={eTkhngJvBgmt+tBUqX-7T6j&vuHT~X`GJ9E=0Bd+z&m>qarVaM&-3Gdf z;n%7rMmSa?CQqO>Ooq8);uB#8&<1;U!=BwFuq}}fw3Q%V0$U2Gqb>Q`CV~3fZV}Ti z8`=Z;+ASB;9vj+E1L|$R1lEY@fNdR$fIJ<>^CF%KG3=jBr)6UHqn)CmV!F`Q0p#gA zUrcw(AGl6T@oX_Y2=`b39QPamNin_XN3X6x{k^E4{jDjP1mroWHBfJF?Crgl7v<2_ z!IWX2YD!1KQXsw*+y9C~=tmj#m5&qCr<|%JnOR`C|H0?*Qx^uvyHZqz@Dzeh}fo z17NF|!)Cy0AbdD!M@$q`F&T(EG7D;8Hmra(Vuo~uxxDCxc7~EZbQUkR$%dtXoyYWn zC1Q>xojstbtOWWvjJV+yK-m%4H-a`skUnw%%mn&z9Q7SHSj_Pj=JE0xbksOlC8nxM zOtk>{MmGZL9lch}m{Oq5n$2QPpxzTpV40Y4MSKeYJ(+%-vWZ6mNnSET`f0RrdOpnO zg)&olkqmlf1?=R-F%yBfvzCgPNc|Hjb2eqp9tY$*CkIGBHw(JLY%!B4JBfVf%@K3{ zBv>Wpg7z>2wuqTb`s6BDBIZKkFPz28VFm#8UxeKkt>y(W*)SApfVhj-in)Zmm#z_W zSrJSF?3+qi_D|*tj<1+4=1THhxme6J;-_sAa}{-7Mfs~VyiLq>%1$2-OT=6~0kG$4 z>X|VBW{SBc2k6hW0@Qac<*us$!ZR(9Zzi@}KNyyYxq;&wR*Jc?7^c7qF|(-qrYbS) zd(7Vm&mIR0#N1LS=GGcsXww?D@F-@knA?YnxuZnPodT=G%*}^sfUS3tcGr9{cW1*y zSS{ur>b+;Vn0b`FcZQhzx&ra{j~A9zp#J&W#XL|6)b}9q4-x-RF>DpHU;xne-#Pv} zU5-Xv+s39wnrLh4!68fb43HauDcOL%lN85RS19%};?FcnC9jCvoB0rfpT z1C|1DPmu2k@;yPmC&>2%aZAXzgnUb;0r{3}74sx*JV~A>my3DILNUyQRbrl@j(?JG zc@|Uxb-XY|%!|aoI9tq1gT<`i_+`>wseyH3UM26Vr9c}ivw`r+S+GLPYpr2CEEDrO zX|Ge}b>d&&F6Ir=-Y9`8SO9CqtfHM&6)*v20_9d=!<)pvN!~Xp|K?O!3DogcSD^e` zo5Z|Lxwn_{a-kfc{nb1977h8|N%E2*^1Mqu?@a{Cu89HRHPrvUg^@59sPltLmiUGbJ|W*H)cpy^>jQ`DX>n==L3MUpKlklxdImOA`|j{LAftx z!)7sG((ad({c@?8uLSzQWLTVf9BH-?{#PR){$KOOeBB19>+4xS8{g!^IG6)F#cUl6 zgxL$3Z$|?4d`Fw#(Z+Yn#e7fx?`Oa!G24oP@U|_yaEb5_Gl4$*SP10*ag~^#+Q39u zB4&Fw427j)e$IjMKs~?E&tH;ab~FOo+_6B+PQp70?i+SNN+M<0`@Widzm1660DPeXPH6s8VOot18wE>k)UN8SRp|x^0wM60eg~Q zkGT@Gu7Yh6?73Wmy|ze@OB-!suu6ix$HP_$qC)}QhrIi2mmo$RG1|=|K0X*Gz%&UG zgcH=2zf^*P9GECUTk^D{{dQ|5XivTE$-}e0pgnoolXqXV!%hi0lHSQejRc+P$9^*< zD4H$7{8Mc+LmY^^7_9fhp{`a3I!GL8F99kj4 zz^*Vufad_*}}LJ6nQDZ6r94yywlA;QT7sD!~QRIeC)=7cQ0HBGN9R{fkRsy985`5?nGE zX#3J4SO(<1EDI`Orvy`DFjs=hi-EYymrHO3dAY6!SCa3_NiYYfW7<5RooQ<&xT;Em z=>i;IO?bvQ39gwX!L{VQj`G)SlVD~apxpK2CAgt0?3Ccf8GsEp5}q|#f}61UCem(R zA_2EA!7WoHxUB@{OE8D>w>OgD4*GE?_06Ts-COwL2|*z`aSRKaoy9vm#eLl!9e5NQiYTTlUn|K0}X!deL)ChlSE ze3-T#p{_?J!3-e%k;Op%N67ccRtb{SmuwA%Pzpm~983nvEF_)h3Bkgtutb7Ijez4t zvw-rCPLN=63@Tx@1dmbIW5hkSK!V3}pbCh8A`2+9Bpb%VJfOZMD`6dMlYq|#gD2?+ z*ZP2G4Z)Mt`6PLtoB~T>s{~Iili+FcJUv^2rPRN4vjopJ@1AnhOIT{aP@@1N*% zgC%$#o1UjX&y#QY0tsHAe=iVzaSku0n*j5Hx>ppzBne*5hmzfdd_Kow&1ZL^M5Mm# zU)s4oA9)65un3DZ=Z}QOgvSd1V#_E?$Q&V(RWIVmA2xH5JvtuHEU#IC;h!OEHr&kH zb*(AfwK|7?`1ONd+nyTq`gMBHi?zQCb+x69dXg=LrTaEEmKzSSToG(Aj)sa4b2Q06 z%W^m$hGmhhUbAM+BK7y|ctDE-S|r*f>a{qaUE8+pB9VGso;~lG5k(&rl`SdGZXN{9 zv$dh;o-<9>mmbr&@i7Ne8UJQjs=e@WK5sNc)QemnjQ-R3n}W(Z@jInmWU0mk5iO_n z)OHNo@?Gh6kyOsz^-cPHx!v*4{6#$fmSNp?cIW@c@8W;6ch+CT>%Rw>>EDUr&fWDa z%f#=tHPo4Un`-}MJe;2=Scm3C9;wrW$-t$l9yeU{aoq`UjR zVRs+-w+{tV8I|gfSnEbL zs24c?Jq^aWMe}CalxoT;rGE|U?@h1rS|(c3p=JlP%hD%uJo;@mQO^_S~aIGs-p1t5;vwkpsH4YMB$M*D@L)iSU3f-Me*do503& z;J7zx&gkEx;~@oS)m(8-&+@XK=T1KO&_fTN9DGnStay0C&~-Vi^YN#edwO^4*8A5L zfYt$y>~sX|F1rSVPhOq_N2x!H$9Fg{%QQIfRRnv-z{bTesWW3 zgZiePYi~@w%#80Yp^exuGDRZ%+cx|Hn`ejh%$fA&|6}gU1LM4^d%ye5KH5CmcWE@z zjP`vrT1TVBl4UI(OO`Er;8SkaP{ zUhg4#Md*7K`%2RHL8has%##2CpQb1|iYI_uK_h&rs(iFbzRbx=PemWQ)1>ocE|%JC zc6n%;4r0jpFOIHub*+xBtPGBi53Z;;I+lmR|2(wP5!^l~Tm#!dBfQG4y)Q#4QG9`R zFup}VIw`AI5_J)a(@9KcShNZQZc@TNEh#R-j>x3Fn(lNsY;^oNTwdN68^%DJcYb<0 zek8|^i5=r2kT9HIU>gu!Xgth!PK?iD<}>F1w{e2s4`Q5FUZd&DyGthZ0+dn|< zw3axp)3BCB%5ix@*KD$&HY=T0lL|DvMM$eY3q+JV($@@9SymEwKz_xf38f;Xh>mRf zkAzYzE4UJl?-aoMolOI90N?q$>@g!6uIsRwulWo9!KRv_#Gp zP1|6#qqW1=maYo#wp;yE{`LXiQef~#Q3?B4{ult9G{RgL?Tsj3B z@VIzCc43@R`yjDhw+|BAb^ScCUDwa&Zf>D@V?W5}Ur`}_Bk!esDBB-o-A<4Wg*yf5 zY@(A1$Am!`S5*+CbJA>5weulC*5O>h|6KUr{?N;z(97zL@H3)w-7SLQt5`}tf2Q!v zEG3ULA2P5~%xI<)$PN?EZe2ABI)zegs>RY}!P4eI|3d#PT3-Gy%~mGQAP~8YexNbZ z{~T>z*Z!FqyqErX`DHb2{1W~fU;h=oT|9OR=E`$o8V8uhOdHH?4RifD&lP+I+frzU zA&y36Q^}4~(B*~B3Vn6$eeVVW&zIGur8VHPrZnvL5 zJTx{oq*~WsIeZ?=j(6i7>TS#iKq>fq{Tcp-4M688crI0G2-I2xaEysfcUh{E>c|B5 zON|11EUV3sf>wJYF`!I;Va>L#Ln=i@!omtcRU~Kx0By{Mcdbx z@Ls|_OiCF{Puk8n#MA?rIW8R~S!z;D4u{c}k6lJb)}@fx4nX4q=_=>wyF8Z}e&$2s zhdf8^nRu6Mzg4^{+c5@dd&~AKpf1rQuD`GtYH%BAmSRK5`e#MfRc{^92XPZT&NM=2 z>^7?f#~UusC6Fe`pwpDJoE$l5i`R#N`*JKa_~XYzk3TN9i{|aY?IIL@@l5bc_=nh_ zNP58Yfz&{@GzRK2QVr0(@o1n+Fv2h5h#>&Th@@so6iA?4EP*^=+Oo(vh3VDd!&SjJ zA|2cA-oJGB9)sCxr5fj8I z%rwxA5P!Zzr*I#i!Ualxt|Q+`M3NR5j~D_{n*l87UkrHtuVLtW$!j@Y+P89CI_^McoPskv4O$#_PxRC_VQi6on~^S*-0bwCj3U^~ zX(G)We`sUpdNlJ%n$Dtr`Sj^!`swNK@97Q%?5B^4j-#hfuZ92T(bGGJ{9?OqI%8}+ z2Bz_WI2tFm>+(IZU6=3FE^#yY933~CgB~nxBWfr8obV>n&m|^9-z#Q)TYgW+&8S`K z?Xq3R&E)$DpOfu6ZYJA_#MU-76Aha8mmz6hPkOJTxQsu3YdvCuDo2Klosc@@?2@DSiBsO69;(Yb$Cg| zvaku7Y!QRr-0|kp#D#QQ3de)p1#RP-t!E~$=dFs|Gdi8?X#AL$Y}av1IWMAvY=5^p z%zIC^6CGsx2Q^$#wiBIY`}B(GspE}vuwRJxOO!h0pge({WWGym z1qX>FD8Lm$69_))Q((E2DhB-9psXb8OE_a*S*cLmm8ErMb;X70sTPw`B1&{Ux7|lL zqXsr%<5(=Z0b_{YLT~X6b*9N}ROcyi76OF)h_;))?yYX@kQmg7ntN57pA=fpLcY1lg)46NJae8^ySJU$^Wux7JJ9_$Wlvm%=&_`fVOV6D(-thJ1 zlXYWiclc@Xu9l(o|G<1}!W6X(Il^=0^(TcPUhh{}XOZi#%f4#tr+#hyUzF=_Pk|4l zKqzK)q9{PW*a`$rp^3ekf=0HW8MdGeTTCngfd;skVpYi=wAfYNaHwoS7x6RCKb2SzkOv3iW2R`sb_&-dB!bW>- zb8{_z?1$&X>AAxtHQB-Gm!`wh!K~VnU{9Mk-PY677Ji_u2jqvmBE5|CBRez$(h8v# zKrLJ)4wFzeoiVHwWI56R$|?3roOrA)F;)z4ID!t(|K#xPw;z7!p`Ovvo`=*M=a!ex zg`X97j`jJWec)X<)ifO^mrhvlDolA;q3}XMLIQ&&V@t>4a^3 z=QveeIgwX}LLVGl>Nvmh!4Gy14t0G{z41tEsCVD3*MxDv*BQV&gUE$T2mfR#PNg&8 z&4f^Z-jg7P@#g+XnrNRo1^Y`WF3bjsMRr(~%Yw<8JQd~yiI5k+7UC1DM!V_IZMPjd zbX&Lg-1mtD___99#JOb=SUz`dIs6wx2MfOP+wFZK;2#_FhyS7vD5c8Sy%Y3LhmF`4 zXtoKJ&<*LABb-uO1(@v`YM+8^w z!^55TsW*T}Y(JV4{)*@b|FyVxw7X{n`w=#x>g4@+C#VqJkBRNN?zA5|F2)PFF8k}c z(>CnWEueD^WBf)Fp)?o4OQ7o8Fb%^Jb_ccy=_?V8zZCw5+-$ZrYDA5GyW0bY z<3?B}FOQi(G#`CiXe!{}-c)~L>yGw?KL27zXxQr=tn-{6+j$4j|Gur;y6Xp0%d%HT zy60M2_H?!O_qSGerIcr%m^g5nWD$><=l+Lrb5CrSI4yK&oJBOpKFtqc=!o+Jn9M4m zf{O&QsD_9iz-%@yf!|m>Y~lw<_C@7l#1F9X3A!JkPw6eH$f~c%PT~i6b7YKq1^!lg z1$NB84pk2u)L0KeAmf&q*Qc`PEl5IZU9Z~)egaLwPjU@(ej>S4jD1l~ySC7x3KO=4 zgk{#E+k!;ylEoY`B+8ZYib`iyW~J3u43R;YUR#cZ83tlQcaylhIN#wunr~n1fAO)Y z#f}35LtVWiBfSlOe3x%PP4(|@n|sc5a$jJnV|rU(@5s~(gWc=D28)mkgAE-950L+J zC=h`4MDh}x4)>2RuY$p~<%SQ=6oJ(8^Mq1dlvkEt=Ez7(wOdU_C0FEX5Tw`ZBrh9A z=;ZRj<0qH>-M{FupILn7?f#)*c(hI~ho4+LGd?nK z>C(UmM&YCV47ynqXP}huE3t^fsdLh(%T>*_FO4mU=-hlpy!?!MAHbXom}B%A86>4m zLSVxJ8f2`B{kWZzR-xflFZuf1~4wq_DVY0p%YzSv02_E1(H@S=?Y_K*;bM# zzKCp;bJBTP^2`mC7J>~-h)^QnlAgva;SjKaHAg3q7Itn*$ff@9u~1K6Z*S-p`>7>0 zbNy$;t@QWuNxYYz&$N7iX{pCrB)03eSz^1s7iBx??D9FfZ4?5=ChcX}uG<8O&(Uoo z*-kcr>@RI2U;#KIW(?buLW*(8gKcLH!dR!MbZjmAUo!f1X|V6*dpz^{U^s9?H@Bo^rtz> z=jb@1>`ye5?X1UY@0E6sq=|0#NLmr@DBE@1F$CO^VuH$*-IDc-B~%O+7vwL8!9&VH z#Na6a%R`@1Rua0At&=5`vJ&!pq}XBSl#6oxsMYM8C|MoZQBy}q%M6!JmC${?0Uq(U_HXVgDBaL1nbh<>l>d7r$6{lMEv~Z5Z?`RonqrAr55V zP7u`^&ZXn|rAnzIKVRZpF%OTv#buB|Plj9e%ydC(R4vABOM^zMs+x>FJNBL$gqvSA znnr=;{J=SylQZg6i(~%fR^1f?nVAC>>o>pyJSOJ7$KrS|v0dMfvYluq`|JBLu|M-9 z<_rsqG1feqb57v$#$Zh?61?#1)K@vjI`_fJn=dDK9}vZ&*gLUeJqHV*7Nqgnuv7Ay>#NoUj6gGiO$eg})#^7XFNw38{_K zq4jS}6J7YZJpU)+=#tp3>+iCi{3x=&?*A&l`1&!vd_oxWuwNuVk8NY%!ZE`m%mVo{t1azr?BB#SS<`s(U0ej9EGiMNV{@MGb-Fe^8Wo$Mp&hxi|i0Yenlu91*!_^)+fkW8clbPNK=Dg?^mEGuvqy7O{U?MlAL*CjW3 ztYh#g861~jlHNZ&y?urd-h~-w(ec04aH(rM21_%JhjgU(z)5>iv1|Hw@tBmV3_;I* zO>G9^>{jl&o-eS18U_W7AfoeH%)%1Uc?TwKP{jahS|MfucND?I-?S@EuP&{>=A_>n zC@Luy3Zl~7CGNc3EJsE+{}rsLB=>yX$kHZ^BVV6Zet=ot^vI?7_xq z@8jp%18T^0V*cPQdsXZ1mcgkj^}as);$+)IUEM8^$+HJ`hwnMq)-w_CO?vAaz07w! zXM+QL^hI8~TL_cDnkKfNqUJC7J1&2 zaqU+k?VpKjkB;}mChf`c0$NRBsbhU@2CpDTBN-z(*JDaImEL+jseVdENz*c!vRHmk73FnKHzNJ^M_5jbI=c}l|+n>KvY z1k=#GqFaUoNub2&E|Kp_{08E2l0b}dKpq9?WcLY3`!a71TWUB0qZrO z{As|R19hTKSg=^*R~^7@+a!%oD^@`B*3)(&(vZ0g^E(9xt8~!~OlG7as6tt`rR&nj z8(A5A&1|(?d{f`s8}M}cgwogJ8|WNpZ*6KsBvEBWc}X#a&XS0NN{w)$EfEpb))r^Y zL%E`mRe-pWXjKp%f%I8=Kk_MP((BI#rgshPtSm2c?-=iG88(>)8V7>kttv0C+7%qA z8?ahO&b_k=pMJpU*-_@Mu`i5o-#1><;3*&2*-~BIvUl{|N2?pE%Lk@gtE*eZ)P$$8 z(Gz~6(Q~xDzOJ434%3!p)3ch4Tjo7NF|h(?VzyGD-14Lyu~h(EG^Ybt7Z$4q;tjy% z!0rb`vPdcKO!yquNtf11gZkiZ$;}Z;Nl{KkZbe2KCog1+Y|Y)`adA|MrA?!hbX00Y zBnV`}AGC;22>$S${+Z?FQ(b{Tmv3Oe-q(RRfGc-;{l^zSzHof1cj(G+&xrpKKj9$c ze@fx=Yx9`nfj1R%j6H${%yYRJqLHiU!zMz^@r4Tq7ta|&hQUt4dpig5&vQT?A6Y(m zc+T53=$&(8S_Li8) z6R5pUK&fiF2o56O%2ABDGkz~tIAJKdo%I#dX~%!~BvvV^ZdTgJ!wKVv zg^-U3+y?ZCRM&X*l@|j1U&)Ah8{!**Y*V@wAX+O5_zsw)g*3j!$o3C99mk#w_=^f%f zFl~t?t8Lp@jwT?^JoG z-{0BQ*9ZPR#QfVkK07%l6O5wUfM*?MF6gUyHP3T&$zZ{yeTE^f;=;c}RMYjxPJ3jXa0eo^*8C zeumn?6Ee=D8PV&F%3TuiY*tJ##sT)jWnnN`kb+>fsucIZ=QgtlP*7N$2W&zM`)Jtk z6*E3FBkN$}R|y$~eO+JcaaUFpq`(Nt;39qmsXL_M)Rd1|-#$IC0K5 z)MH;db7pDzbogq2-&#h<)au#7s&n|KulW6a{lHF}yk{04Sv+%QamhQdb$eYPD^13P zsHr6bBO?P5dXi2o>2q46596oUGdVyshXR}Wod)2ViSav8U{O(FZfRcWmh@C4Q^D_q z;GzV-lWt1`PM3x>olWF+n&|bfW`t}f_bi`;->LW4eegT+z_;$?X~64Lf^lR0q|bLUbn(3@CpTVXnnQu$#s_h0d7@z%3`@26V3rXh?oKS49qsn{QpYGQI8N9}m3n zf_TUDnSt@??laTj+n7ft5To79^76!!IY_UAcSy?1a!i}fJh29bP?rdD1eYLQZ;`ZB z#OoCWDx+Nth8|)Zota{~?G5Bmc#M>icu`SymB(p|MEJpRh&-eiF^Q9%66NB~g^z!z zsRIQTrg~m26j5r5-x!(3EhI_r0B--H zulMjAl9pHeLnDFkV>C~CDfyesD!ha@Z{CEZ0-wTPVO+1_Ul#Lmab3BlSP?&a^%{rI z;yV~Aun3GDqJU_&M%WF;&h7Z7O~&=hQ1_lm1G*U(lV?=OAZ)~&%s%~TVS8#?T$&Eo zQ#`PI2V}GMj$71IO#j68lNxT6*nVP@_TzEw(K#I1q@CwR;{lc(owG-qvjt~{#H|wF ze^j%(WjkcEHjlHh{Yf@!?PsVR`)(Gt2;PJ2hbrYsX@NITNE0u9fY`ve2<@bYLuky4 zDK^Af!6OUPd=YY&k{zW$uS1|P(f;S)%`~MdPyvO9B_$*ibYgY}z?Y4G zry&|X3%QVp(c=IH2OI(#arn|n#OK$p+=30f_G0+A;eYw} zf5#)hlS{zA@eaHjncUci>;p$TI0=}J8+#h4JmyZh%oKN)rc?*2^trQ0Q80HDBqogy z;0m1m6wFM1aVOT0a!|-K-j*ZYI=1IaBHvy9`0R-I>Gb+nDm=7D`MKmf5Lx{Ghd^I? zpY${SK*=Hhi-V6sgUa#sP!K3zv~v>i=vs^wdgbKgvZE?1Y6l^}3U(2E&W6^%F0XoETYE94#W1^xp);`0S``tf{nC}BV)G;^a@3A+Zltxk_4%ZOvl zhim~NA|5=bv|3C^Ex-$l0)-$d*oh-_DV`FliT)v{`QuTQt}ItpRV5N_ zDRczJ1|vXu8_tZ#;n6##np)hIttrCN;`a9MDlaZ7-(Fr+T&}vusv2u6D{FW3hZn>j zIX!M?cwYaT#xKvXX5d>B*X0o=ho1%dC`$qy2l1sPKokY?cHCmgRakB9fD%#qf74L5&-!G;aF;1#svth+U$4!Ux1t;Urw>Ic-_Ha7jo{XHzf>6`(bqm-gc5aCWPmiOnoLS zU^(UwX@u$y6u<^`DzVGn0Ld39NtKSQDnug|=@Lpi>M2)DUrS1c==b{jIy(EwS|0D8 zIUutk#NM6(I`~zMP9K@OB|1lvrE)G38z$TF71$4rkQ;Hi5CeSn$Xtj)Woolr2wj!m z05R6DoI88);MsF4R6d}4c-VUO$lKP=oLPI@k+Z?Uz{7z-{sXk<_w%|{FpZ;jA;uNc z9<>YO+oN`2e0$WsdqritP{`+RunW08nO*n_;yB^k(Y_b83ty3Tq1GO?3+eq=BK@Ow zA+?hZBHtgi3(5X#jI>AXzxa7X?LTTK9Ya2c?LS4XUorfhdC(rY`D0Ynp%Np$+JL6e{c$Rphn0cUC;|-Zpu?u<{<_ucM_E_4*_CKo1 zoC^6IqMhu2HeyrL^J!1W_R}%#TViE(L~|m{TEPP^SJH=2XP? z<(!K0GNE{?%IeGOONs#Vo0U>gs-raIX^I$5MDwT-9dAB~v%aB%3Y*NTXspX;6Z(;8 zRz+@88AI$Rmf!E+SD39ORX7F%kI*i~AD}(uRD2)$#uQHSQpKZe4~$Yyg}bUC71B*h zseoim%$1-SMe`}j%M_)|Q|>7)B!wU*QG&&YOq9SxC{eyCQJDb@ctqC*9pIInqUW-mpLX zqpf|9X8p?E!0r@bvsPk@A^S*&Qfz=y zY=%`k(iy~(JeE@}Dmiq_DJlbPwKe4J>h!fV)pXQ$ILk_k3jw=1GO(Q0qB;$VSYBiu zk)z;OW8GVXj$oPH9xYoTik3#)UDt-DriQB93K5-D&{jP(_1(&{!YbpeeWJ6vtpL&Q zg>9u}m6c`XZue8~aF>@=+V_n8cKhx^loqHe+`avggYJg%LjRcjeX_y*TXzHhA(Tn_ z7t|Ewf>Nj#o%I#UktZGTzAQpVh&Ti?y zIQAsMMx?-Offv*XreMEZjI4|wD-wdVT%gT*ssO6V3**8=Xl7^`D~=}^RhGSu`Bnd73WrtBa!O=O@{M8OnYnaXi-3`~yWWJtA4 z1a|Kao$K*KfAacQ7n_6l8{54PTs|~!otbdg)mM+&a;=m7+opE} zO5C-T-PY{vcTd6s$GmyGJa3$*Jn!YC?W~LO{JcEBZauG~?ak+PVBQ7;=XDH{`W#o^ z>CNSJ#5_UwwRS6AHLaQ5t()a_ydeqoQcvGvw`*Q-&+E}MEjFjaZ{9h&DRqmq;!jCv ztn06{;34n|cu3No?SIf1HjA3C{s5jXRANd+*c-)uBOt?Q;B<~9(2moC#1hgubThnG zsjYNYIvZlrIkaS_oQ*jhOcV`cgln>Y!8_3IpXyv0>~8i?ZtL!9^bQSqJBHNUzWwb@ z)4rZinjz)Lw%+;n>22NJ+k%709UAFee;H%_6SBb|b5j(m86fn!M+7R%h-8#Ja=K(3 ztA<=7o8(XSPh7=?xoXeoT(G`!{i~P}p4EhTTh*^XHh2RRRAjgt2^Bzk$e)RWhP)RR zXtxY^kZUW2OnCx5aEUmnP8#T;yW%ADu`)dU(T|2+c)?k8Wyhcxn*Pv-ro&%E6kp8y zirBM8?}PH3VplDw>9`vAYOB?D0C|sHHUgPYyeB^wki7ga6-7sUaQ{VIfP~A(;03c6 z`1RMWVE|W#r++^FC=KD!@db=w$KV3(L7un42|fT;3_JE#H52#7F=ji;m@SPJxFXS} zO1YQns#z_iLU4=^RT6AM2^QnsxOG7RN^_ASW(~k=*&@||MYdOMi*{kN_1Pd%>*Kzm zpw0^7XjhVc(~4s+6Vt9zDq*(cznpAm3MOsRQ-rvZT4eeXand)Hu<@=cZtl<*;e#?e zMAl6kPaE^btcjK-!Zg9&z$7ZPvtg-DXW}*Y~_^C!0||U$+@$`>(Zj zkD1%S3uH6D2wpIVt1s&K{bqgX-EX!p*`CS!{>_FlkS(;aFXg!)W?!3!Zl@*ATeop# zJK1S+-qOaEc`kG6dlAPxHGVY}yj%=}L@}HMilRX0=JOsNvL)y`{RFfW;bU07Nk82i zu(`4_k!5Q}MjKW7fCPY2i9Anfzq$wtu5gEV`A1yibxmg%7f(M!YwzE>)h`xcuGY1O zUtBtMiqAldS)JpvaUp>h>JW?YUd%-c8GYL3Cs$ zxjR!4;fBr$cW`NTE=D8RU z=cw_+Da{U-?L-qfM`^DKSZRt{fjK7RzL0lHKWUQZzWnm@OBYhM7)|N+3yYsu-QfqE zeGW&TQ=DGEL1zxkRvc7rvU~%Tg&AfboLce=W1M;v|C>*Kxji)1X@DR#8MlVE8ZAOK z8@jhItn?cJI$KSH7{Oa}y$**rSKJl-*ZK|d*5ZDLqrW(O7g!DS;c+qVA@(gs4kfnh zaz(b2T#@~CxsuqQc@p!M7{43XQeyr~&*blw|I+&OXO7-#Ni!PKEVmy0%wHZoZ%H#5 zGpy%Dkw|$wrO;|EOnE%~>+nn0Qc3__r(DCkJ79CtTBa!#fih5tka%bgFeYg+kynlx zBb!~OzvTI1(qF`%?fl!NOV)IQA>DHOV)$p*{z)~4|7&*rmM!(!V%IvkFF-qfKGP0b zeGKgq+jZK>cA}l^uhXsrSQN$?X}_SplJxodK9|p-eJ-D)?{jYFeJk5}-$uqsye`{y zUJtQOMSj?g`7eCjy;1&4NVKDvz(Xkev^6B|m=1;S;rthVF4N`qIJzXZN6s)B2jiQv zzaHO0`YG~_QFr=YJVz)4_`MD~V4U9=J|ZmX8~Tk0p1CH{pP}+xvW^r#i+xgW<1s#@ z#j$Je0Ig*Et(w1Z0D1zw+XL?zBG^?keGb}E|4&l80qOE0%fRjM4d4osNF6Ednb0NL z11;&$C!tH?vIlncNeAP`Nuc~v4*tj0o}l$1@DG|F`-p&P3_NrBGNyC+@-x%FS`04! zYC8NM_$&O6)0hhAM3_@0bhrYADk77(b_7-Ld^0aS}>K~OR5DEs6*p&5e_zckcalnGjeb< zyH+CV@Q^;dj24WnJE#&KU%F5-G)fr1_@NuOXNWC%}bQ0l8HDs&~8 zEQ8lklB1MKP?ifi`(svc#sptvw9@eKme5e*ifR$R%=d&#fF{@{bdZowne=7@YLO4B zBfwqUko*DV9|Lx)Fq$pG2E0=`xN1`b%8?@%95yC`C{4Ci1j@pvr{SJd(go&&)F8&M z5~c>vV!}5N#-QPAP(4(t8r(CP8u!gW(}pjh1D)WO>rI);zMpWC@fC)P#|yu#7&E7*=*}q+~ZILw&z3 z*swFu-hF(bvEaK;)wCU(w^?s%-_h8(qx~>pTb+Z0mQT1_THK$gYHj^e%~sLA(-m{-=+7x6dglY`=CIG=NrIl0oM zF$EB{@D(oZ?c(pI*Z-w^4q6N7i1De%n2vJX$K|+h z{toiB3^G4UX(f{wao!XutHp}Qc!?z%GDpa6$Gkil^!B!lkMB9mYbxK#=hb_dh7VznVT_Tncwo)LvxRdBpL6>b9%J;p zqVK8Pf0D;8Yh@Tsh~~uTk*Y)GXV(&@K{yDek&}&>#e(dzoU+*I#GDWu0HRMP3o=+% z&A>UaG(H0bJ5!hW)SX3S? zD=aKSu!Xy!#^tJs23!2?-^h>)q7=L?xcmrvEnh*c$vm<@5|WAI%#i#ykxYDS`*f25 z$CTB)b#BO%N(M;d)c7hK5bp3@xm_tKUAZD7{L5TlYN{_+ycMR$V))~{uXrrHuO7wN zqS!}byDo2JJINKdFj*-U!^o+Q6Y@s_3%IQ_z&XjqvzA!l6&rBvK=(`L60}9 zs4Wkg58!tj`sEVNh!hYrj1QITcp0cH3MGN)=yaErv>jZr;gDgtvRpR@#U(`ILi_s= zfW2++c~9V*-~8sZ=n9@27z=iv3x>Z&SmOxZ=~6v#f}yr}X>jg3-hq`aqQAkvNToz$ zpa5}Z&^*Zuq6k4`RU;-B0fNYw47gYX$d9nAlJ=6NTHt8Qa#jMV273^s^+^b6bY6%xQZklHjFbT2hL%cRHToW!pD=2*Fz~qabv!E zbm@e;aomfSFM|acU!t)CV^HouBp0&pHELEs#;5|pGnDP1DrwADCrgH6=fwHPZq`WjEA ztGu+ZAj^@OmzI}2Qku4xu3ROhhxky5%T6MLCqBW=de>ePX z<;s!k8GONmoH%rFI#ZNGsP9Pu=HO77+yOsaQJ^gGQw~tn@|62YOE2KK@bc91lA`>) z?9B8uIZs`g)aRC!Y^_DTQwD>7I(AC`OP`7QyNy$c;a>yf%dI#Fql#Q9GbRe57{%Q& z(^;m|kEa6_1H2vIL7&kw4i=tKKI;+r#{8TtQjfJ;;B3$)1hO@gZ^o1}2ai7-I|&sW zo0MPm{nw?Sj*s#Q#OI-^9kw1hF7$sB-Wi@8|aS$nOKe=p3wc)O&i?7!= zP`zn4eZOj|HaNE+0|nbMTp8fmqC(Wb+=3CNJ1c3!3LkE%;_7|~AFx=G(?D%aFAuJ{ zn;g}D5>JW#thDR_kE=YlJqbmkzMNv^!RrN7&QhaBDKs2mJW1gyP;L19CzqYsqOuXA z5u$|an{C=D;N0*zXHL+QNMD0BgsCG}mGU(+Q96(_xR9->2h~N-zNkNl>p6%F?G)VR z8gFQ#8FM|@@CUjB!#D_~Q>X|6OWK~gPw2C8ItKQo<`a_b8+}5uUDp{P>olK`Y}a)L z*-n_SY?rzL%SQ4EK}uQ>MSRbEDR(!Rkd?8HxU0XK4d{!I8c_(NpI^UY=amxN;?XPEk9%qw4cBwhw! zckAH@=0bLm_=MMSXMstVCVa>@7EuqV;r&n(d9zfMM^(x78#4VTdd3gq{Gv1duE-fr z(?M!%;Dv$(jTqPW<3A{j&IXGbc00gcLQ*Gmw5O45=vom#Aq0n2DG1SV0|0+ujIc3gae zoOdf0*=93C;7OxUvQjlVE5N$M+8g(8q&GzRBb^*?uo{)4`qNpa+H4lSg^%vF>O?rK zK-uhzNH|A#AmQ4(6k`ft+s(ha7aZm+EEGy%V_{=mjk^lfs*rD%h0I|@nHv?jG6=X8 z6^$xovjR#%?T}JL$V!Jna_XNhbrWj#$AAk${5SmB`mM~nzE(CG}7=lU|Nhb^ft@!dbR7uq8k83x+AWEK8BN4q~5q34- zHw3p#dxtnTb9kR-%5_C*!*LlFc2Dy)T3w}WQ)97klxD-hVH*d2J*Ah)s%r; zjTBIPQB^)I{!;ugXk3KYp$8*%(TD@1#m;0sbHJOh*HIr0RnY)??2Q&jGb=l(fIOB* zrP9GBm>wqZ;Kr|^4wUva_|EXHXo<9{k}ADK8dO@`kZDheBspTK2*8V^b;lNX0zfG; z8#+2$Lax%nN<*uuuf^+cGT7U4J@p}9h4{-RkEgLDuP8UuUE5syw6D0_jqvP-+(MfB zQ{s!_W2iksxi`ol#Qa#YVO8X>aKXN<$-#;?qza6)#ef#|K z(rwdPIeUzQe6x5j#sTtRESK;*`EBK0Ly@Sucm?3@@V34u!G=>=uybkoiVqfKrTKZ93bI? zy@6DEB46rkl5++|O(WPfOG8+Eg2w>L72G__bs*D0OVpzWo(Z&ahTK>LfEP}p);4*- zv9pWl5*)g|fHZLcbJ-4?g)_jYJ}ZhOLi#y#d~}hI*<8@zsDveVek2&aqvv;DHHq@e z-zmL)L|k1z^PS6=zoYhruT09=;V>mx7=( znD2S(!qLWw<|7Mt-n;)C{&7~k1z7S|L_hz~eN%hFe@Z_XLp$`{T*v~xm(VR^TH$Vo zzwu(kcZ3*MoMKeTpO2q18QDj!@Nx>1CTk`?H**Uh2#d+c_YzvT_JrHhmdk!5Vn#hG zQI-hDk-|@x9u8#T_ydV(tIs8F!uUw~kiv={4F)ZLO1Mo1(!`fX<%tTcD zz|2<{Q>k^IA+!hOEWdkUkHMsV51GM-D|e0+sHcEvKU%eOEPwq$tS7&NdHkEao_bAc zioc+`nUHe4?)*FT)9&-)n${R&_RZgk-(rjz%H;S}61#b%AxhrF1Jxx)5HVB^tBZVs z(Gx%`Yr|C-H9!%p>p*U`*`nRxM10F!V0L9^)_cHB7eaq!QFrb(?b>lbJdbOq?hBv4 z=T73wZFm>%)53W7@^~Q&=>9HYjCH^mwC|Yid84mD!Ng&DH@)BuJ41yl%+FU8qzISiqauG{0R{`?#F6Jn zWdVltB`Sl|L6Io5({NguT{#{Hxf!&m6l}_yy$imM#okcgLWggmcX#L5*gNslSrBaB z-~ZgeqIZ1UyEyP%|Ni#ihsBS^0?+l0j`lql7^8Kn#yUw}C#+m3@Zbn|e!$aI2Y}?O zs;m;IPze1+6SQ1t-z9k!Od7$xNrH)wU0ygE>hHqcbr}bCiM;TSo7=@(C~}N)f6r3A zW7sCR>k+BFs%e*eGTCCxn?(w7U*r~FkidlCAC`;|;^K$l+e6+bN&4D zJmy}^f$nEOO^85+CT{7$CWK__A~*J#Z3gX$c(Pxey>0vXU3lPG^)mlps2FQ6#yZ1v zK%Eg>F`!y@(Vdxe>JT0uGur4~>NZPB?09M1IjSMtwe!Nrg`NF8YW*c=Z!kUH&igQh z?3SJ6z6(xT)2^&>U%&aNksd}UbUx3ZpN$BCh z6`yZq@Zop$jE(iY3&QqU++sl&R*d!bj*;(Odmr5^6RQ(}&YG|!-8`ZbX}8LxMRX#Z zIZ57o@8y@)s^3@rqnWl{3ppDv*VaYgU1@;cG`qbOxr(l0rBzN_)4> z&278<7NZ#1CKh@ttE($}O~Ed|nje04cI3jytomTM?Z_gsNiI~%FwYBbEA}cDeKf7!6!ssrs68^||UDXA5EuS^oeLpyR{*d=B)eps41gTH4cY12u zCbu&~3(^AuSqqXWrsRkrPZLFG+oTqx*SL0J-KCc|ri>~IaGs4^VgJ4F4fXU;{)4=7 z?bH%oPjF)T9rV}IDGU)jIK{q58YAVgX<-$hDeMY`TGJheXCzz$AX^b;BLWUA776qeYSq1=%_|C4xhPN* z>0>Z3x0=W?#Ts!@Oet{mg`FF{5|OU0&~l;>Xl2(J1LBqiu4Yj}5haUKufPE-_ub=j z3o|ndbK_I<^HVjAjWzhOADn$G{AsZD*WS5%Wo38xuGxdzyS?JA-tKO1_%3fZ=GKYz zptG3dCL)5Mf*_IsnW*3rx}wY^FXY0Bq$zGNe{frKM&6b@X$E<$q#0*vH&?;>fl)!% zr-FO4b2}dyoak5{_?OVx`)|MJ9^cks5y(Ee9lAJFWV)2=9$_5q>4s{F{fu~WTHjL2 z$K!9%82{>5d9DMPYboBF zr64W~yv}+iHYH5f6{!>wL?#zc-a!t9lNK*g;Uw~k_vuP!Q!q~L#JoXA;1Yb7z0_&( z3&N3*e`qw4@u8bML{Ckp%_40>tD!^0d`f>hcIz(>>?u1!L8P0eCw|^t9UE0LdU)scaf3_j{EI1JDkDb8z zM3{dXq<@*x73cs>VD{oBMa>mbhFwU(f~2g(%{)6ZBOM1U0y(Vl2Q1{LK|js1NYSEm z7B>W=@FRYn`!|12kN@@_|G)LyPc4fvY7T#9>9qLN*}%|HAlx<_xchFDcE#M;mf<Lq#_1HAq&w1y?_T_b1qRz?EXC@ChYRbP1@0&<+%-5SmlWDByyxnKH@A ztzE%E*bN&L;+OZ|KXc#1>tA^IzFqIU|6>#SbsXYtVm@*$2aSucI%#u#j?Yv6-S8yh zyBJx(-b6d-IEOug+k9}*WjdX#u##wuU}$Iwa7j@e1t>ML##KrR?56Y-WLeO?n2w4} zsxu%*2-6Xg7W$U>9NiT1)nG6TP+#5^@=ftP8B@~c(XdR&0@Oi^i6=w&Ak%SI(W1#H#^2=zmYZBGUIEDv)d6-#&eI@?y3zB)@^slgYdKP z-@XP8D^qUMJo(hYq(EPWXhKx#8ZM}`;!qST;59Sde9%KWb6unxq8p870}jeack~8` zjRS0vzqkYDx5)^7HJ|(@xZ%TMEHjniod!hTB z1jA3*tg6_Kb;h}FzzGXG5J3%*?Ne5gSD9aln>TT7pIH|fa44dV8c+c)TSk^jBC5S| z=GLQ6e(KTGo&LquGb`PLgWdSCpIuqM{T{nxZ$|nbH#GasE-kN|4UYMj=~__y0aAh* z^I*m_pkEpAAvz@6RFNJs=JW61>Z||A{K&uvh5>fRn`>^B;X6TMLW1J#wT>hOj7 zY7e!zD$fS4(l$C0hSW5?t8`k;lEei(v5=s$ls^9Tonowc<>hWB0u*M&-+1G zV8FU5>?8r}F5V35j(o5=*4;5nLsjo_G-BH`OBA|7x(?Q z(YY4TfNLZ(U%tTmQr1jH?52X>K@*PM1g$|K=tUHBx+&IO{DxRJZWBf|Hobx)G!{$7 zTh8o0H)oh5e`aFZ^owh2P;nynE#4tKh?i-v{G%MltRR!nl!D zRa77`Zi*so5cRrs7byo4gK_Jgt9Z5*_R!H#SBtm3x3~Smxh5Y1>g&B~NIg8;6j~$h zJ+Ww+9dp$-cxsxaYAane6(s|c+qY^AJPwQ&bHSeCII|3;GT=-nPlm*|!8%cVn-)07 zA)O?IZ|m^3&oIB+VEy4q`U7#g->#c45r2a~^P2kkk}xDH){~#lJWcfj>AoMF`*<%< z9_W9R=L?+~C$rRR8UG=L7L2BW59l4Kxd|WvdpEjbtD##yu=;0TSo_#9)&1g&;a9@n z|K%_7PJTAOvw+`ui+YIPS#a}JZLYw%D5xZYlS5=+Jjrl!eDZtYv&i!Q#rtPt);LA+ z1S;`+aw^9mlSE|>D0k(|hyFDb`d1?G`i)=ylGaqKHwd$xlbEUs9E zfW|wtICZ=OdAR!ToCks5BfM!)nlYAY=sOax9}M(^1*GXNI4rezG78QsMUz0-O{J!( z*Tv5pRff-DtK^ibjGkLIqUU`J?YOi#6hy9i8OT)QSZ z7Vyu-4)xma#0N%t-b*zt-rF;R;bT5hZ$kcqIKec7;VV#S06qwkqL81go}JM`ty#8o*8L2QCIabw}>0kK*Te zavXlH-BWLKLX0qsoFk8tsBeEQ^pX9y9Gm~hN7{Y9_K&FUzuvzyxG>$+(9i@j;$4uW ztQRE46M)8qo#TaZ*m)GN*Ww8P3~qSmpImy@F2uKVtin-8fLD)I0BhV#N4-cb#<>0K+Jt~gd%h3uNQ*B1rkczC|{1}Z313C z2pSV!{^9Yf9-JEGAV(Zt4qk%Lj={?%u1;`5LO(oG??!O1{%C2xY@z6pOGIl;WUu_z7XZA1=qq0TJ|9dOVeW)2zQ;9DHVWYwgxq z@tyFG_Z{0&Us(%)ooV35+7k_uV(eIlH-WL!X%aiK7>wONbN^T$4e6fypY5xxsj2K! z-OoJp%Z`&2xCX_%0s(aI*0wG!J59|u9n$D7-JA) zq`HgJ2k-`w5HVUJAPPj(u{}r?3pm##>I2XNA-oa1Jrdc3lutL0Z_p>l=c6mQ&Msoy z_CvERUBi7f!BYQlXUncbJY=FP);C%+U%MwDNqeOBK57*vtKCkELa`FXWSimcAp|Z% zmxB>$K*R@}2yl2A&6MFvh^UU`6)VM*f(Zix{+QA_2OJ3C6wq9+k{l@^?ZQ0?pZZkH zO$i~jn{Ly2ifc*#wW#kIK+)vyQXz4P$2WuSuZzbQzb+onoXvPVugiZ1j|Z$vij>T$ z*k}viKRUBKga41>)}(QFU7fqCw$?sBHS;Ug{S(IMUz(a9>1ce3(#-J(&8r_Yro0fs zxi-h*A(1pJJ_Al!RQbux$i-KHJ-Zo;r&3-R6seLfDDyb?35xH3SNF)V`5%SOUOMu? z11&v0|CGMBFF3z`;~)M(ls?r`U)KVD1_qD!-%u)*p#ZLmAqXC!lnAJ_QbZ4mY5)qM z>UB{V-nK;5$V$1STBYLRm8RO*6mY~sE8?LbBtCKVA;^AP<=@IRvT>!-Cq5zTWM4RR zZDy!Xr0Gi@<%1tn!G9U&fl6zeVe-YBV)DGDIQk7d_y{n0*yx}B^iQULJpKOZpO8F^ z>OZfK$wNzg9Za5$7%E=ib>XhR`Oxx})u-;82)0~VVN>R}|M2`}Tpu8mebBW~nFjhx zax6aXEgP_S61!}P<25<^;DHAg|0C7?!yl5>{qvt=kM}~?h0cYX+1q$-+V8J}!}Gxl zeHY#1aCod`EdEAYm~i-~7vGv{HyCUwx6eNf9DaNp8q9kts4`N8xOM%;_!_hr%ySI$ zB;9W_96olz;&J!{rIko3-lr?B4Z(=&N>j$G@GgpvqYn18VessVhO^q zTtL!qxW@~48igFwLC&!^8Wc}r9%sA^xd9e^{`@lT@e*@ed$fDJa5FFN@lu=RMP7^j z`bA#hYuZI#SaZfssNRo?yi6TaK|A9b+@2fPE^T<~pBL9I>%OC15EXT7M#titH^<_g zZyt+}%Fbk1Joy<8oIZWy*Zn=+fjfHoe;u%&UV7j1$K`8N5C8AsfX4$Q zo^2$yiFl5l=$JA0^mD)Fe%=TxN#ltw}U|TG&@uj4AADtjN<()+!*fX0sQ$G;#{>Md>vpv zO%P*<+T@>1u*vaz#3oncaCnNQz-J7>zcIG{#=yEy%Ye}oKiu@lffW*1-*B1fBCa^q z!S%y)?cKw;(sZcHJ9n7Xsi+-JzD+tW6JnzA`z6pF{$l8{U*|QJc01KeNLAbc7qVgq z4^otB!k)wdGJ$I-98ybcG+F^+XSHPsgOe^_F`HYzbo&>>-#;vJR?{*~#w_xqh+l*c zh>x}J%gqhpLE2i!#t@UvXJG&96Z#mVv6>i(7OM#kD1z?FHxd%sZj3tA8nKDVBS5xK zlQLCYOabo2WyQEsF*iHCD5FR^)ofOIZ(Q_d_V}O#Tm+f}L3WxhWk0>Fm9#Ivrp2tB zSPoxbKDjgYCid_&)h@uP5f>u@83nY6XpUG}&|Rl#s)CR;rX}D^jgAZ`I*e9W!XP8v zVAIlU#tk+g3d%K6 zp%md3$5rw}uwMepX*8PP@iCcdOjBA8UWyfw95LB|@#jR7NNg-`v$QLK&jhG9|IF5rzA0$KxkDy07O$OIiugG6McWycSBkwRw{owJjE>S`bv+#>uW5+>fI=g87-6V$~OP{z|L-fA+ zF0_*@a4S`^J^Wwwc9H?`|2{!2SieA|9r0K(?HreN^D1mxg=mHTf6x67aR0aC-xSM1 z?T>N$r?~wp{tenf$;#(Le?`6Xi=+9n$yqBLZV?^TG@f^&Buwog*2yE7Z9123u zE!e^4i}=)HKzQhyVhR+eCg; zxc0fCw~W81zyw^!=x`_sGDeVphG?IR%*;%_WKqT{QNR?1@L-5lgv=hpn3~9o{`H~7 z`S7!$YcwF~O8+{BhwBsAm#zXcL9GSY!CAnsR-fDwd)G6{!-AtJw5CPV#|&nbWjRu0HlZA^2b7dUnL&_Q|Q zL(DbLa06@CJg*^}Prv1P826Wu@+R-FC6?-*M%&9sT_~CMaukTTl3}`gag7>-H9E3k1s3C_Pwnz}2ze7z4+AkW7RksWez` ztcjI#GuF6*pCHb%y2dn<{#1&ItfXp`smM76D+)*}#K;mzOx63UWpp$^9MaOwN=p|DD=x*Vbxo&gCo=)+O@bgRW7(2h*HB{v4_l<0|pE0M5fUBsO^wLwJa>VhVsA@V=%{7y zc<%&a0s0%aZl4?|si<*P)=o9mcp7S5W6-WJpS7E#q>~`{Fh7u!39W97nh7qiR;C=K zDi+k_p+XJX%@1&!gR1Z#=49jA2P)hkelXH#uej^b^75_sgg+<#{Bxn{cl=>fYtd7+ zV_VV-Q`{xXV?*znKQ$ftMDRq@WOY?quiL{ip8c+L@0pqJ3e)vDic#DoJ}L(#`EHvicLkz=7$sQCjf` zmcc^#bP{1M*msDXAt_3^APrRFyjc5)8u2+FQBC^C(i?<2aJWa#<*-+c$Cp5#L%Y zU~0F{({jJDgryB9w)%#LeGfk{ICJ*F?WMIfBX#Na;`rM}zyL7m%c>pjSWZpq z%VigPX)(`lpN{d6wA9@}%lt4cU{&7A@TKLGhv&RqL;kl{b*H;&ZD|awr?7vK?mq(3 zNuFeulyyuN1$7cwGs#8a7kSMTUEQr@i)@P&N?Ljv#qVqBX$f&>HGxI1cQGIwf7;rO zzqT?7>7D8<8VU6>g8TUu;LG6{^M9iIM__|CVSMUU|1B-+&^;9VKFN_&#*R#n48>TqUw zC?g|9AdqANh`@GmrS@v7{GlKa0q^E>o7Gx(Re2e{RGQ~3bD$PGGzEqYDFrnL2a3f^ zMo~=W9>fSI4&Ch8*H+=L&MgsQq%+vNue~kQYwg@R=rv!v21V!L_T62UMN^Oa!`)-z zd#!Ul480U0(6Zrg=dD#I!vdBGC#YfTt%0pK=;1K6 z$=oLEjmEptc!TXWf9o!P-&CJ}*RAu{t{q!I2)l%?1`5hh_M?2Nytu^aEFPX3?r9kSJ`%9c@A&+lGxkMOZ{yc$nwx9B z^de|$N1ZwXfT_vQn*s2o$yv_CtP(VrXBZLa=Y35 zXZV!*GY@eb3-J)1QySY&_$;++_-v#MB;mJ%?$_tnkajo4V! z7l#e>I^ZN)e~Ohw_q~DAf_&K7ND-%dEGhUf)o#o)A&v{7vMB_eplFeSf>_w)me-ct z{CN53!4rMSEf6XLwtnh$%nAR>2Bk#|jbF{e2@_CkcH7;ls7PnOTWYQ-3HGKG zR}@zv#T6O8*?7*thUb_r{@xFwtAhUT2lW47EL<9kyfPhJ>RpuAUfJrvqHGBa!rye_5D*=Yu*bvIFQ-c_^ zKuU~Ji%EknOPGzSWI*1HvA$Waj0Hw=+1su_w?wgri&k>}YNv(8jv+$y*`(+zED%bK z+vO}RDX1^3&(4&WYbp66KP5&CvQiTjd7Aky1tRvA%Y{Nd+1I;=J(XwF?lYC1p`I4M z)#5KLac*~(^!0tU&%S&73yp)0>1oGc;}^zvceS>5P1ILE{d9Hxu9tV=|4jFTm`H5B z-a_CePPG<3d*o$jLp|cSNUX#XffTr}1TFIZcNDHDbh_yNcazs3E%e;HtlV4_ZuS8v z$q}~u9p2K??wwnvQfvl8O3K;!FClpDlP%L7-tcdu-bZWabZzV5m^)gmB)K-pzq;(51yi*(B$U({m;F3 zGD%aw_kD%QEO$HSfBxscFZNYN#DQj-E}eebms1lT-tGUAy)mqhHN24@rKTtHmqp#? z%5{6?jM_1*KWYj9b`j{7WNmhm{BEZzACwL!>I`BkDUT1FO(CzCXOW=a3SV$sW`zBV zax=IIJmfP5>Jd>|uQ(KpRrlU=?=5vN*0C)ex3pjNy`Ic$QM6@}QR$_Z!s1o(Fe{Js zXITcJQQ(PWSEjtYY{aS-!D<%;L0gj;@)hT~u`yt;)zu+LUMCtQ>Td@CQ`a?8?r?Q8 zNJsGg)BB}|0p^&0KD zWQc?%B4mg72K|Cn%v^l&%udFimo)zfu5-_1Enp3B5#wcf-i#GWuvGgGcGfs ziMh4_2>o6G_OMomhW};XrI+q|2#_AmuW&dj@`s--E-ft_8UhTM-oRHul%tJDn;`NrflflXJd2?+bU>NRPhpaU{Bl2d{LhU^(A zHLuhSf;9^Dg91Q|arTJ(5;MtvXHoe;90}WD`PX6ju`tGfyTe<dhClv z-!iueZCmKph0jywP=#~01dM)-oNW}L8bP6tAQ5+Q=(Upot?@Q8L&*Xv#M)Z}MA!ydlj zhIis|JR%&PKdj+;x74TLQd2T{ZwJ>xJ zgDYH7EO>G}tq!*o7-Co#yKiCk;>fP?8vp0n&ATslDQkf}EFKSs<+5EzlJ0>j_8XeB z#P@I?V4n%UfK%v6BpgPO8J&pb1R|-xCnM+-)&vBVFTgf*d{NWnh?1g;gqB5N=6eRv zchno<{L~geG7g_;{eAuCzjN~FYFdP3_pRkvg|D!6Z^^mTI)E<&y9MF-v~P)blP{6t z62PqqX%%v==*VQiK=Doi(SI<(^1LSl%F@!BE4y^(305cXp@p6w;b6x+6Tu0G5%wq0;=>LMWY?u$xtb4xDzMK=I@V9S;R^0$G|@^_7_$tb^E zv1Mxn!{e}B&G|b?GJc^#7)bP11WJkv3vyj1Nrw(MDBTLE3gQ*C^CYI5h1m|9bb_pR zWG|D1wN3|>Z*dekizs;~;5A~D`Ve`QG#*bmRPGR$S5}1f_x3vSh~3Xz+|zOX3ul1* z)K?^p?aNOO?{$Cs1r6+BaRh{ec)yznNE6;^UV2VLTCrcVU`fmu%7maWk{Cv$h6~IG zC8rJ8U;?%l_Ua5miBRFeAkLz76wJ~f=#);01}ylB6$tvh9xmO2s^E56_FRvJqqMRN zi!=oi8ea)L@W{pk_#2QR@Rra~$KfC(nURDZh7KJX*XgB1rkkCc=-w9i49d;nm36w9Dgk#vF zFk>I3Z8!}w4Zey*61D1k6b4PNjdu@K3zuMskj@pW0ZNx}yQVW&@xsu;IZ|JYinOVm z9WxlZBGY57y|vMEI|eU1cgw!W#-UiEE_$GA^%VzqA8Bl>KewsVS6bWB*|@*CD^yk; zdwgi)>V!Yxs4bdl-**144ZWkCO{Fc4+TsiPXD&LIeZ6+vU6;SEIy&Uc+1k-M<*vye ziIlfnEKr$f-WrW0xgQouJ@&WaQsR=nQoRU8#}T)I0wJoHZzM5aWk0k;19u=n5J79Cnm1%gDR`n={7V^QNk z3nz_pVq0t@?xb)|O{hlD0Y?_9B|rfOK1m|hUpjtQ?IXYZ(x)C`Tl(d1^sydx zvwWKA<>%#%R9sB~DV`0}Adk(+#wWUDun@7$(Pjef4v@m;PqNZ*W)9$>mz82w05Cz)V!7ST_v-cCA4y33;brKjj;-Fj0H_oF*4JRnJcp2mDNkg#DF)O1OA)QK}~!-c>!BDyWwU*}^F~K=CPQi6x*P z1_NX%P)bNF68R4xDY$Y&jc`~0acpYq*loRSErU`-+B0+Tq7$~{Rq>(TV8!EoYj3k# z?QL=M>-zbqR9=^{7|bNWto zHz|W7B79B(O?JdzV8Yfh!iLK>RV&c!$z5xB888@esQ41#KouHR zI16Crj;EHi@=F|QY2}w(h|y8~(RPLs4V?akNUs2&Q=N09iaJyig%TP8Pd~X5*n!w; z+ldpl)v=Myo8@oC<8Nl67O3x7|$P*dtrA<+|+6Biqhj!|u%OiQXLTru8I# zP5R^V^$hsStY)mIM_bRuAI`d-T)0iGr*xL}RL{bCN@T)&zx5=c5^}M+o~l2HqC`kY zS5V)_v7QSD0E;3cyPR)e8AsG5y&@j}8rHFsuV`;knR)-_`?l<_i7?%KFd3=gi@9O& zzM&1GD3^$0E_>8DP<`Jvz^5PZ$-!E>Aa{Bdi-F%;l!wB(zAVZh0R4mjtdmIpfr(EK z6Q2Ri{UFp}O-b@hSqvbo=3`T9t-VnnOtToM>+15j$hbu5Y>3|=c^`EU2rvm#8U;OBo1J!lGmw}xNLfTSK{=&LpaPxlF^jx;}*}7;?uDqRnHn&?@ za^Po|N{5}GhKPf6^SWW$dXV9djMT)_+t$D~goYGoJZduWR&2R0Qsc;d~WEARq(- zz66~-jt@3W9>+(7g-!z1#@fK0Md*hqu6#D)=n4x2AyV!wD+m+@u$l850e`KTTBRZI ztS$!JBh5isJp9CsaH{*3lm|Ci6STEEcV2JS*@j}HTM(i+WIM5G_)ubCAmOsvei)DQ z#XA*dzbvofcv)4xX8tFuCtlzfsgp}|ca*FZv*Z~2uCu@TzUn^B_F%)l&EeV_RP0Zf+y)KwRs5FbUM>imV#@vTB@aX@j6QBU-9gTL_ld5MM7YnC(l!caCfphq_>R9 z1|4Mz6Xlwz5+SDqm`REaO2J(pYb&os2Tr`)g@tase*d-GSmZAG@0j|f5x+k|FJGPm z@nB9z-uyPgY*`HBP2rKN*(2)w{Rl+3`Wt~FkOr;6M0Bye#v&Xj`dpnEcA^pOkd4N6 z>i`DYTvA2PQijAxec&gaQzzxYpCaz#3Q&5&@&Ih%V!QZ&d zP8NH)dLjVXN;?tV#Lsdb>Q2KxQ4x)l z`+X(F7<`f47B*Jr)P*ias9HXB+hn-ZV$ zM?U6{u>N>i(NF6N-LC+Pg5ADy+DrXy@>&u8sIa^dVLc7y^2RW`%vWoa{p>PNl^KO2 zu(LFLA~{~6dmrJP;+q=nD@<52VeBr1w+Dt36@Ueb1&BemqP`mGl&~=2)pj5_0;!f9 zYD-U=NWg5gBE?i|X?-w?85}OJNGF(7UV{_NMH%D$?1o(*%xVh6o7Fk02}Xx)v}38jS3G`Wg3bpOovUtUtE-p z>az-LiU1q@k_}(eGT3N)lwiZd{oWfkT&W^iX2r8tgpDKMBiQJ%Z z80kpG=%%I=no`imhY(k?5gvo51cE4+>A(~y1;PkH)M#uWwiwmU0zH!`E{ViGHMnB= z*%JxqWh4#YY56E>pop%NnM5i?3N=#Aq!lkz<I25%I&{HjoN z{}!K*YER3LbB4dD?~%dA)BV)O($uWaf4U&+VCNBBxAItAzqSyMO*JB`?ej9Mgu3JwZ-m=hQ`X{p(;;7Rm*h^ z@ycLPBqM4aizT-VcU3o52J-@@kbO)0$mVsdJKAZ9B7D8HE;FOMx@IsdWQqBU8?wy3 zw2l&wmCF&o2kRhd6a9%E7Xl+8g-QZk9;O*(4W|(A$9vQgi8W)-bH)lK7fwjUHK(#z zSS`uYp|V)D<^VP?Ln#CYG22|$*4cK|RVTjpz3<9ck$Ai`(atUl-+nu(6k_n;{HIa3 z>Z9HYj$>5ggJML_Vmn1IFY?0t!?x4P-A7LN%g3f7hhOEkQ&g%kWKXZd+n0A88DZa+ zU#uET;W))RuWUQTI1hPwmU`9W&3c&{g-vz*omx?Pc^QxSOy!|1@o_P z(KBT`4RX1=hT@}YqBc;QC8kq_4W^q;LqU?8agoPfke_ZkMa@8@V)6ij$CzqJfugb;A_cFH|um^;S*a;@4S3mZ2~EaQk>WOSUHsC=e}X>Ma=m z`MDlfwhiU8Psr!xmbsj^jBGo*ZvK_Ka=J=#d-CqWq`)Mn3VU*MdkR}QOcfg{)uOUe zMCq9}RDkupZK$Y53g06{#}Hy`n6LG#&wcgi)s0_z>8qFD$ZCeY=1U zxTYd;juLS+THWp_dE?GoZ>f_1CvSbzDZS17x$aMyoQ_lB+GwwsBY%-?p1-}R^PS9! zJT@zT6K!5N3=}(RkjIX|C!pF<36Hsji~_P$11hx{UW}E1^^{wY7pr|9wjqC(@1fUV2rh#BO8LfcOY!Qen&i|0p^EmAYP>2bwpp3r z_ZEjs!fuzf$W{dFO*YHcj2&ut1k{XWRnTK;ni;F0(3xFFns)TW2dPWMV7yRi_hOt`jceAlvqU@o%8$3N`3q_{78e&50Y!L!Al~!CEWis6V?ILG$kfc5)htnp zS>wPVhb6N6C}xfLfw2*`)Oy$C4}bW@i|-l}TW_4Xc>cmMfF1XQD^9e+x0OejM**~S zN9;K!9iw$o-C0!cTy2SrUy&)2>&*!n$m!jvL`0FoiEJI zbm5L&M_T%Rz*etgy5>!h_`qPtjzbr0Z#cB-Idu%MM^Sz=R--48ATf*E@bL#e&ttHX zVCl)X#tqD>b0}q1gf^RVMZuyP3CQn|=z9M&qzA94~D7UF1FCLz0!a;HkFcDEY z0t&866jirk;0I?g=vT$3jK*_Xcg^%EQ+G4BY8z%jb;iA|KVrvGfc5aM1KNx$58cMsSWAWL6uC;5s@G`f}_SNi)v1<88qCE#4 zv~Xr}LtozpdU1RO%7~P&Z4$;HVL_-u2spKo$y2cQ0zz6!Gi>G&oY)P8iTregUUEz+ zRcjJVu>0}N0`Ch&&ag+BPJUyO{Zy94PaJ+~{rpF8TS@2!e5l`8G3+0WLSLdg7e#j< z7J*hE8bjncj$E|wfWA*ctBR%3NNEWZYO5oS(Z+zUq`b7;mMs*sVwHTPr3%sp;yXxf zEk;IQ4Qw#*r#`cazP)Xu-7URM)18TJO}#DMqiwVO&Aq+NvA(`oBAHC!WyZn2oycLD zXl>b8Tf4EPbpq|icJ_6mh|7cBjg8&(l0QEjZyz3Rk0V$Dpc4}KlZQDH4j_IH5C+m? z+3~Xk8y6fF)4&8J+5>?wZfH#k4&Vw`&~GFcsRPUuA|1^uH6e+T?K@K*PpeBHV2j+KPs3Y|BP)-TmN+0oeSS1Mp#s6w*f(EXNsw@k&zk_H#r_C z!PH%nSK@LgG&|)%PDP>^Bs&X|eUKn*XSwhsYn1varnmIg#s`nb2j$b2-lc?Ebj$zA z;+2hS=HCIycFIpyHWCb(fU2{i12FUoROkQwsHs=*7W#6c3N@wJA&S{jAm!^o>rw?F zC{q*0^oi(D<<4;Rs@B$3lVZ#E&hDMfLy6tdUbgL6!^Un$)V``bK0F*xY>UOVC3-I? zs&K4sEFZ(VB7Z`(V@!p>UBqz1LPO#cto_*A$DsEr<)!l|*FM>@9dKfHV9$l;mx{{D8n%$tV-HG>@w*R>qlQMBXG zp&jz$I}Wi!gB?XQ0sJAn(CEkOd<|lRyS2qG<9p$5Gy>mgEE)^=&@mGrS8DcW z!h6Co;%8+Pk?m#@@^dI`35@(0xHy>GQ3Q#=Lde%CotvlpNvKmz{!VmS=c`o4%9gJZ z-xx+8=Ww7dO*}b~MK&KK#6p3b-quh{)+A?iS*sd1@BA=o+?Ch%#|Ps5;SzS;*kAqS z>_a;Srq4&7!zULGv*%etLSUyL%+CuM4+-MK^YafjMll^6ziHuO_8ZoQ;~;;^ah87k z1|0t}zh1ZE@vCwF=lJ#d6^~zs>mT9A4J#hMmXD8~Z(Q;C6*&F^zuu%BUo!q1aew9c z8QSqBSL!u`!S{ui3R^zi&&QuDtW*FVeWXVI=-a{o`F7v%Ho2MYY;k16nz zKbE@wCLqOc*y9TPl;fx{vv~Xl9RD%D9`IY?_|>@obNqV1Z-wL6;rd7Talmhd z(enYn6^>tl<1g^*0YBxqm_EK6aew9cfZqz&Uy18~#>Wr%DaWPs>#xT3&+_Ad-wOBt zBy+Ln#a|W4RIcRGyb{#&uWXx}WiJnw4YH;&oT! zx@Y+@%hG#-)_;;+#GaRW5K&V|nVp(aHkM{8UZ;eZrWb!Qyl;==!O6)%dX?ALm*d4Q z+A?_A;O5QvvnA17d09nsbH!zq%?d5M2^~y+BXvNNTB>U$({!!1q=;&|mL(sxlv29Z zO((8BwCx}}+QoXu2M6TKx_}D84Y=ozF|UHN>sn)EKv8sxwY-8161?0#=vtNn+sv{MlaeG0m@^(?pN;w}2zx{bk5PrWs(gSrL){_?+Q=Cas~DI> z0ikeRUAX+?`P1 zNv>A`^H$Bx50!Ej4I@+ zTl~evfdJa3KUePYl+%mmC1Rf)WzS+>If|xo7F|jqYiq?33gs@ybCftsa7B*aZ?xng z$5^cwsAwx>TS;H%KOWVYgTsDi3f9+@BL$ot{ejIj+R?jyeXl3(`^rn& zQiHGD#$^JMd0ZM#Um_EnfK2fs=lRg=R*;D<$ML85aX3>~IDQ-FnIse7{wwgz6X2OI za-N9$ufP*8$ML85aom4}yHa2W`*D4f7Pc#-pm zAW6{6bTo`hP4VNp8tFWOej=r)>4mjLg-r046$XofDLoA|nj~n75K)7Av_cvyl8w5- zi4CiJH|Y9wwf<-w%U$2KZep;!t}YU(1G`+`bK}r>U#!sAP?=mk+SRkZQ$s8T-!I4b zpHj!aIx)CZN7Ho7MO_U57AG`4O-vs?RrJ?bj5_?3npR7ug>-kYrYu9n3HEH%_33N; z6}7qJG;+*l7$c8h2JE8VhCEM0#rW!xMEBTO_sxSFep4L^*1%0%L-N+5ER4MD6=dN> zSr5k*yj&vdUCC^abx3|H6mC`|>4lrQB)mx9NcWCW?DwMHhbUp_MbuFegm8iUeWE7NMNECG?rcjTCg97R4~QBL7&0T8JS zx7&obAl4r6y9#vS8Lo?zYII=rC(lhq$Re;?y;!A*CsRBF5x(e)=a20Us z8SteNA(>c{yP{5sB3&pEJCo8a7x4;uCGd)B+jFhbrgHKUR{ zQB@iBmQmysF_s)OTDNB*G4COR&BP*B>Ci=qBVxO3k+MFSt?Ln>|%@s4(n+*82e9k2f9x9iI zx`7{L&K@p8q9!Ih=V^1Cein0NS3GdE@5+Pm^m(~hcs|&leBcq=#LsB`(D+wf_X1#Q zMEE&h!7N3#4j>B3BC2-kZbi<%0P{f=W6Lq>a84=Q$ZSOs*NsSgMXoEc9gEp$*$Cyr zWHg)h2o{TRn>90AGFptu!bDF>SpVPOWg<~tT3QH>Q5q?Yl!rq>zt2-vSW*bMQ5x-{ zjBf+~AXIUH_$-t2>s2{l0z#*Q@V3_ec%ZZdGEvFeJAN@<+1&peNB0ycPqbF&mz3n^ zm$Y=&#kvj6k?L0YJlYJ4Fa=F{Kl_ZTCyJy$6Ak+0!fUWc+z;6U-{pO(Jz}3?kH8-L zG`pJpNLt19N|nIqFW+uevyY#JuH&K)Lp`jYpd3+TSc#M zWn~yIcJ*lgjeVn|eK+=xc2?&GNBhdxRy1s=d$OjtwoPC=hmhLuVE3M}eU?B`hf^I_ZWF>4n(-{*y@GeGYK@6~^^Z z#Me*^F6;ti;O7>9gq|Sk0Iq6L>=Z|`GNGlWMTnBoRAHQonkFq7czGmJp7vsQguUJ{ zz2r&l6GkKo&tfi@;n|eas8$_vrw@Rf)2gq$5TQ9DK8wMaUU9)M)=Tm6!6?2X)_qFjUv%jc>e{#kuDm*n@; zECdG%8D_ z!iGtido!C0yxxM6;!5f!9+|;8UKp8qbRIHA=@19u$*Wo)5GL4pz(` zRNyTw?#&#_WM88P`3myA^S9D-`P@E+xxJ3LQ60JSh9As=x*SB*7vvaJa0VKq6Ag8s z(UOA@^^eR6y}p|YGKSO>4aeU5OcVt~eu7Y3;PU2r@FZuh2Ytn%c*H0KgD4OfxZ)#1 zP3NTmd#)}qJlu8ZrTsH4HRH|gi;%mQ3OkQCZtHH@+$2pcf#E^a4Sboy&?_8L=LFFm z>dysO8dT!f? zjj89p7c;(S5~F7s3T`z(SCcgTd!Yl(C5W+_80BT=_qnm zWp?#-WqLF5|4i*UISo#o%@`gH8w#=uVvb8Mb;R(&fFF!DowLz?PVH8Eyr7^hhsOHs z!u9O?kg+Yqjew5ehscLVwGyNOp~3(H5vam|9@&fNR8IP@RuaVsDU#w)DLsZ>&;IrH z+i$<=fb`|0{7_Q9A&Gn6gnM7j?~UCLDM^ua(0>y2k*awJka-l6z=ToC!LjpPX+5x- z;uz`0u72w#-1n~slI+|h8>PbUqHrnh`!BdJ)xh^AO2ME|0ZwUBaTu<5A+5|kQ4`9n zqW>PhpFCpP^$U_Y63{S1PSg>`X0r8sI`n;sBppaI2&F~nRqVr+xtEyONiUAvnHjQn7Ui{ZZ=e> zcI&f$-MO>F+*7CQ*o_B_dtXQqJy9USNA=ICa{@9EkY_4cQEng5-IJXmvP`oGhAp7x zj850Q#+D^oEG-sTttpcP*YF}1uDfzQ}u|Lo?dR)Wq zmp?njb|@FoSOqiUA?zRD=aisO`Xq3nn#yh3rZjdbh2yItF z%CKEgCmG`g&7Hw$^}++W>5%tf}RT0#{$JA&QC)G<4BD;HAZ#N2H#X= zbNYN z4MriH2+#}^-at9+*ay6M2Qu1mK&kRZQB)d7AZ|dsVD4>o?iw#p?u8qH3t+a;bVvu_ z_Y#)hOJxIK0HFU;W)f7J^K#GtT6fTtKK@;WE57&BD1`&~Lv&p)lq3qta6;4@zo$qA z0GlHjV2t724*qid_}ua1Y#;lOe1m)g&btXG|KNjM@8Q?8;w!Jb=gKQnH{fHK#XX3J z=mpedM*#p(ISQ@>1rdb9IW`fcT0A}~f53L(;d95?x$Io|(>REe$T$A|4|HuCo_o)g z>DOv&rWgDP4@^i*yaIRAG%XqdEf_D9q{A4L^}=fL#kyO0{*rspfR1D7Mb50nDSpr8 z_aOd1N%xq$TV3xfu|{uTe+Pvv!w+UdIxa&DDaC`NNyn*s0vE(lG=or2VaG5IRf1l6 z&O{;$hk`<|!V`sagqz?~fu}swg>PNbPSazlT;E#X-#6xUUex68id}i-)WHl-;JC+G zQd;B}?8DrC_S( z!V9Yf)=FCxq+NCoS_U8ju!4}E!&DHL!W6h)Z@=mGNg)0fj`z6cLN50ZOxG(gg}D`0 z1K|SD4y3gc6~VSt>KMVcOdu^&&VX2cmQyCa8TC~7Yl zEaq>x*q0`K#ib=q&+&jKhIwJXCLIU#0zYWG1(JobT{L-w8YW%C63h!( z1hS2K9q10XV+cv+MOVrXcpo%e;`hPx1zfl3gIs6F^Km9Uex>Kf7w!;?7XFBNd_dXC zjr+yfZ*~q30}Yw*D(oCr2%m<$^N-nVP;bmlwrw>6jodY)$QWO3&M+6{@5;^3wp5X^5#m?sZ9en(=3)@6Jo)?tPLBVH(bk0XH4B*Ht`x5&v&_65WT2L|2Z&;2L z`My~gvDpN{R%$DSxJ?fx2_ISX6y`>8h$MzUR9*U={a4yZ4ySzr7gphR3!nNWkWTtb3S?tvu(jUc9$V?A0^#h0D3mE$)eC&!3Krwk?*f^@e zY58#3CGRmL;MuFV=N0gu(6a?`P?6Wf!IbX0_)JFvz)|Zo2_tTsh>%>y>jMESsKY=& zVwdc!v}EV!?#eGRXRt5r$ZvKQH#;3I#nuP}Y7PFxKMDSjvK9D4{$-ek+wHdLV1ihF zSL5=hyqWftyVE`|d*a0_JikN;i}j)j6xBkteRxg`b|N+TrM;Pk57?ea!4eD>1LQfP zz^Ls5^HB(ji`&EhvS%KDY|oQV%J)6~*xo0f48Qf(fB%_Ye4YV3r}h8kIk8Z^+vwqc zS8HaqZ{U)XeL?IMUjjZy7P`eAdWX-U2Y4M3p2zn>j@LaJ zUh@wk5S%{Hez=a^kN7jvnfF0VGD!RODr0*Gzfa+J?)xH@OtJJ)rCt1{_`Vp-dW@NN z8*y%h{34Esazl#t8ihzAwBi!4pwkm2MxKSMlHEDChuujek%*#n`qi}iSt!mHnR*;+ z=p8#lZKDaqIa^~qEP!_RdOX3(9(mWs9+^_ewelE2d}qzVA@;UZ2Cr^XxcuwT9-(|F zWg+|)h3TW?im$>+Oc{dJ3}sa*CR`=xiEJydSt$q8bRZ?@DP0Z1VKFZCdCKC0Wzt&|+4LLq>( zUNc(a^LdYA45x>5-@q8Y4G2H+sG~ryFLKdm-<#@i{={MJF4E~s^Z92QABO{b8*6H& zJPBYssYmHUqgrHX8YU&RDS0}95C#MKZgjiZRZq>gi9h)9?4wn6-}Wse7v7Au|4Es< zdhZ;XyDihS!rZa8>2tRrOxKcQ$w2{Io1O06eqjl@vI(9MM&zK{~lHi`u5L(`tN)c zeS0O)KEE&|j0)F&b7V~+X@x2J2ODH2s-n<{vzXp$$R-#0E~0!Aio;1#LI;OI6rU)| z48|JE(hHzUP2f71Y}89JStRtLclSVdSE94EIo4Q%sD{8$aA>J5hrHErO{VN3XSL

k`rao1i6vydQ%xjP$IoB8UQi}!T|(cEJa|d@CP^E zS#UGW7)i!~_@-StvlQ$)aG)zFnRUB19XwcFCz_?ky4ohmEY?*YJT(WT}!POF%e)sh7v zRH+EyDZ`D_N?J+@oRss!#ho7>&GBR>9&?5R1aqqAP7wK9FmV2ree?`JNVa?p8|x=$ zi2OL&Eaj)uJR(0jb&BZfi-={YLX1U7DxZIs!@T@K{62RAzZWRKpZ*4Zr{iBzj^9DY z&&TmT^k@p{iWa(+{NMStQ9iC8FZ@mTwetIE(rD;7-Ph++G$2e&8e3Oj$@dHQyUQ_CX;bGJ0nXn8d0UjY)*Yap%!HzUFv;I$1t^JzWxt* zqeS}$yocGGF`W(K-K~r(3l(EJjU%XN73l1!t?_vUp}(hNxO1qvv8KH?9tnA?eN|ep zfE9vpfW-af!2x24DAj3r?O?6a)N;W1@+?ef#?{&#_lt=#s65j zzw=^6Qul+(3gSjom(%Hu8{sm6NTJv5%D^InpiD!o?4oF+pddor0PR9GOFGwpZW8LL zq-iN9Qkagfs0h8j5JjXkm`8t-LVIDRH4{}`ln8)KkZ6@H@x%h+Rm;E2clj$S{CK?~ z2Irnx{LD^71N1ErHKSSM(-i@KH0lpjz%q#`Vn1AlJ#;7bP*7KRhrnn*+=HD2dZ_G} z)Hz3#eMUzym!gG3xZI@eFWlz`u;bP8{aT^!;|jiyAHi<@t@=Cd-;h+n_pec^;QN}! z@B{e1+c^EGntu)FfY&`q`+HA{R#%T_g_!)Jm>{#)7*+)nABYb#ioxV z3JR4xpfK%0u0EiQvcNND;BiX^*#sPj)6)J~Kze7wM5R^;U;KIj z)1{i`)d_kOmP1GnTyO%c6NptZW|=IK4z@N#W@t1Kv`?iy8Q4TEAXrAUIt0;M;c8|w z8&N@e#cM&U%1ZO|T?l3h`AaLxqWLBH2tsh>xxj?qmsSO^#q^C(eq9jm(@2y2xuwLY z2tnufgfhhN+?!{kIe$nd-|{>1lZ#Ol1j{=RuahZd0)`DLEE@pJlc>be09bN)Xv;>afg~6F{*3C|#XZ1ZA4D8jJ3y=O*9ZB<()>?U-$w`%e64^i ziIQT`G!%l~J7~!ejn*tg@1c7lBpFncAnB1d3<>OHdqo&ITH94sB3_rekDv*M`VZxM zfT-}BdyEvMf!C4%4iD)3qF>(A*wEk)<6BmDg9L1r|Oied%#?G>z! z9HaFD1nGghX3ru(#gyw5jSiD>4+uhz!I3lLvI7Y4h@;Wxj9^3k2Ai`SCPURIEo%qzgk@O9yFUW;;crjTXI%$gz0 za}dyhdGJf8Fb@VGX3e(lSb~Arzr3=R=e^}eQDeoZ+C3rRv+`vtL0|x~_5983 zkbJ)~PnJlVCTk)Mf?b9p%%F4gKLIr-e@med5~ARi_bJwv7zIPwVOPio{}HS%mW{~} z!U;JryJO2pESKe2SPt5%P;``2jgBfzG@o&$16SJAcq&{yBRJ?%$MQ=j5>=HIh^!Lo zYb#=vG1M9bIu(VALo0F@H#V(j*;Y-DwMv&M_8&geCq-6*lI5*s|iy z>bGTaS@&yF$nB@(?`T2@hb4H%)qthMwXIs=0Y%uYAyvz6hNT*u)MgPS5GbdPIV703 zsfr;C3JyosRBn#Ts^i*LVWQ26aCXXWI<~^Kq62(bz0$=MG`c~~G@zQ(odY4PDc~oQ ze?_^!HUI(FQ>H7`Em7K*?o5nVvb$lnk0EAU{~j=<_%*;47EF?E;Uo0@c+=kV=6-V) zXi`X<=qo7H>%)jT&PM}ICGPka%i!raB_okf3q|I8?KEVe*MtV48IfMuiliDRyaJku zXyb&O4v0gRj9Evv$m~|LB%qwqjF6eBn*v-2ABEgp+f<&*oujkaT5Ouyk$B&GDALpiq1hYdR)80zjq zuyiVHQEn(LD)f2_3(LULKx6&`vMu$Uj6xf&2aVa6*khrlG!hEnXCi+a3tEtuo26q; z6vIb}WD;~uu=Qdci(C%7&T4J3LguB^Dh)We7ZC`Jnl8a^4O;f%mB6)vSwdy|JU-J6 z94C4q=P$wPw^pFD%5MaoC*ShE#hmz{0Vj>#;v*tC=^tQWIE~N?4vRL<-v`j|X;io_ zaSciX0U`z|6!3XUZE$`8CP6?U7{&h7~6_IezWn#1tvhf6LFbR$m-?M?|V3cqd=AoOHY+o7}z zN%X-6+^F1uIV{YflK9lsi=vN}SX8!)S#z?jGn(i_+gHfR#icpRF9jn&@3v$+VnF%d$v&j;5go((GCIGwP@Ju zMq6MNA}ah`>(VfJRa&bM-<{itvF~I}TDUw-;ZMWt2$5O%Fb^)=fgFb(&T%FZ8>(0q z8xHtI!_YrA1L8NhN43(ja!IuR`_4cK>Q2)h`IP1kpJhtAgWanKuiTQ@+E8~+=exZt z%r0{0j(fM?dB8nx9X~pH@9Im|+tm3&fBp;fMi@iP_*m*(8+i`s+=oQvo&2z{TfKq%TBz<0bOqr!jD$Kp1#pslx(C)j^6a{ zuj2Y0?|yX2bL3n+=Vi>tCioI%R*MnLRDwsCA|lcPoNa=QXPaaAMkJ1=jJ*skR(DKw ziJdU4am%eIM=qA%#Z0~;|7qthCU4F_F=I={f6Nb)?FqgYt}_Ou_OFwzqn&%dMA)8` zF_0}5eQQx42#+S;)_Vaq6QLH0AvFbH>JXa_U=Ok|<{#le{>8jGF4fIFxOe{F@SUe} zsIXRiM{^F0;WG-poPLVq0>>NJ)7n1)P{_M+{5BjnQ9hRe#y%B7h!#=OMR?^;9pd9K z0_J1L$&+^@`-7hLsykPHm4cbqOXl9*xxbxpKbRtLKipIljsuCr!x#YW=d{!PjJTgW zhCeaNC-J*;nSYy-i}JVLUdhaZdD)y7`}+zWbJC?ydMg5k5N;w!J3xJ!$!JYwO&Ma= zd5}EbQc?2DqfJ03;brr<=DgzZN%8p3VaYi6&M?LVyX`Y8!g|hWoXcLX$!WC%Cup)gPR{zYj5nC)~c)`u)Zu1CAb;IhfjJhb_>2a0W95$rF zD+qhW406(DKrv!83;w>uJ~>yzXY%@m?Df-+GJ1RW+h@#&&uGb<81mb$MSe{+ssRIc z5k-N3k!AbaAQ&8!z@V2JQMG}wi{|EbLo6vd$UgHCv?r{TLFz(ZH-q)pn1wd%bYBDAmr(^{{LkOmhvhe!JtqH-l?}8a8@2N-ISmgtM#w?!kunG(G8bv!bA{lH3F+KpoM!aE5Q@nr#wv*mR z6)ho==mS@@$1VXPvkyGLM$xSNK&3QR38M7Tx%L3xc-XI5&j zL&f0G?LZK!D#8%-N{ex~B6|tfS+fv^=x)S-k@83{MW;_`_C6QlkrYp)UHkC04IAS9 z@!r*~4Q;*StKWf8v$S<^@Ss4NFH6_`+3;DGZ-L`OTK(=bJW;p2ZeqXF?{Nk{Pi zxSjcwz@%?fbJ_U#DWaTu3RCQ~(Ip{zni3S4eSGK6@%Z>|X;NyatZcyB?(y5}D=O-* zx!ip2#7z_DnlJyjapzc7vq9c#Xs#ODY5X`_-_T`dH<}S{!*L@joG(_17Xx|`VQ-?0 zsvaRdrZ6{?`o9*Tk&+H$UG0Q@Xb2!59DdLP1Gq_m`ae1M6Xs9_5_xa-IZ!9hz?^s{knW)7%MWr~S zuD!G+Z#=QOrDe4*9t_5P9QRgXjcn3xtPpj?Lmi}Wfx`x!rY=;NfvHBlQRCK}6LnEAAc#!w@rFE?)d>R?# z@Sp~(RVg1R9AhY3!!l;k4Gyxxgka8Q7PDoC5m76GQtO?9;;L7?0B%aN3F)EgHDI;M z6^H^zgrd=sVkShZqOd*(e6-SvrlQzLk>rTd_*S?r$dOOZGjy7)5254@-=v_oK58Aj zw4#X-?2KiOZJS!xF@kz3hYn3{-8#vPBW2AU9nCH6?KiW#>ke!!pE}Udcxilie0*3I zXPeqb`a4QG`g;>4iQb=*N1F)_Wc!+72aO8b66c^!k)_0&2Wc1wthh8EiYBI% zCQ-X}wPi)ciZDDuTh6#EBgnoQ_h-6K@_*lM@xsGJsaXUBHz;rjV_jeLTMx*g-6M&A>s{!$ZN61 z>Xj~a$lz0uA;o|WP(=T)yQiC)rn`^-;Sa}K zdU{%p-<}*#vZ4Ad9qs4Tk0)yh?uAKwhZRW#6&T~Tu<3j?RvNu(%r6g(~2hn8%Vj+g}>U{b= z!`UUyL)&*8WK-R-)l-X%dw8s zKOP;2uxUj}pFU1KdU8^z9TrlY1r_B)34Q0h&SI~VSJq%$CQG3h65KiCcll`Csrp`O zvS*CxCkHl6_M%ZlG<3n>+NrV0jk_mD5*%uLTGUta@ zuRoA^|5q&gH?{YzYOUzXEOpNGkLhXa#?Gutj&+l&=CP$N){3E{p}*a037UWGu1B0z{wTbqxfUXW0W z^L+W}1D5H|ax2kg87T2Zwbm34<}`THKBw2N+KG4q4_@p;>WBZ(et6JZT;fBIORx4W z_iLZ{I(%;7$H+xH2WVT3{M(8^7SEYQbd4(ZQuzfpYnT(>J2GLY!2=F>s| zzNewQxw^W!ydl}znoRo~j>ck9dc`U&Ih99;ox?{fb1aqFrKQ=+e_(Yz=ZC}R_c)a| zj1B6cPzPD+c0@=+`4G(Cgi8qoRjP}kGY9k(x810Zg;)i4@Xt@bEdTDMm+Hh>(nk?1 z;5f;yc;W|7eqYgxgtwpb759JkJ+Jp&erCN0=R6W~9>c7RSNj1?xr%87bJZIK86=vhxikutz$rT8Nk|B=9btD8dQo$lt+dDSa+dDStDO7q|O^%tzdRaF8 z8m^ZGIfFJAJFRTVF_#CGNKH;Q74O&!c0MpW0r~v{u;kB9(Oh{xv`A1U* z;`cI486XR78d=~MuS^!scJ)M}q^yi6M47M5=k+YQEjXRXdLKGLQqfsx1yUQIVK1La zFTT2Z^)FX*b8Q$jy#~C;Le++%@^aWY?)N zDL(fky#Qe*Ai|tYNOOSniZyaz-*Mmu3zPDT`tZAmaHDIG_b$f82!ipDL+rF=TCfX; zaO5!hA0ok;3cJwagFKXoZgkNTAQiU9*#uvv=((@W3ysf8eFbDjZ z0Bp`a2Li#uB(I_hTL{wLsP-x-ZW+J+Qe6Kp8f$*E=nDVCx{<$OrHgvCE6 zCr1ynODu*ECAiojrW-H)*krsPuWNqBF8$AKKa)THpWARQ=5vj3FP8QVTxJ!t`aNK} zC_0nMjQ|cKp^gHH6%(RD-VNAJY8Zo3l^ocE`iV;2o*%LVTdx)BxtGTT<5@nTA954b zi!7%c6F>x2cBV}QRMc|KG}hJrS^b&c#wP(biy6n1AJ97F&{QEs`r+?fOOo{6eR znlT3Gf?OGFs)tOkN+=#zNYxM_PN~14>P_CzhvM-M4Y6YR_rsHu!>su2=0u|T?Kh~8 z_#6H4{i}8k?u+-Ivu5X-bNX*@ZwfcH_ZPZL+=cyugsg#lal80&z=?QbSE2*fK#Yx7 zIzrzpP_O?zXQX0F8kbD}++G$6m6Zj9KULqKSKrzH36*F9xBU8A~Xjon)48yTn1F$vWfEWasgzP(J|ZEIorW% z<>y0*0PLncfkcSa_P6ZlIMK1AC5azPk|p?!Wc4o!-#~AZ-vMeLY%IA>$nx2|qLG`S zbljq{uP!AC$W)lh#R{4_JDa}ng;M_?{@^eD!Z$jbx|=#H{iXJZz0?mCK@$FeBKima zKd#vlbWd9|wOS$H2CPzDq_So2@40BGhk}Ls4U;kIv(Y(tMeG-{Jq*3vu$OK|$CoEz zCnP#V_E?CU(4m*)G%5RbdBV9xNeL%ne>1C;e~t+5k2E*m)7(t-KoZ8m&m?gbzpq6| zBoO%vO;x$G5zK69B|V6)sHAtc>~kW{cnm#?UgM}rHm1DT9A_MycTm6KgV5(ZyhcNB zq6@?gb~Ew~8(_Od6%yp2ksgYkSX95yFeMmthEbah@w6zNQH&H{SI%PfGNK72cBWS^ zW7u63Yem`u(SyHk*+1D{6t?SF+*0xpP8z|LAG87tWrXXagyWSovm1f2AJFPgN_#~0bwd)3eEGa5ZTm`r|4 z`;5UcWbF&c5hmVCbq|qKMwTex2zEXYg7aXUcNPu|UymIjtn{B9h62iMLz~CD$CxM=Py66(N0`G?KfQODkX zUsT$(Ubv@#@EqcEhz!G>@O z=~&QLmq}HqFHRLMrR^Zd62!!EI+Ii@e(F*gYU#O<6SK0i!dc;fkD{f4N$}qw@i^sW zMoJM=bSt#5Q>8>GS0ZOC1Ng0yY47B-bg1+3`eCS!e+JN7coBybi7?f?I#2|T} zvuz-+EH6K|DD^?1*IySK*stNqh$h0=$50qa{smhBR3`8S6F2|?8@SW&x8r3q8A^~G zt#S>hzN!nNk&__UvY__T06`}o>KPvDXY(f9x;=W$#*c}Lz4FJ5^}cW{ z7WN@d;HVtPD=p2#i#2-6W8w0qa?fi19lsIG7vRr&9de~v2qt`dZhTfs9|y2?UT>cQ zBN0Gq)&V8RDhnyiNu1~5P1BQ8R3cA$axT7s+lMPvIW?E0C`48(H2_2nVq}j>D-a5Z zp{%CU<0m=IVTUZLMAV~iBObx^Rr^AAGgQEB{q~3F@oGBmbSW*AEB(CjS>{uz&Wy z#Z^}h#4!2$wT7WO33A|NKH&Hp9uP2l4y%l+~5 zp4lgpWwuQAWHMQsq-nEFo21R8OS+`AX-k(BAzf0sP)b__Dq1N!3dkZB1%%!!D)wFx zrQ%gUyr2>gh0EoFEPpRty^0%`tDyc$bNc^&-}juEGigd&^xn^ZDI_zQIp;mk`@GNl zywCpNJ!zH(9C|fpr4wJdHvPwb2fVUhH>DOtDd7x3P8|gdWY|&Xz>6-6Ttn%g zp%nilzYEI_RgnJR<)PhlKs@uE#~xez*kj+(>jvH!dg6(pHze)$%q=l_CG}4^TD{^9}hk+7LVVTdEdYCKmbQoBu>aNni>0rxz9}c-bOr+lR@(o-b0XlEOHF*Mn$Q*9o6H78gEu zEc8Iz^2Ke7m$z-*)U|2r^LzI;?A;5&;jP8XmoL^s<8N))vSq`CAF2BY{^!1@j@AYp z?QSVYHfgYd17mQvz>;M3MbA1?q5go&$E$*nB`i74a18so;UB}#p-uaRaFsw+R|U*!nX9c zxaJx8J+1+~a2x5&sq|=w4P7- zh`TM6vq%5p;NbW_$cU*_RbN3tUzPDaz>vX9nCCTlu_b--kkdC1F+AjuiVN!EsoE}2 zIZ!>2WQzU4ri(7)sqD`H?rk`evZbP8k}jiY zCL`S@HSjw55d9TH=fG6g_#n<<0P$-fM*!C9AEx{67l=RlQmb$qulMxzEJ1?trQzwda7*+oh_{CS z3jUwoYJ!pU|xve@@jw& zk2NtF3^t&X5%3w4*tG0fX2lLiJCH{N(r*!_SbuKAhc0bdxv6{0{L7L9$sM2kuwne7 zSu8Z}XlU*0{_uvmi+1i@acS;_tDEO+>^g8@h4K9r7q8fCK|UxW8ebL7dTXUU=>zAV75MD_nOzP_k)mbN{9; zAY`X_W?8~`O(Ntt9>;1<44;G76uwjFkxu7{d0c+pL>_1N=#s~MKf~e9dN9r7zE?Fn z7@S=tQbqu4LfZ>Wb&xr!XBTLwQ;l;JoK8xe8UCs&xkA<~$StT~;3Opv@>eN(u*AF) zCYSt136p!p{S+LH3q*L24>>blp|&)&stz z@CIC<2S0YkGKy+A>*1$#?}6DC2Ps=W1-!mJs88Va2Xgno?VW=Pc92jUsNU-W)Gp6< z%q!s40^2lit{;&?h~3zT_#Z!X^p)um;UL}5Aziu$F(8<`FR)(@ihK5-)~LgXA7hwJ z->jaQGdkK@r`9Dxi7Q{nrZR8)qPw1IjuXvzP#H@rb<3m8-vyP zs~(^9wfa08!j0wfZBcFL9Z~H{LbP&#uYDhUEr2+>YD8Dun(`N5S5}$3lB+OX?JzEN zdRO{{?gvA|ae)2^C?JY*5D*YY0$xObx-+{HKIinVeE(a9Q!%6?)Yia(B;z5mEg6H7 zOOu1i?yW)#5NDMWmjk7p59mg((B;1%+GJ_>zF;%x3%wrseMyLcXJ|n%@=xC zHuTjF3?0IM`c=0UpEp3$@fBBytqt>ocMTna4H+wG!$~=z_r(;&IH%i>x9>*qd&dwM3oY- zkE;VYh(19`8TO9ff0c&aMLz|3w$GrGt8-uc#ZmCaA2^Z`9idfpcVD` z=iF0k%=bV35#Hn3y0+@rcK|SJ^A>4|R8?Upm=BX5vp^u&V2Lajm$*0@W0Cn}C#t}9 z;xur?q8@+%kDJo#R$bda_8+VJMh+Z1qc8QZGnzI@&ey+Y&Lf-F?f#4L`@zx6{{5fc zD%&@mMSAkgXS23L0Dg%o?fg`zq6Dpcav6w+r{{v%n&)7pBo(kKhjTZGi=obV(Gcnk zl^?OrrI3+GOj47b?1JGGpC?0)qQzj+rSfb;_z`B}0&Y1IB^`y+v)PpT!Vg;hAOfkr zMLWK?L7$}6gq(^~P}0RjYkrUXBvkV4nn25J}1u6+i)`28x6pdVi7cFNb5A_4I9Lhx8U+s$L zoZhXF!K5q@$O&9?s?UR5MD9Il`5MW+m8^+~E!$3`Y?Mf@`qQ=6&>@?w9{KTv%T-D3@qJl+GQ!@V7shy=Lis z=dHWcI3_L)Z>^OTq7)1hX%8+hxZil({}0h#aWqYP^_{hunOL-E&1co7rC%3S&3%<~ zMi+hc$@wPjOqvf#CRdw{DiL^wXNJ7v{B(7^dEJh)TpMU*_j~PF27K@C}LG&!x z`{CcMxLCZNmcVzS?s=p!a1OS8-E8 zYey#sz$gPl8+`7v;}0RG`5`ji5mEZW-nj!SAQZ1n^`-9k)&-)@c-H4jb+0?WXYQij z??!emrJiv3F!coSO>y&i#iENsF?EFRLq{NwtkmF_LtwLxKIH;%%6J0+Lsx)gAUIZ_ z@ypXi)qT*Iey9MYU>^(;P{&76!EFZNMrty}zdCcmt^o-?Fgazhp>pkX(6<%f5TRd%Fvsr%J=KK|R$Rsh2Yo1e} zuP`H{r8)pRqUO5>COa5`aYn(y37#OJXyTQzAgaPyR=qWBPNgP@(e#vKR{Wv3OCw4#Rm+NGA`DHow`t3eE3AgQD z=sTwE*D~@?H0Af(t@b89m7Zs6c^N+^2Dhwe$7y*JT#|IB+v*k4doaP*NF9%Bm;J1a zALD(VHNk0NX_Q9BJ;BG>=ViPpgM7FT={M;H6|Yu#BV|yaX%eI>+TrLq16Ehfv!00s zGf{#vRVH~!=9;Xphf@VW7!F~R7c=z$oY=M{+oJ=f6Tox<3}8jY^9i z{ML?LpamH@Im(P`G`To-0YfN32cz1z#XI79=&J)t-ibl!#~gO!yDXPoX$l+#R>o^4 zeleWN0g%d@#H=KG(xc)Y;|JpAZ(lL=JT4#5j_4urB=+@I_K7p=e}cnXF~XLm+;SwH zo(G#O%Zddi%VMWF)BuC7dwe|bWbu8+j*siZh=6-YoGI%#pqiZQZ&yaknJJayNLM0K zKnuW)hKbAG%68-K(1T}wefchLuEUw`kHn^Han zUXNTP2qAtchyHtLFA@W>_Sw+pW%Ms{#S(dXeH#M)ly@9jS)49?hJohkXg~j@o`p+h zom=B@mABXQEF7GTx6X>Tvhtd!UsM)0S0ekWm~mHSweo|5i)RhaTD+;SrAobtPOa*V zRh7BV>}aeWWVbr}Qh!U|ga+RP^FXS#W>#;=Mdb$6%_&DQd1nkRFBkL0aQxHx4;MQo zjV6=O+K_VU!nukQYy*rW`tq6tGRT^1nyV_9AW|Qyr;DeZYN&Knn6@&cTF}>{Y=)R0 z63CSAqTW#$8EWr=FJjr;z6ED?&VsXI%940@PEFO(sa<`Wx?jUjeSCb{B~$h-hA*S~ zoR3fW_!4+5!l5-{Yx|l?u{YMc)%a=ew*JoHO5@rR0No3}OBsB60p|Fgm`13<+3BO# zZum!h2r_N5ZT?x~;zR)Lk&$Hb)Iobi(uGSo42EY0nzTFq%WiDrxY>3#yPg04y}kr1 ze=9^gA=UuML5!bNP(eGk9&{ls7_W`fBNA0fTU76*^Is)v$y6(}20i7107~#v*U*_iFznufRcVOMvVMp(Z6)MJ8 zj+?}|tZS0P7(t}1VE;378RDis$O|wm!kNIH-(Jem|? z@jLO6!z;u^;-VERjO&f-Wz4qFn3J_g+8xlZfzC<97~mr&JE35i&d3%g z+*mNF@F_TcA`!Sd;Pr_+C}f}#Vmq1^g3?6bvJ1Yp$?HbyvB!7t%ooIqL&is|+jDX{ z;`&S#JGBlTc>~WXg{=koyfBs{!-~!`oezcJ??lx;1i&~D_X6tz<9yH&f!s|sybi&I zORi=`uS>307(?FORK31^T7`p&4cRG=qIu)44Qpl(di3c_*G;*$DV1v4wXu8lte)=K zv;7y24sG0(Y%A{CxMFs=v}@^>zOm5@hX(qG=FG>h{sGFgXQOxWdx-dQ>-#Z(UAq%& z!1}C||Ef>YqZZ1tEZ_i7qS&;~_?dD`BHRnJ{UEX2$Btd6H_14if)t(k+9H6Y0=d~~ zjgI5OnBH{uHH+5m-mo{e^WX;kt?|%Be;-rN!@Ua;`BIn)gI=}8U>_Rwdc?x$I3^Hz z1C|kmDJytv(KTmp*o`Qgw^-F>ZS3za8V?Z{Cd|6qiQRxPTTu6u@#uaH@NnB@;E8p@ zP)2~Q*uOeK%j($=5Zos8y-SrP{R1`5*x6I#EdiY zkgn(0kn7HII2<#XVBImv<+_e6HQob}aK;%qpu?H+M)!tXRRm)=)zmsQUKI|p=Xh7g z)SlL!hPtYjcuPr9s4`rcr&__I{D8=10xQBVlbz^1iWn^rrvr5caQLWmR&~VV=`6pW zNWAXC&!q;17);^!;4|V6i;cWFOaa1=n1j!j;ETZ&{;Dibh=jO#(d_=&i{vk37h?*= z3pJJfl{NC0QJ?vCe!}`Vp}CK}2VCw+<8r3908;X|9!gF@0e;&3rqa@0Ae`(v0jxq) z%|(8*Cuh42ui-$*;rF8jNDwVCOw`Vt^qy?w=1b(NyWO~3mqkW~Q^`dOd%7(Q4=r7| zYSF4WsqO_m3y@BUc)Y5a@tMctI-+o0KMAhs84qaih z{@RyJ3DKWS_KVba#6xq9#eF4}qN1dvvhq)DJ)&oN-BjJtTHDs$-G+D0*2-9^D2v6A zo&9k7o%pBOo&BA&k5Q9m3mFGiD^^o6bb#46fY_ zgeIonao?!e=*{5wvhoC2Cp3E0b-)>cuc^sHJA|%({NWR~sS3h#(@l$Sy6MxYTW?LF zD=Dr%D5j$x^&*VRp57<63gF`OiI#)jlJ-HJqoYHkqhk8Dfo=F7S7O|xmW15ARtss! ztyZia7Hx3({Vaumh$p<2p)uM9UQR=NNDGC-LDVZjjeB_0J0Ywk+aZ)9K}Fo}-SmZR z+rD5+MM=)zEb~>CTsYt5S$HY3zVJ-eEdMtQRH&hUcGgo7W&jT2bnVB(sB(ZoxZN)1 z0Jx!4!`ieg97gCwVHjpF3liTXkWi1G1RA4NJac#gM2A(*hf_-;m#_+P?J94gm9SyG zd^($l020diNKd1D>)epz_9J)FxxBVU#ilhi*0k2P9-mnl0PD8LPshPeB#D#T*xuVe z7eA?)in?x7ry94LZ}F_y+%4UEqHF^vm*|Mg~{QqVQFz6s*$gO)zd$cCt#+8AEDR0+r9q- z+>WB;H8oXLSU$6AT~(qA{=3rRNM*E=K`}M)ns`3rR;-$&5M)$DSR%_bL{kx$`PibO z^75i+x0PMMdSw?zYw$i3=kIK~Qc%A@f{@ zY%sS2DmG>VjRpBA_aX16nDIkI4peEF1EB0P@YiPHBt5v6puN99a z^&A>FRW`DMSNP^{>4uGy4z;<97R?pY$N!>x$GSH4)3~`};hcs!3s;P-pa7_H@;gm< zW~RC__%`F}CV>ZGbrK%TAp&ulIv3I*%~OpgO_gElmUaFr#B{ z>x7$=nyyW+57vib@mM^>&^j0$+hb(<$c9F*Gy7ZkOTb)EQUtLSwzos~+_Qb(b?|C> zigVX&-&bV4yX&s)Yy2g?+=gJ$KI`3e`v%9x4mxz7ySU(d>ve2wXD%|wh2x<0O6GPU z;(3+41YJvUB{FS>R6(03&qf<_)Y4#~UKwW{BV{Tv>L6m1*R9_f}VJp0v_5@g?(@Fx(O3UIkMKW4TK)RAy!BSi-l==^ZK8RW!)7#qD$w2F- zraZ=2tH5f8SSMN#ZQWeMIY_yU@ztvC7#{@Xgt6k3nRKx{UR~e-N$@!dwX-nxNK4D` zdd60-9$EuM0#>E0xN5(Os}3ezh^uzh*Sdn91QZD*1RH;@h5@AVMSX4UA5}*1e<$94 z97(};dDQU^#)JLJ#INiRp24pz)*R7-gv|xGP>^c)UG`yi!qbL(&3UPN@qwBb^&f#S zXrn4t#20=x~O3M&5xZV)8IPVna=4;HEPWo1P&1zn}P z7L^vki(^kjpLkqQTT*gQq|oAbqPc6%oG$(_y%#@^n2Aci?`v=G<48Ys}%y7d4Im39-wGB9K5oF2?c{@l^XjjTc@6SUd;~f$ZHmxKswExJu~eqVfb0o24Q?AcKM**YwHA!_WcX;W zcpb^JdTjjH;s-|0gt4;YAXV%r73NF~stOv7_pTGo>x?QrX7q?3u*fv`qo!4WMlP0m zW0-XW_$i_pktxN{Wf!97xsMdRG0RLWL+4vG8TE#JVObG}fumlil$`*y7I{jLX45#+ z*N(qJqtOvFz823KzmMjgpQOWSC%}+n!NA~Y1V+NP#`utlFl2gwO%4N&N9)<^L@NE* zpF!k7=5Bh?GvG>alh7SYpAYy@~RM1#?Tj> zC^#7aA=%SHIUieUC_P!av$ywV>wkbGX=pEKXf5O~ldmJt|T zClnZu7Z&$s3yK(j2s#|LovFhq2?I#c9Ms{MwSu=&0&r}gsA1}G5QSm=DQQZ=$w2!Y zLJ6a=y?chNA_D7t8>!1}^d*;Fy3pgAf8mn3o4Q7mo9C-JT6x>2n8#VFmBf({7-v|K z6h~oY0SU?!1-nq15YrKD$Bk6m*Fdopv3U4@z?Q0QCT|1R&?VFM*dQzP1oOP$U2d>R z@GrRXNCt4&QPjcey<-@Fh3{?lZ6%P?2t2t! zew;AAH(L#=zb?|Lp09$@iTPXr6D|s`B6Wd@<$jVCgy3ssrPgvx5eEuAya0Ci+ z^Y%nUFgL#-xCiSCrfjbZrTIN>zck$jklX=@p&&O zSv_URvLvFa(Y6aE3@hn#P9VBE5e4@#>rYLR+31l1+hjS9C;p~?pto-3)TSb2QuAGF zQ_OsK3ZIFW^JgzYl66tEBK_v2^t<$%Nj?I$GP&*^MBqbmgE1WL8p#5#1{FHV84$M1 z^Pe^Q1U*DZx>Kyd4JL#L6tpdAchoC)WHRwzkDk@`a@&W#yFNGUbcJ%e4(qQx{q%U< zZ#&j8_5Bs>_rG~<5&I@!+OlOEv%lBN%=#3@e_LL2Hg+hEY&_5 zC8%pa?*p%}5~ZN?b+#W{cMQGcAUFR-#ib~HN^2g^b=d!#yP z%z4Pq!%gt}dgxEK`+X@$WchyK{=0uH+y6VJ?2>NaJfd%ee=8pxwpMFH(!ijGY8Q(K z=FINyn$}!jTU}8S3!{*^X%*|bqf(*@AgzV_iuM?qY22a>p$RfG&Qq9`Pxx|h1`@A(*O!Ta+f|ed zZ7f^gyQ(Kq@7freT^5;Ye4;+lliLz0n;qH+@uaV-Cr};0|2f z@=_fB1)9L)6?zsPhUe~F;L~;Af}L~uZrs4TtSRc5+7SsR}Rwxe&xVn)(F^m^ClBJp9y1Y70=xb}@_0{#22o#Tm3sCq{&49rsP~^GC z=v`rsElkjuKwX`P#Nu4%IF=PNflV_jLv(O&mH1)RUgPU9eL0;5c?mu;x~uk1n9zcr zo(0CylA@v#{22eiKeN`M7&;N8wCZ>ytaMBz9P{kNg2}T}njzp;E=BleNuo3niz?_% zGej2j0t6{!#0bEqp`;$&6?rM?Bf;uZD_cg^QTsGQa6l^`xAqP@(kq?QqiOOO`38MoVIRtqY_5dVd;v6q< z<>R0S&Kny@Jq@jmtpzc3c9Xfq=@Ls5sD{}jiaXTRyT};erX%sxBmFIV9#fac@4lyM z$azn3X=#zsR8n-;T}6l^Zdq1!_vhn7S@Y7nON&2$Pf>~S-rZHpr%oM;-_tTwb=T*s zmKo2M#P0cg5vrxhwb8&2cYq&OBG1NL8<5Zk&YgzyC5x_L;vzLk%0lE-;c4Yg+qqcQ zI%QI)HTzmcA)LgqT8t@+;$*5X$0pO2rC740YamL>SP#B3+QW+qsZN*@>3T?1m&MEC zg{6h1!GiZ;R48;?@9Bh-&J~5a7f;YvXWu9-|K{yJYsT6cXLWl;3x6B`DnH3u9dXXg zvwD3mzwGNhYvz#r&A-WOK{VJ4(@>u`Pm88POjCe6niVJzG@vTk@vwjq4-C`OS?kiL z9WE%;hn$hThhS22iO(I(EAxyW(6{@_0)Kzrf4m{bY}I}*V{P(iC!~c33GxVGfbt;i z>mxYuRCx@XvO%Ukgp={0hvsup51yDjDM|{ebBx>Y zk%yXE1a|JF`bNYmpcV=&pFBeu@N%4L#YxgCiNxhdFiah;HVIbQ3GNA}1)4JY?=C4Z zW{4*O#s0guz4}_)Q`N2^S0wjo*Gl8EWw43uoN3&aQ(YiRRvI5w<2S&6$-0Je z4N!|8+Z}5FjA<7a%fO8&YCWL#Yz(di99a4^<2_^ip}dH5NG~i9{oCL8kMsUMQ0CLO zk00=sGEx%&QTAz+FxRJQ%r#g{#KW+J7ukkHA;wyST2DNfWMLQ)6F6w}z%t`9tb#ai zrg3XdBC>3mxO%1WODuxeflsgq@;QF2!#ueTu+h_aWy&(x@5lQ}f@t{1%=TK;Y-*=$ z0?i%hTDtYMSGV2mFAj(&j2R^*ZK7Djp3aS68LG{>{9bRA_{rdZ5$2d>BUAVnUw|GUVElI^kwct+1`8lA*RR(-T#g-B0pph|>C>TqtPM4YHX;WBy|_azL_o^e zFjWBK*$UwYR7j2pMIs@XkfE5M8b^9YCRxGjl|S`bfSUZv164Y?mcyERz z21mEKBe!#h9{nK*Ns6yj$N}rk&0c4OQmY1fc$ZPTW{*zv;!mQ(39A!d-NS5`v%P}=-4GbVoG7~!% zMG;B2swxb>tA9y%qPaX83znCyy5YwILua%vsBSJREDA*`zc_u?U@Q(ls@xhf^USsaC5^B;w&9f6+aPo$wp)Ubp`8-`)T~Z@A%x zp&M=hn3CQsI5#|$p9i9{;Dp%9tjAp{1!K38Ceh~wlFfSXK!*k}E2U<3H7^m&+5lt8 zx&oo`rqM`N@TsN48Qh)zXoYd{UzcCL{yO7b;lFOf<(C80E6@3@1dd>4!I!6P_*M=8 z${@1RL?kGIh|Dm8^@t9VSXA{>aO`9=3iErF>w_{m=-LyO*>M&EnP8Z~9(ovUz3Z+m z`%Kh5_)TDr`&8jAPh0n`007G%t^(@?bJZ>;J!oP#FlE}P*OTu8Ys`p1o-V&f1-%Jh z52t)U8JsX~W}b{k{VT+$<)p7L&L0t5<+LRc+vg46fXX0N)@leH*8pfswgJIF7{Gj$ zWr9)3Na)0-L5%WF@v&QPy<_06TW{6thK$>R#`n_b4Og#D)ehgWY^PWcD8*2uCSF&g zh+CoqSqe%`U`)EKSlBL4X@tE=%=~D{Mg9F3Evcv=a6eKw+esA7*gAkZv_s414-84X z;i1P6xBQM{0d_lmUl4bknTTJ+3o_ytDj4Yd*t<_0cD(mi|D#7Qk$67#GVr_tk+qkk zg1IP>;mE@Z>!_%MNImm#szsy{xFG`%HJnY#8MqY>%@TAJrs5M%bd@4?$-KdP zxI!+;7ERt5Wm34fexp$?e!kKCy#fE~FB!Vz!$X(sn|JBHc_<03ANv#hG#ll7b2NAy zu;V?PwO3mVT@-xfV%RWH_}$I=*DhJZP|iNVeI69&zx%{5pLo~8gX0sA_P=!$ljD@M z1pFbp!AEvH6d)$J$C(TMw?_i^JGh-h0xCx$x4T{*5Rm;n+l`yV1ATqQkl4L_+nc{i zT+8+**BUPyFUz^l!Q5wIW;tAKI;+9%P>Du)I->=Y_(eN6xT8IYEiTS)!))8$eZn#8 z=p~q|ZP8&eAGb7Bjms#lN^x}Z zi@?@7&5%j?6tQtcdt&${mT&*no7=XF-NsN~pLoEyDFIEZUQQPCd;n{4pInPDsM8lN zfL#r0fC(U^ddYOr49{e`n#V0XD0PH7xokHI}!EdD_3vom6V}TZB7b5A!Y9hW-gvR5xN|~JaY9%0{ zcPOyVe-x159F<$lq%ioau3cnI6VEL&J|Y&+{@)92k8 zFTJ8Pey1q<-eZsLx(goBH4HiuUqSHVJI_9A#8C=XK3m0$?U2ugFM`ho?$GL2EJmvf zD51*IhiDQ@#2BH7u`F0v8<(nFC!b6~zP(eRY-g0zR{vW*?6-Kc*ptPYd~!gV>u!=k^W4mS3JHMcylcWZtby6-u5q6CzyIJ}LyoGxt? z&z=U`61*0*MUpw9h=%!ah??^315CCkI#`tA@FgIPQq-Gn0(7iPL!vx1X>fQPqD&EL zpNorP)p=fk6K)6^?-nP}@W)d!CE!m)g1ObP^5FenEf}qiMGAZs^*CQ0Rk2tVxR);x z4HraWRmDGkHby!kzP178CfKrJ^MOu*H3Gg+$yAb1)&UkoMuM5c3YK}3@lkR0W81zv z@ZC557ULY+d5Gg2`zc~x_v?$Wez03pc?f!GoCx6;k{|)riDL;-H8$H9@0HH+rC z5ScH{R41`m0S-vzro~D{(`HeWM+{0Ruy9r4brnndiEYpRWKe=3977DA1sI6rGh={( zk49~7DplaYE~zOm%tvgmQ`DA$u{%r$(B)3A_<{f!vHwZ}dJ_Va$dd0|fs!X}THu@}DhMM<0TK1i!4 zu6rLu>>qDHZSsc{J*#`3IF)<;%Lv-;5onREov6OP^&I`S@g25%P*w5d_c&%AebRmp z>++xMIgXzlr3nq>F6YBIrjy>|yz)IrewlmTuP=%EWIB6sa%&%tFG95~jP z_ngwYI@V;?RU16Xv)Y4$X4@8t1MR>`uRoA={n$ybAG2RCnon|lJjf%)hA$A^B`3e< zl8N^`e)4-BpKy2sRkdIe$NrRcKhA3C z6}F7`^qh!6Nv}`q8*$5R`fcofB)C>5U1K}!9Q_00K`x?+*zx=1lR*M$WLSKSwcNQr zC!ULaeXN;ih_UKDR$$J>if3k3Df*7<;iGKT+Hp+}A~)tDgL4p$q?OPY&O&FyO-Ly^ zPrE?7P`gC?sCJolrFN}$y>^rKY3+98GT)S?h#)QUl#u$z9t?N-%^wd z6$-wZ%#X|V-um47%%XCBZl`eTa}Pe}dwy?yW?g5E#m}wJxMQqq?Vs^;>pJVbbsb?j zm1htZ>t5^Cg3tP!V_KhaOzX#b2CuWOo$NE~Ui;kmnFTBF<@?F-H`%@RXINtpFV;BL zys;|}oBxTenYXXwt??oG_uJ}?{A+KoXtuuD+iScaFTh85uP(+%xc&ufvcuf%Z(A4R z8l$RL+{?fDt-Od2#usnnZ`|Ii-pjA$Q{)49;|u&B(8;glxcCF3ALflO;NkLeIo4k5 zf%uElF~Le-!hv45K9%3n#usEPXci2j zix9}zLW>^DA-khzdSwLRcr-#H>XxZClALU{mk?HfV7!V7#0)1Y5~an3X1wZTag&Hw zMetQzc4TwQNW2Mv&DuUsl(`3X4=`Z0Jhi#cJpoF5$&&Sao2Y)Zwi%tT3#vmXvihUO#l^D{;@slJjm-(e@1KTte9mcpy-mIP$A8eA zm{q)3Y@3y6Ze0BP++aKusxHXI2>Na2)$4idto8W1c-HsQ1LxKjWv;Cq#6y}J9UQ8$ z+3$HK3w2jfFQeIb z8jPl^4Zo7cQ#M}5>+C(65mqK!G^^{4r{8?@P4lKV-~9EjGxV$OW6yTKmvt%7vmMXj zy)hK>wVo&Me-mw6ZJW}~^1*jJM2S2^=uYh<_&G< zwBX=C^^gcL*paa@IbPcs$Ft81HQ#|59X3Mbuu8uvUXVX59j9QEKrhZd>DYmR%^~qK zexJU!%{a<=$%+FL$CMQ?J^-$qYD^X_kn_&K>DCa*q;jJ8u&WXEV|f&1WFVHnTmWZg z0Tg&??AzzjR}Uy=Aa{>mF~hXct~ z4RP;q52nlZp@;>8cs5FJOzc;UPlb*rx9=o(8;@JbW9*^TSA@ zT^wZKcCCnWFY7ySue@eD_6nbA@fJ*G?fBjL#_6!ku+9KwC<`tZ<83%$I#DeLDuiqq zwT-v{07@wvfY!zx64vowi68!40^BAZ!}s*ub&LB|mOKQ5T}@`2)CubF}Wy znxo5_oI5lh@`UE6=J?sAPPXtg zt3!Zgu`B>p9IJ(=-%l5cKi`iQ7YLN~IVD4oher;!Au9xc-5?Ybhz#O7JM~SBEW}ON(@nY3^#O) zAP~#3%!+^`8&VD-FXO_PcyB&XoMU`SyeJ=M>~eIB9^I-Q#WgVJYk!uL5v8(#AHr_Z z8LD`!^90Wd%TgoAkH-`b;6a}n^%7tC+T2u1hN!?tCVXfQ$mn}}gCxH!s1yhgZK`|+!v{0S%aO=K6cbIW zh-Kna#W|yg#`hl@HU7Xy>yM1zH#!P7FpnT$y|mZFQ{7+^^!z#;PZ@UU9>$cWf8YD_sa`r4?Y>gdPD}1Ar=V5^ez!*tjEX(RA4U(o=D6zU>!V|GUWz8du5%B{g z883_)yIgM{Dc}#*#;Nv`i5qH}!>QFWMEUCcoID3>HDIlBT>-2TN(Lb#)#GuaMGxe{ z5!4$-;j2(ctyCx$q7fk+2%_^kwpsR?6=2OA5_McLbg#DWwnL+%qldCK9G6Z`Hf!T4 zTH@XVylVeL6XvOP+5}EhprJ+nshBY2@4>OgaptbSpac` z1s(+hPzb}WBvb%MG6BM(_NE;c@N9<(i<@EffCcf$C^ZEzRH;k$#XK1-VZg#43rzpVp=&<7ka)acF;$CngNP!H{(Xxt%OkNWki741OswVbhw^L%_Vs5JSHVhBvb4j2u8prlxB&3)`OegA9LtR;Msx8IDh z(T?wrtel0;)xJ*oe0=aWAGQF_svox&OeHXajL#N^^4YDan0G5r8Sj?Er(j&0N28 zAjA2XO{2CaMwqm^+Qf|jYT$p8BJxbCz_5_oto5^vD>1I{v^mdPaNCF+sAO3qeHT^54Nw>xn=Qgb61=6iRWkb&Pn$**{#XD<+ir=j%|!n z*neprE4Lr#gtgis*XktkiX>H#D3?XT4{d{d>7?EYReaB~AWM)%Ce6NlJRWj z2jFlopXxfYlpuJpS`UX-i~HKsIP6NbO9>1J&S?M~-FF-K4CRCI2Xe1H;Hsj@~cVRPrbZ5Bdb9;gQk)j*D?c z1`fz2fCC40n>?nU7O1%_3%+puu5E8fQG@#5%({z>~O%fo2vrr^;CJt#1DAh__ z$iE4z{dB>h2mK+9DN4o@$0Mcyq6EG3bsHT@b6hsQ$>FWiG9*Ha3@uSO z0Wy?Yz-zBrdH`3|uA|%wdWV(YriZGc_1ucf0XhggUL!sEXx)>}stz9M-nKB2IQPIx z2xPA>+R-z5=I6E;Rk+^LFEZm=^35>6-GB?k#Bq_^gbl;MkKmD&w{K1)7Hse4xLZDV z=4j83MQU8|`IImFTs;9Qa7 zkm4Dm1EVW=MN0CKT;`2|c~CyZkPK&#wfo`P7Fxg$kr$1Bm|raDqy}vuBm;2ki#FfB zadQE~$3p*jeHn$38F^JBLq8A1Bivj$j<|PptM1{ozStrdn%| z)j22CACACIHBo;!E!&Ue)7*KcT|nBr zNFF6$i1QHQsXwHkKTrq!-_Rc1WnEYP5y7{4JKKqusTPeKb-SFpg$;#z$LrFxZl_&mKxtMM#XaDYur6Y%roNsW!@vBHvNHe zq$!u4Twa$VL6Dj(_Y%#ArahMXNy?)2r>ui0p(`TIk=Q5F7Mb2-7)#0i>2nS|9vvxRFi>Dzk0~gdH zlq{~Sx*S{Dbw{dj76mC_&*($HX98mLrd=;Q%+QlaitJ4B)(t8ojdDn~IRBLzsL|_7V ziL?VIta40B$FiU|OG;6tmt)F8=UHlC8cgXTRov0!)RHz#8^i~(msm=gBxwu1Hg2bA z@AS_V6RosurB4+TR(a0(7$qn5N*4?p+$8u`381IwPa=@YQf0#U!E9#MaRmS@TaJQ_ zGQJ7hY5S94L^HWDV52?pbk9L$N3)K>ti5`=eK{?AkmBTNzoX46rtqh1eWzo;%#t0_ z7EH-k%r*Y2{lW@Lcp^<0<+*$MG6{jBVbs3HgpI5MYy83d!sl8iA;*FCF}KH3p8{z;9iaA3M;GT~bi-Q@H(JC}+DqV12;9|uMx z5M!IDgJ;gEFr&~Y#ON$s3MK6t7YrG=VhvxRx_aF_Q#iQ2g9?j~auZmLnP)8jJ8(!c zeJdb@3{^5!Gvq@0X(j)shKGu{>dB0Po6KN5X&)*3W7XrN22H_FMkbkYZ&LO%pFyllFU)_ej}ZK9X{ed{j2x)HAq|Oq;mnB+hW5$!+3N@l(?>`4FdrtwdJDnE*)W zziUy4El0&XQ7VKC3oE{qHu3!lgw5%|8n6e9A&N6>{2tXvelNl;`u>)d@%?Od3>G}^ z*t_~v)H{o4)!G@U#lbwbXomR|S{o`C?}vi}U~BsUh07^i$n3&jPBd_UL~1twD2xiN zyfj)}Sk3-mUbhwz5jOLUnYHOqqKXZ3Q1uNdFGy>$HH!Z~Lz_1b%^!B@T`M>5Sk)`s z!+p&i9nJX3*|O%bHCxs$JYc+Q{O6a3HZ1M!c&wvW{vzBCz+dt6HwV$gk?Y)h z>{h)DeZdsZpqxOG$qpY=A&A^5S)|&DzCbHB6HwSb#fS~ zaBk-YWW%|gXnvtOc~5U`NH!+pl|}W%_4Yof6I-*gULmXBu{te!eZGIj+nwc!L^*!m zGhbJ%y>WTEQG`s1T`Ye)rdO9&B&t3*>u^t2@?Uzs$OTcoFwpDkH1qvhnpM7E z`}F2yOR~PUswv)N&uKixe7`g%xGAiLwI|N=>sqv^i$95}Qxny#ttZa!yINh>wP zF_~^hdBVAgbf3x-&+0Q)2TSCkDmvDps4G`bA6`wOv?LPn37#U^o}Z`|Es>s|&Z#=^ zjY@v)(F&bid{CQSW)|n^F1YvVgR^xJUUB*2#h0&WQ!mEPky(taVcn}(De@!bK$lxb#h4T}A+KCZs{8{lI9l0-C#WSccPX7Yxk>U)1(UlD`9geHO zpAcjNb|E|3P96|PQ@aSaJ8lpu|%t`M&d4jMO_X`8UMz^M-2qxejzR-+;Q$%mfL zvgf&1_uybbOF-1KbxW+cN5G=o%T({@c-8TJpZ#FX_J+ajHs<1R#2xim?r=O@Soo;P zK>pc$efkmc&ZE7~aPDT~ciVD<$McV)&+4y?*A1Y5n>c8UbgTtN+CX#Un>pwd5+vmL z0i+1YgOcGKj6K&}`xy;%6?i8a8c5rUdj!@e;Rd@77UbooSvHIAx;oKG5!Bf3NJJgz zof$>}+1f|mXnW()nil7tzu&WG=T7|Vsi{E2X&qggNyNggH~e6nCj zlIFUGW-rYR@d~LB2Gaf^Ad&@&~}ro3(M|q=;z3sXl^{2$%xXUAar~7jO-#{>+fc!HZolhhgP^Y z?!X4bjwB7YefJ}e8XptUEx92VR_Ik>a4CkwJ?N)bxUOUTJV~s;gGo!pTDexI-9Gq0 z1ODK9B6D=mzMgLwfTvWnrVE2iWis#&)nilRcY+XlK(bYjUP z(B^F9DfU#3dv4V~HbaqT*Mi==R>t-YLg$GAtL!zmssj9N1y0khHM-A^tydckA z(FAcy2`cD-DFc0FwPhAt_Lzn2J7Z?S{phiEE;e*`u?M1y?cH52Hg|W8$lvg+q@q<$Y$>c3BIObKdJt-AKV5r-3;2H4Mz52)YlH_UUXypA~y_l1k`+{ z4w1*qW9gq^pv*Jxg^ZzMxs(){B>8YT zpkO2zpi_;a8tpQiM!rG)jCSM(@k|=*IBQ4$KzGmR+Nmqg^NqM>G)+Bw+SHF;+1{C) z-Z8buIpROxcwF4=S-WWV0+cN8p1xq&k~!t^!I~*cr!~}0t1L+^x|?VO{jbvc5o+$! z>zt)nJC7UH80F_lEMy)2bL%YN${x#S@o)Gcg5;3+#N>)R{2feWOdtNlmup#Gp3w4w zc}VaIsKVKZxuJL!$El_=CpkGeoqHCo857lYRo5DY;y0@X#LN}OMb-5y|Zz zU)8@R<#|>%IdyrywnoYGO^r3JwXK!qMTz3XX~^@FOSH$y@!TNXF5DdW(OW0V@5UF7 zFAsseegooSo)M4Bd6k}`Or^w!)vW;k>jwXe z;5ktdj07G&E@(AgyJ)Rk$p9-7RAfq2LLNcnnED15Z z?vlIix&+KX%s)6bc2IHy`7F@>pY%9v3R77ByMa7qNkut&SLll+T7VEdO*VYeHaJ~0 zpCZnzuBNCk9LmqtridvlD`hI($tZ&3q7k+>;mL`zIJ6(sccRG{&QV|pg2Okv=MDCk zl+P-UFOHYbsw|&(#@wFieI8GrfA!$AE6?|jxVoF4JF~gri6i=^pWKU;gai z>X~hA2#!*FRlz$|Tbx=jDa1}RU4z3A?Gn@ZhA7~Tgu|vLp*2lyn%dHAp8?71G-g2a z!3k&aHSwEMn8=-^M4lzLpr-0<>=rY246Ca-PFmw53R7+^>_o7j$r2bBIKFyxWs74a z78GY}dQ3Mg=%@+dbHhPSfe%a`sjG-A6^JZFktqn8&>0|TdOfxRA{^Wi7p^!eF0>g_ zkR%-tKQ$WUGaUPmzS81o#86J#a{^kKR;#T_t*EW^d(cBdK1)u7q9)QMUE3gGfc-i> zFy}z(r8x(||CE21<>lhpS|VOiUKq(O%PT_@#DI&nEgV5+q!Sv!y6JILP!CV&TIq?i zf;y;*m3xP-tTzG;R}3xO-QT}^;ey?B=Ink@yic=Hr2Lpi*0sV$}f$GNX~U3Z4KQp)oizesvF8yg@KSoCJ$&Fxg%#i$;82 zO)D*qR7NZFbG>07`rVmGwWh%pvui*On+9a9N4{&g^7D2XFJF)sG3g_jJo;7r&*OuT{%($~>p(`- z`VNM^+UdoI&P|2dQ0k0)6lp>a6lBw61duemPQdE~ykLDYWmrH|ftgdn%lzsbKg$<* z+>%t%@IomH2Sr*(fkBn6e z;Ps;CN06MD$9<09%T)dM-A{SBCRi-UBhiSOXu&Xw7CF$qQ&Q^@BX9&Yk3!;|BS#b{ zdGCqyjZYh|L3}q>i=9V~z>=5Q%cMzGK7;KP#zd$;k}Ly2AjuNV`QYx_qWN-#*X{G# zcbQ-Avf?5vFV;W_3CvGdL~2{|JPTIte;a9H`Qv1cmL*WFzeT9V>qMsYmx)xUgjX3a zSadr6vbgFG)*nD^7T>GT7Nq7y!e|2KbcArm*{xe?zh-)~tp^F{Vh9bj%8QF+e^CVW zo7s-UGqxCeym(~crl<#vhER?UEvz7uDX@R+(EYB`!u3x+HBwma1Xud81R1<@{GSV3 za^2;j<=ejUm2E?za!+ncq28CpPxEl*qh4Mln3n@)KoUT{0*VUm=bW6`td)8-St~?g z*x4nJtX3|81WepaU};2k!U67eI49(Eh5avq*S_=W>{lgFZwA zls-}`x7WBzd@LOI`l_*$GyC^rN5sX(&PR;lhkd!ao@Xo&A2|ZBAp4tp2eSVeS_2OM ztMRe->ze*R!vmd76KHTt5hCK#`<8-6( zRGKKw6M#fnR>Jl6TAbhFN+e61QnDpV!qzDT-Xt}-chK9zSK?o@{jR&VugNX;pkInL z#TIdeL-^1(1?}I?FDQ2VP%Z27{}H^Sq=#^!Du5gPtsU{sS_K->Zd@f1zr}`k;}b{r z8U2rlFPnHD{dhLNQdPZ6wJoX9d;z-#+RyFRz&vvT{{2`8$-m$(XXy+uE(mA2umQMp zwrJp2{xzE4FGliMSW2?{(vspLSyKbWz(~E$>|EyplX@NK=mf$|iuR-|N=wd#Ml0C* zpGw^YA`YdD`@hruj?muuj`2gx<*nHRv+q(YtL|%{>OX(Y*l_TmB<(|o0Ax08mq9}Q z^56pjSgyJQ!s|hyjXj(R=z&Cmvr6qd?5k~>KNlVuzt1iwl*V^&FZbxB2eWpzoKKR$Ap&F)uquAejWtS-m4=h3sbI$lzen8?qumvW&& zm4pKhj}u*)J={yME>IY8dmuc45G=V$v8hS-Qdt!9@@LAG+4d6R7$)naW3qT7@Q&)9 zqjnR!4>bhev8G{M#;!TV1?P{A?Zid{-vlfcufRLX%&-8kGn<`xA;N}IGh4}9VzURL zE_i~^)-f%BtDU{xc%&?W z`3-Ighs2Z{Ky3jJrND_7NSyKz#MuIVQyMGu`^<&+TIWkU8-&Y>g`G(>SY-J^c6m0r z&5xbJ?<_AIdFsjag{3aP?)b4O47KPf9D$zl&=8Nn<)Lynj==HnNwp#iM?P@*xv5m7 zAmD{r1pWDd7M$D;EHEY}d3{F6#6f5ZnOOo`8dYIz*)Z6RWmU#u_{Ud`z^ku{cV6~~ zA+F{8RBh2kH~?=N-;G$JU(C^$mi7dL-DTt7z!S4z^T39KTBiZjh@@EyWCsuVeil#w z2l_ZKhQwf3uy8^`d_O@OG*hOChwGuvdaSMTVqkqvnw~y?0ROn2(%u1^oeLCXMj=`L zEnq{H6JVxbrWltjd*p+>=3$t8i1LAu50WMgFP`!P#FyUB2aJCe3Zt%_gPxiOboC(g z6HsX$Or$K_i-4MIFf}^+QP-H1IxH!50}KXYc{rqLB}Jjia3$^wqU|qA5XC?u?Qrax zT6rQCO;Tr9eoZLu=j__BVdu!o*-LSTUp#ZxteG=s^&QrWM$SKf`aU899)0!po#JLIFn}o5 z4jsEi+<*Eu18N}f86^%0=ko^-TI36i?bBXK+aKc^d}U?O)y0x{Rb{c$xd@gp#NMh^ zGmStVa3)%TSW*_|-V(dXkv32hgEDLYVozFefd12-jG>uxy_jfZ^rcw ztsi!~80MbPZcf4Q3P8$=b&t-TVOvw!wMJw=^-)mec?#PYcFUYb3 z+7UgZe+{y1HMW|xV;}pH=KH$P|LNG5D9%pm>#4)6(c>CKNi*jL*gFs~QdvRXQJbh}tZXcfh04R_MoG#qiP(eQ2riZ z6BEtfFG-HLkfMSZ;MbX zf7gigb6qQc-$G2&$3pvIWxvC5_H9 z7YK%DKnBGIt|y$B&{}~U-TUq!Hkumu7s{_CU0T?!x&v{B6l@^g;FUFvP!2)b)|FpN z#OTwLA-PDRPl%Q$O~01@3plpWBy>I;TlnYD*20#p$upq>o|LtzRpr>v2?0rp&P}N7 zsvD}PmGKF)`x#=4ry)~eQ$qDUyOBEJIeFL-%K zIxeh%Mg4FOw^c}2v3YpXO4->7v!rq+M+!yr#yxvB;^&T%L}^8&p|7VqmA9j(FRw%W zo$~M5F1ohw*|W`fYWp7XujRGnEgzjdckb*P<{BTLJNu5g{NNL_=UTB}wpb|miZCx( z`y3)pjvL{CvlMC|XunrgU*FPr2sWtI_6F*7*;^ z{FSI}52O7_xUI8;uE?JG;Xf3_;|2d6nQ#1Qx_@50xw&~p$y9xIUETQOQ%h!yiP(Y# z#yL~)kFrh#=g>UILeNTCsu;Tfmg4lLz}Hpk1Y*Z93-p|J}V`+G4r z0gL&CdWXIfW4IqgKto&8|IOT&z{gotecyYZ*)x-5GMP-4NivhkWU}wcWX~i`leTHn zbZ^o^nl{}_Te`5x5?KYSs4Rj--iqJ?Dg`R?qJYY)A|NUvsJOf!pu#JnuM0HIlkb1- z^UP$jwL$&9-&dF=&pgYy=bn4+*^eviad~*K{bS>c)$j8qogd`~D^)5U&RHiL6=|XY ztI_n3&46>mA^hhFd(0kl=mQNW9%y>tgnZu?_Z`>evW=oq9GUyN!N#Aw(DcId@_kd| z^Z5P~`Ht&CsI6Ry`o>ew3y1KuF!pY9D0JVwFO`+PbnksHm6f3!?b6@;dI`P%1`8?) z^VI|ykSsyjY$S3nY$+lgJzmMcYY^8YVC%tvxs=GEGn+2A+rSPj#9R>f0Y|IJYL)H* z>o9|43@(6R9JJ#g2Ci8El9OhZda{v}!(s5w7FxQ}Pj~nr;W)I?5-Mqzn zBCGr6WjEhxNf9E&l6RT2bH(Om=YODo`wITyjt;&;ec(A)vBOd9S~c+=UvcfVpZUs- zHsyq&qX)8y%A6vYQ`^TwgfX;C66()CBbZw`h;#=*X?e0_zw+;r@>xj zvnIe@ZZcU0;Vw6gIJn7TTIu&O*51<87_J3(3HW;b%W|_Zveaa1@@@B%^6nz_7Ck1Q zvq1t4MKySxs|Ny*uR=9c(?N3i5XPR?%2O(_~|JCjm{%~V@ZMfD315HPtM^z$0% zsE(pM&?y;OBIv_xQq15^sGw>zLGnOS+Xxm;TOYP<8CA|&S$|O>XO-oJbwzbxzu6vl zaw2-4gm}nim5eu@^U_|Zi(NIk?t*F;aJ(zilo`Hw?d6LZxw3? zhDP_CvoBCkTwH+H$)2k=@HN*RyXv6&=l+cwdJZpJ>GO849Xh;I{nF^6US3{Ra-_7P zvh;9Cl^kEjX{Hjkx`3w+X^4UN4PU88>=ra zT3OS1yt$^arkUuV1N!QFSVuN+aFQdi$dMY(c|)HG^kmv8H>EFoLnmg+P^(KcUQd7`CW~ef@5AlAlK~fKZMs_`58tsal z3(FfUgw`iWY9PDcus;(Ytk?$Q1ix)REbanVMv;64pl$xuTw4iRytQ#%#oG4TD{I@^Yw7GO>8-Y1^7J^oRI*04RbpmC3D`1Uo?s@9 z1P3XtSu8_^BA?(Qfd$tBG0_4)q(lrBF+(&x{#cox&sk%*rYgTOAHh#$rHCxg@whdd z*yqJbkgD-a!wWP<$?oNr6|>h)!-o1+6c>b%p@a49EuE3(`ufbAoXq+e$an`Bn8L>? zer{vs;>=r*9(^3akJ+?llK4SUC->+AiZ#m1KuAX-)D&#dZ_{}*|>9iVo4(Y->%;FD{q@CwZ^(* zr?ooO)#gPPJ&fa4RAl}(=$!H(8s%J`sTbO;{x|vz#TzT)byi-wX!Ytv^eU*RCIb6&6-k7Z%c10KNNp{#kxR{8TY35Dmp5XyuGQA{LQWE`Q&J4n2Po zFS3wQJo!n{g{`odT_UsO|W5HlbI@{#^_Yd8F z|2}cSz#DH2Oxyr?yK(csV%~CCA=ycNFd&id!XijS(8POkXh$ns!A2-6TZMw@X?7dS z;W^XehGrgX@eSXAsPc31et(%-uLLu;27#Npvz%8j$;mUv`r z<`iat9XGHe@dFH<8bAUNmCyS1068#G`4lMF06aOQ6m@aLmIq&o8$vQ!yV4;Px5S?d3qC_sKD@S;+DS~tYO1TIjK?4yw@y28RUST*~SX5s>aeDZODcOlt zZRj38i2mQ;vI@T?(d|xjQ0u&=iZ?2nB{X|ET747qYqq>_4bhYzA*@7Fr9J1h zw4B$obWe+VwS8&0c}r?_a@WRh@^t%>hVW8b#|3?F^dITyIK1Lwb;4cRT2V6NAHvyiS;K%rLUk5g!iIKfY@LF%UB4Z1WNLci;oDn8}6gMEH)` zXYqmIkgEkvf}uatj6>ejc4ev^SV!^;a*p`gS2n-5?bQ|imo4e98EQbEIy9kMap{sJ zm-6(V;g_HNz30-t#-X~pp~l9IwLLwT^!HubqrvL~&?VwG=r~r!Hb>4W%oP@yeFATk zFp_N<-q6b_=L5opWh?>7dzPzXyiZy*D9xS(4Fg;0^>XGd^Ol8>@JMklq%0y_3;oP^ z7DZSiri{dUB3Y>cYVqz`KsEGX7uG5qDggxx)&0Zj73JkCsvC!ryAtYh%UfH^bL$ei zl80Oa4?_Ow7wtgSj7Sq@blIq6mH>Lr-=f8-QYmhMs?;8t%zRT&x3 zXq@eGoJGj-*b~{A&n-5vaX8!J2I@d;5~X%TnY#gw17uwwN(oA`DPbqGSuE1$PbnTY z%L*EBb!BO>x5$f}w}K!@d)lBA&pv4Be9)`_F_CKCv46LrtF~mv!04K5GIDb=uAOiE ztrdmUrmH^q!H2TkXpZFVua|8#(`F|Wd{-t3WjSi_fJ1f zysxCUx~jLNv9+qIb>dS^d_~i`igg5GjDHi{8Ei44$B}9ckHgp${1jLz;hzzGEWyo4 zmBB1AKq;_6^6D9Y>il^+h&p4A2X3>*kYlhV#&8Pzccw>}%vg{{Y%Rw4tp!_(M6uaK zTZ)MpkOu`s$F`Pu%wqLT;Wf6k%m9~zNMKTMli)WKufsq1Z2yBFjm$*r=FY*B-?MFf z1)%GAt<+)9VJjp3K$Di;4(uZ7VvNqx_C(~e;Two>@qvLcPzr68#Hd+lH($Bh+1 z8%3Vao0;xOEq9m42yNaK0RfK;9@5?sz$`*Wb!*ng8A##I&Q+ad!C-k=FlXJPr{Li$6F8xmUb!wk&b1h z`D?yBb7Y&_HYWOJpg~+;@Mw}%6X2~Fj%@N}rzl9zTTYUT6{dPJC*gl-#FqjLjgdrc+3bm8 zKbR5hnvA=&A)-YB8kq&{LF}^N)p04d4#a)LGiI#S85RVX4@iDNe>jarAJ`bSB^V5L zTisB^U|Y9n1Jcy^cXU3jHuDu9NXv3MGt)0o9~kZ$DR}yO8Fk&YfFgcO0iwj*43`7U zPaD*9actUXG&dnSeoY@uz>cKEbWc(uaFif@miC!rLD)lVFeH-bzKW@#Tz*-9rvo_{ zR(quHvddOgCoXL!UzL*fEb!R z!=8?uqbQ~mXJYznj*}?yTrsy-B%XZc@O%)yC%-GYCB7@z*CO_!tW3pfE9bD`LnluG z+i!y`Yqi=AGBoO&H4$>q+C)IkHN!7pHJPk~3@)!N@D5o`2uEqB8(3^uTL0V~um!-9 z&dR_c3k&jmZkN5nQ86#ZpxZ}P2W;nNpeA6OgE#}Vm53kl_%>{JMx%KU z3)DCQS}_|}CLqS1{Dl4-7b+6i>Jn>3h!6>I(#$L)aVW>(qA`3bKqCKh0rn4k_LUg?L zY2!uED8;_urH$s=nAk4CJwoDo(>2%R<*i=Bc`qom``Cw<~gPh5Y z1=`ouSE~=)c%ynm-&tTci<7hbu!xpX{#a=sgMz;xk3hNryR~RS7OYNi@*zuJQH;Yj zlNEGmTv1qBT zQ{VWbDY7FH*&Z?d54ZtzV1zSPVkecICc*F!FkzaruQfJL2$mT8cXY3t?nT z;AT4#`^~f^BX)sO{mJ!gq1rBqiDt8woSo=N^rSk?4zt6Fz};jizoCl;4jg2?9?%y{^+BR?!BWzdgUta;IF^h_$(G*8dcWC-V|jB!VWtFi4(F?1-k$G_)h>tOc zVBJI1iYQk%sXyj5{CFv#x%1efW1m`b$L))6JJ!KVd3}AUx}_w1>m7G2I(Dq*j@!F# zKZfUjAJ3nd^ZC|U&xfU4e?ECP$sqwSf)p*I!%$pOUthwH^BVQXYR{5mw=KT?jwPQ0 z6dmCberG9KVxw1e&oMmtj-F%37Tv*^b@GdN{{47<5_qWxku-kD-$kJLDmEG!Vccdm zqJbV)cECfah~z|JHwpWWBu4_0QBs_ylpS#XGUs}R=$Xwfjz0!CtCCL2l45C5BZILo z&T1mjYc;_)n3xJg#s;DxGr$pb=b^|uELwL>0N+pyM;?(!kua88(NbR`x`5!)iT~KU z_qM%z`6YWFUiRwVy>H0Ba6rA5zoBM!v{dk0N(t*DCH4Hyv9U4r1zt+;_$ogz|Co0M zdoRTiB~Wc6#Lt_J2<(#$c4Dmq$^NTo{z%z23}=%a*#eW9Uje&yY2D5Eu5Wzy^~dgd z_E~Ymz{CXuc-L%bqm!?~5A`xAtUc0_!YxZF67d=oA(5?q5i@}8fNTudj?wUq!_Z9y z=$IJ+W{7vu|0T=Pp+E)<3DV&uH>DagMPu%}>86|3;dRrezx~{&Z{jx(4%~U?z~I2; zuf2xdhO<%R;{R2=i|2PmI`A}@4vDJ4Nhz=u;t>26%&>!ME}>^GK8#XqM-n@hNE!na z8Vo5;r=9AT0)jwTi9eh_}DtJS(9bAGbyPOF^xGZi3Sv)30@tAEk^B_LVwvm)HVKduS zR#lSgXw$|G>sAe{=v&&;wJ6fo)DW(#9IhHJFD)$aO30P zlxHHM0^;-tl{6y?lIvCplwXQkwWHZf(Lh*`C>TsMIhCrvNZYSuJ-C61-_hX>2EE?k z=l1UXKnHyZ^2@bP^-2Aw4p{GNJLG#^_uih`y6*0}jt+XS?b+Kc^7Yf-Jsj3Acr|*V z_RFTO+S;ze^wveU!TYp7fo9ozyBaY=zrbqDq)5gLL=qS>v0)j-@)U#>ECop*?jBny zRAuaHkU(NxJ|Pm5H8=$^i&Wboh>kmQ$TUmeUGE6KNbk=%5Vhz6P!m=}ItKEHc=eJO)M!dT%3@G$%!x{j^L$+|KDt z!^)trgy}vIDSt#`1k2(aAJ=tGMF&cEN3bf+-9aE(k1H6y$BoR*ezYQ34M=45BJyQHdW!{Ju~8T`_WKp;cC zIwSB_<@$r29S2L&%+BOWkGHq9b-2EMxV5y`>#0n3n$wC8b#xwF54WhIk83Y>@BsF0 z8SntT5cT7%O6br8GbcV^Q?8WlLk@mX?;0rlye=9SJrX6~P_-9Bv+MZ5?ga-n#=C zCu#h6o0>0uT#+-y+|U@}p{MCO8}w&{_snpDa{e|s&bPt>=^%fcq$J1;BqVC|LP8=u z{BiUWqXQ#m`LA-8?KDhWYeJ0YcI4~1Sz5%Egn}~5Bx{8Dd4W4yfX5*{9R^tnZX&9} zr#Vp+j8;_e%F0kljzor(n6I-EdtoBS27<1v0#3#-e^V8@qmjfM z^_#l05f37N#Z*jlC;4ijEx|wy9)UD$TsAg4w%Lf352_`Aya^z6zL4g04`cN;Rh8u> zK4gP=I@3G!aAt=y4FS!Pv&lVqhClZGsKI43eY6XWV4mi*o!E3%y_af%{!Qi_q(aNA zlhtOUxwzP%;fkV=5|d)2TryZJgarvCW(bE!Ac3HW#3_iOD~l1sT!k3s;<`}KpPS>Q zJqarqwU)K?k&ptjTQnH~3<+IuA^d38x0nP20b6&;NZE&%1_Cd5vwj-Dzn>;Lyf5S} zsVl8%t7~uf`u$$ND<#qCOiXb&oe9NU2?X-Q- zMF|Ne7{pDc1lVd}u3S-C3Z@kvNtsqlb8>%U z0oIP}%P!+BcwKkqX;v?8d8q~e)A~b?G5GP?<@lRf4_WPKmgmF(!ZE-GV5Nh}8kR`p z@l$X=J`mlHI?IUeZ*3_93Kn&=^tARMFuu90c{&0dU_c1hSU3Wv*=J3%Aa_(eUI;?I z9<$iOj6~*Iyv~Z9!!ZMHRexC%jJKVgFyLyq05a5cke-T-l`5$HncP!>(AxJ8IqM@s+ z;j+sq_~VMRV&`JMqP?QL?a2Mv?z9}BL$03^ef?~YY@LA-hlY7{wTY{T$<*#7-%D9+lp5N!A~OWawPP}D{Ty3lZ-(ZwR7iuT5uGU-#BfsC0E zPcdBJ9$Ft$EwozlkwvA8nxQ;aluxG$J>qHMOK2YV+d^>RoX<0NR_x5_uro+n(eTvB zI+0g&USxNT<{z8Bj%HKI+1zA=vfibO7j;Hjno1i>8)h!1_sx3G;!9fy`h0V{+0Kf* zIrENUbf&GfR))B`h6*jNZaVVb7nQY^w$gT0K7;qXZz`LiV=f3iajM-y*v_o@JI+uG z40++NAvY2lXqr12Wmd>I3SEgK7?ONTD1!if8c~ppA^Qj^(wmV0(@425Wb;E2Fv@)K za<7>peIcdk`9WlPK@bS?cqXGpnd>$VFU7k@`RDSok9Jsln_ zZs}Rtw)OC#k-9@mo?8XJ6qR|MNGuzStV2{N)#0#MjaG|s42rpxLUY87eLYS$)=&I-W~jvPXODj>rZHc*01=0OFkXXJKX7JAhj?=m z-pG$*hZg`9K7j*NcYs<1GY`y7oI36)CL~~DPS8BwprEcW$Or|%9jOkO7-3%Y$fohp zj1CkmtBJa8zD$Xg$>#M-kYu@-XOZ_t*j+`5iAApG5GT33$5G#L^=HZ zSwL2_U^x`CK?>X5f&z?Cn&8YYg33RwQaSiK4I9ZtI72%bD--7qMqsC(UXh$uB;{3+ z#*7qYI9JHXsim5e*=3GGQMx9XxCJhTL;XXtBC4>U77^4=9V%M40hE-Y0B z15z_V35sEi1_}^m!s0Nd1o?RgBMAC^D9~p_z6cs>r+HAItxl`X7UE=e0I%>!vkKA^ z0yH17U^1k_$yKZVXeiPl`qdF0a``M)zf-j=Us~5$5<1wkdU(~)`t3vgk+x-3#rqep zIJZx^`tF2)`t;~aL!U%#1QgNYw&M2c!o~z&>iK=E&a;2z%L#iov@Y&xC|hLDP2JJm zzY`gR7^fNW=vX zdRCv8nq7`OL?p7*A+fP8i2CPL6U}4Pt}Da`4AW2_NcNf@glP%t7tf%0fj(W<=w!39)x2h%E)xKToTHMAONlo1xG zslq5tR$1BoY@?h$=QAf={o3pS_pOKyxE zT&&om;X;}C1^CJq@Re$0fp)P?kwMgrgHIT2#F1cCxXsAXyo&f5l|*XjOSUH|2r?KJ zi9&Li)|T2D&LV9sU9DZ{H_}|&48^sqC`8G}l$cG0)mSyJc1C$l7>4$Fc#ouZ?3;1C zC>Z5M{^%E;vg6{&FMk>3O!wYf*Va+Xf8fjCf9}bX^83Ky)6ZOW71)%xS$^yrSyQEc zY2S#>vj(>gG&j_@y4ynfH*l{3Zs=-j>(btdMhxRu>UdekRB74GJMXqbGL0+ zw`SAYO)L6(S1(_^bV<+Rj`p^ea9tHLLp4(ec(g;}Y#CpcF%xI+19uC;xofy;hL4F^9dH%gH1)fqd@zcpW zWUf^RR_sf@s-^Pxp73MyE11gUHzEddMC*!`q`PkQ~}g4RqTl=_cKT~uBz=Shd0q$n)ohk!BmGNpDku9 z**WalNW$8soyht(ITE8;{~b1LH?+H>h!juei4LBKbZW}{M>&>g>c2h3Y;qbIvj0<) zT#CcNMq)>(1VHoI?_nCY!{XK$yFSJ2#4?q|0P0q+>glF}0Pw$_v-+In%eq(gtn7$1 zH-=%JEiDNyE?7LXNPrt;YF!9^yw=bG=2#4d5%7x+u1>OIlF;PL>yOMuzM@b zs>PKWoz?ziR7ljOHPIq?dYVXe@lYVL;Ii75{*XVMw zk@&+j#fuksL(_1I9gMj#?p9fjOmhYi4^@8X8@uB0p`AOl3BTs5D{dIO;o=JqU4HoT z^Y-jGxbxtakxfGz)~{LBx4e6M&-ODDLGHhe2>xj*iKmz!%p!`Z8WiIVvwnIB62UfY1TA7 z`2-(Vi%-rdGQxk(U!Hqz{xtBb>6J#dV4vdZ=O^#ucZ;`SZSB$Wqf>!48B0uofRus2 z8WdnCL+UtaB&r7*!v}zpV=zptn3j!7f-!;^FcPQw`62PyiF4K}i`1(J)axOCKhFZf zAL{9y8+PJP+!xuDbYna^g{1^ipiYxo5rj$Bk1T*tjY|oH z;Y0Mh$DbcEZZueokM926?{__pS^}r;35fnlqs{o1`g8T?e==AMlT4^zN8aCK$h*r& z?mR3$aNR?PMrsKjO<6NYyukD$f&>~gISq}&NSjlT2Dl;!FHf*Ah-^w+8}JAHrG6VA zK?ZDfJ=7U0wH*%Eqb&~(q#;3otUr)qq1Pe01ZW5!s;Cw=rM{}VPO*vVibID6w;63p zLC3y*kq`<2Z5vdt7}_^bQgV0WGOKl2^|Dlf4*z1nAB*b( z

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≦fXo=WUuIA1w6wc ze~u{Wb9+aAvsl~X_C7Z%y?9g7;~Om*Y2NJVomZXf_|uD${+1m5eV$(Co5AnzN_)iO zk$!cK{^^8XB#Pf|ReI_g>7U8bAN2I%hmHA_(u)Ts{h=KF;e=lO6#e&@)F53$FBT-#1w4#e0(;y$~(rx=ejI`&Up%eY&ep_joO?K6-ez zZ;d?Mke@c%aGy@gbI z$L}*w?_iv6ru7>UdX)z)CFBucK4MkIU7z>Mf*0FI{6)F)w4$V?KO%liwtvCJ4iDcg z_;DVu;}uA3opm|*k{o|O|GA?ff6V`+eY%R!`k5wvrmK&BobZjlK387X$m+d`=PH@I z1;!toyoHn8@fYEq2fJvuUK2j-C3r;2RiWTX`D8x+2T$P{G9~{PKo*R{csXgi_yk`1;L;*n zu4#}j9#7NcqBqxpaA`dSl2aGmRf-4W=y;mTgpbg>zPP&F$jOQ>C&$L>91@1kp|t3N z?%3F$zRbhesr_e;#?JhVV~%+OK(AA~5aQvF4JLDA#UKi02W>~F;z{`Rx8t1_?57GGZHnfb25qGv4sLJr1xj<;J* zqciQp$t?S)+RpMgw@K(#%d_;qaP;19yxs5vskO00GBWE{AL;y5%O`g0SgJmL6Tsx` z@K9HueoMdv!y5FQL1)mnE4|<-U#ym#UG;WW`YSra{^E>6@syzG5%C3u1Wx3UnrAxa z$RF!G(Ca)SKR=%2V$2)T8gFTS=EhQSe^Q+4g*ojYT|MH4inCsg9fIx`{H#%kbBo-R z+L&t;a=P?sl?V5yls5UPU&L5=U7$ROk=Al7vpB+94p9T9WKJlZ zuSe#nXszbEdW1f|PiRlHz^~sij&6Q4rbXG}om=?y^OQMN#611nq^(VBlh?nGJ)cAa zk18M9O=+DmpCtYEwgMzlm+BDf!cNA?MkAMA0v-Ylb}7=wWKS?7)jl1Ol>aizg<7@I zm=zga`JTh64cO+!&i9eE`2NA3s|9!3tkNf?$lr}iddz)nk8vr#H)VPs>Xh~)O|8=! zc&mYFMh0El*?-B`(|Dy34fgc3Zl0bidhI`aTQXwmS+dq - + + UIAppFonts + + JetBrainsMono-Regular.ttf + JetBrainsMono-Medium.ttf + JetBrainsMono-SemiBold.ttf + JetBrainsMono-Bold.ttf + CFBundleURLTypes diff --git a/apps/TerminalX/iOS/AppRouter.swift b/apps/TerminalX/iOS/AppRouter.swift new file mode 100644 index 0000000..fac341a --- /dev/null +++ b/apps/TerminalX/iOS/AppRouter.swift @@ -0,0 +1,97 @@ +import SwiftUI + +/// 顶层去处。设计定稿只有两处落点:首页(主机管理)与终端。 +/// +/// 关键:路由与**会话生命周期解耦** —— 回首页/最小化只改 route,会话照跑 +/// (会话 surface 由 `SessionCanvas` 常驻挂载,见 ContentView)。 +enum AppRoute: Equatable { + case home + case terminal(UUID) + + var sessionID: UUID? { + if case .terminal(let id) = self { return id } + return nil + } +} + +/// 侧边栏三态。同一条栏,元素不增不减,只是展示密度不同。 +enum SidebarState: Equatable { + /// 56pt 轨道:只显图标与两字缩写 + case collapsed + /// 288pt 面板**盖住**轨道,右侧终端压遮罩;点终端任意处收起。终端不 reflow、不触发 PTY resize。 + case overlay + /// 288pt 钉在左侧:遮罩消失,终端变窄。**只在钉住/解除时各 resize 一次**。 + case pinned + + var isExpanded: Bool { self != .collapsed } + + /// 该状态下侧边栏占据的布局宽度(overlay 是浮层,不占位)。 + var layoutWidth: CGFloat { + switch self { + case .collapsed, .overlay: TX.Layout.railWidth + case .pinned: TX.Layout.sidebarWidth + } + } +} + +/// 终端页上的模态浮层。放在 router 而不是视图 `@State`:⌘W/⌘D 快捷键与无头 URL 驱动 +/// 都要能打开它,视图内部状态触达不到。 +enum AppModal: Equatable { + /// 关闭确认(3c) + case closeConfirm + /// 分屏菜单(3b)。`vertical` = 竖分(⌘D)/ 横分(⌘⇧D) + case splitMenu(vertical: Bool) +} + +/// 导航状态持有者。视图只读它、只经方法改它。 +@MainActor +final class AppRouter: ObservableObject { + @Published var route: AppRoute = .home + @Published var sidebar: SidebarState = .collapsed + /// 当前模态浮层(关闭确认 / 分屏菜单)。 + @Published var modal: AppModal? + + /// 状态切换动效:设计定稿 200–260ms easeOut(侧边栏浮层从左滑入 + 遮罩淡入)。 + /// 所有侧边栏状态变更都经这里,键盘快捷键与点击拿到同一条曲线。 + static let sidebarAnimation = Animation.easeOut(duration: 0.24) + + private func animateSidebar(_ change: () -> Void) { + withAnimation(Self.sidebarAnimation, change) + } + + /// 进入某会话(点会话卡 / 侧边栏会话行 / 新建连接后)。恢复焦点由会话自身持有。 + func enter(_ sessionID: UUID) { + route = .terminal(sessionID) + } + + /// 回首页:会话完全不动(⌘⇧H / 侧边栏首页行)。 + func goHome() { + modal = nil + route = .home + if sidebar == .overlay { animateSidebar { sidebar = .collapsed } } + } + + /// 开合侧边栏(⌘\ / 开合键)。折叠 ⇄ 遮罩;Pin 态视作展开,收起回折叠。 + func toggleSidebar() { + animateSidebar { + switch sidebar { + case .collapsed: sidebar = .overlay + case .overlay, .pinned: sidebar = .collapsed + } + } + } + + /// Pin 切换(只在展开态出现)。钉住/解除各触发一次终端 resize。 + func togglePin() { + animateSidebar { sidebar = sidebar == .pinned ? .overlay : .pinned } + } + + /// 打开/关闭模态浮层(红点、⌘W、⌘D、⌘⇧D 与无头 URL 共用同一入口)。 + func present(_ modal: AppModal) { self.modal = modal } + func dismissModal() { modal = nil } + + /// 点终端任意处:遮罩态收起(Pin 态不受影响)。 + func dismissOverlaySidebar() { + if sidebar == .overlay { animateSidebar { sidebar = .collapsed } } + } +} diff --git a/apps/TerminalX/iOS/ContentView.swift b/apps/TerminalX/iOS/ContentView.swift index 43debc4..376b4a1 100644 --- a/apps/TerminalX/iOS/ContentView.swift +++ b/apps/TerminalX/iOS/ContentView.swift @@ -4,8 +4,11 @@ import GhosttyTerminal import TXCore struct ContentView: View { - @StateObject private var model = SSHTerminalModel() + @StateObject private var manager = SessionManager() + @StateObject private var router = AppRouter() @StateObject private var hostStore = HostStore() + @StateObject private var credStore = FileCredentialStore() + @StateObject private var tailscaleStore = TailscaleStore() @StateObject private var theme = TXThemeManager() @Environment(\.scenePhase) private var scenePhase @@ -15,426 +18,381 @@ struct ContentView: View { if let tsnetKey = UserDefaults.standard.string(forKey: "txTsnetKey"), !tsnetKey.isEmpty, (UserDefaults.standard.string(forKey: "txHost") ?? "").isEmpty { TsnetProbeView(authKey: tsnetKey) - } else if model.showsTerminal { - TerminalScreen(model: model) } else { - HomeView(model: model, store: hostStore) + RootView(manager: manager, router: router, hostStore: hostStore, + credStore: credStore, tailscaleStore: tailscaleStore) } } .environmentObject(theme) .preferredColorScheme(theme.palette.isDark ? .dark : .light) .onChange(of: theme.palette.id) { _, _ in - // 主题切换:app 视图经 palette 响应式更新;这里把终端配色同步推给已连接的 surface。 - model.applyTerminalTheme(theme.palette.terminalTheme) + // 主题切换:app 视图经 palette 响应式更新;这里把终端配色同步推给所有会话的 surface。 + manager.applyTerminalTheme(theme.palette.terminalTheme) + } + .onAppear { + manager.bind(credentials: credStore, tailscale: tailscaleStore) + #if DEBUG + UIPreviewFixture.forceLandscapeIfRequested() + // 无头 UI 验证:造假会话+假主机供截图对拍(不建连接、不写盘),与 -txHost 真实连接互斥。 + if UIPreviewFixture.isEnabled { + hostStore.injectFixtureHosts(UIPreviewFixture.hosts) + tailscaleStore.injectFixtureConnections(UIPreviewFixture.tailnets) + if let s = manager.spawnFixtureSessions() { router.enter(s.id) } + return + } + #endif + #if DEBUG + // 无头验证**真实的「首页点主机」路径**(`-txOpenAsHost 1`):把启动参数造成一台主机塞进 + // 内存主机表,再走 manager.open(host:) —— 即探测 tmux → 弹选择器 → 选完才进终端。 + // 与 `-txHost` 的老路径(autoConnectIfConfigured:直连 + 立刻进终端)互斥。 + if UserDefaults.standard.bool(forKey: "txOpenAsHost"), + let h = UIPreviewFixture.hostFromLaunchArgs() { + hostStore.injectFixtureHosts([h]) + manager.open(host: h) + return + } + #endif + // 无头验证:启动参数带 -txHost 时建会话并直接进终端(老路径,保留给回归脚本)。 + if let s = manager.autoConnectIfConfigured() { router.enter(s.id) } } - .onAppear { model.autoConnectIfConfigured() } .onChange(of: scenePhase) { _, phase in switch phase { - case .background: model.enterBackground() - case .active: model.enterForeground() + case .background: manager.enterBackground() + case .active: manager.enterForeground() default: break } } + #if DEBUG + // 无头验证:terminalx://ui/<动作> 驱动导航(见 UIPreviewFixture.handle)。 + // 非 ui/ 前缀的 URL(如 terminalx://resume)只用于把 app 拉回前台,无需处理。 + .onOpenURL { url in + guard url.host == "ui" else { return } + let parts = url.pathComponents.filter { $0 != "/" } + _ = UIPreviewFixture.handle(uiCommand: parts, manager: manager, router: router) + } + #endif + } +} + +/// 顶层壳:会话画布常驻在底,首页盖在其上。 +/// +/// **为什么不用 fullScreenCover**:会话必须在离开终端后继续跑并保留内容,且「最小化缩回首页卡片」 +/// 需要 matchedGeometry —— 那要求两端在同一视图层级,独立 presentation 层做不到。 +struct RootView: View { + @ObservedObject var manager: SessionManager + @ObservedObject var router: AppRouter + @ObservedObject var hostStore: HostStore + @ObservedObject var credStore: FileCredentialStore + @ObservedObject var tailscaleStore: TailscaleStore + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + ZStack { + SessionCanvas(manager: manager, router: router, + hostStore: hostStore, credStore: credStore, tailscaleStore: tailscaleStore) + if router.route == .home { + HomeView(manager: manager, router: router, hostStore: hostStore, + credStore: credStore, tailscaleStore: tailscaleStore) + .transition(.opacity) + } + } + .background(p.bgDeep.ignoresSafeArea()) + // 沉浸只在终端页:设计稿 1b/3a 顶部就是胶囊/logo(无状态栏),1e 首页与 1g 设置有状态栏。 + .statusBarHidden(router.route != .home) + // 连接流程的一部分:探测出 tmux 会话后在**首页上**弹选择器,选完才进终端。 + // 挂在 RootView 而不是终端页 —— 用户点主机时还没进终端。 + .sheet(isPresented: Binding( + get: { manager.sessionAwaitingChoice != nil }, + set: { if !$0 { manager.sessionAwaitingChoice?.useNativeTerminal() } } + )) { + if let s = manager.sessionAwaitingChoice, let choice = s.pendingSessionChoice { + SessionPickerSheet( + host: s.displayName, + choice: choice, + onAttach: { s.attachTmuxSession($0) }, + onCreate: { s.createTmuxSession($0) }, + onNative: { s.useNativeTerminal() }) + .environmentObject(theme) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.hidden) + } + } + // 会话就绪(选择完成 / 无 tmux / 失败)→ 进终端页。 + .onChange(of: manager.enterRequest) { _, id in + guard let id else { return } + manager.enterRequest = nil + router.enter(id) + } + // 兜底:路由指向的会话已不存在(被关闭/清理)→ 回首页,而不是停在空画布上黑屏。 + .onChange(of: manager.sessions.count) { _, _ in + if let id = router.route.sessionID, manager.session(id) == nil { router.goHome() } + } + } +} + +/// 所有会话的终端视图**常驻挂载**,只有当前路由指向的那条可见。 +/// +/// 这是"回首页/切会话内容不丢"的根:GhosttyKit 的 surface 一旦从视图树摘除就释放 grid, +/// 故沿用 tmux 切 tab 已验证的手法——ZStack 全挂载 + opacity 切换 + 关掉非活动者的命中测试。 +/// 画布尺寸恒为全屏,故切换路由**不会触发 PTY resize**。 +struct SessionCanvas: View { + @ObservedObject var manager: SessionManager + @ObservedObject var router: AppRouter + @ObservedObject var hostStore: HostStore + @ObservedObject var credStore: FileCredentialStore + @ObservedObject var tailscaleStore: TailscaleStore + + var body: some View { + ZStack { + ForEach(manager.sessions) { session in + let visible = router.route.sessionID == session.id + TerminalScreen(session: session, manager: manager, router: router, + tailscaleStore: tailscaleStore) + .opacity(visible ? 1 : 0) + // 离开终端时轻微缩小淡出,观感上"缩回首页卡片"。 + // `scaleEffect` 只是渲染变换、不改 frame → **不触发 PTY resize**, + // 故不能用 frame 动画替代(那会让 tmux 反复重排)。 + .scaleEffect(visible ? 1 : 0.97) + .animation(AppRouter.sidebarAnimation, value: visible) + .allowsHitTesting(visible) + } + } + } +} + +/// 终端主界面 · 沉浸轨道(设计稿 `1b` + `3a`)。 +/// +/// 布局:左 56pt 轨道(或 Pin 态的 288pt 面板)+ 右舞台。舞台顶部留 86pt 预留带给浮起胶囊, +/// 底部 34pt 提示条。遮罩态的面板是**浮层**,盖住轨道、不参与布局 → 终端不 reflow、不 resize; +/// 只有 Pin 切换才改变舞台宽度(各 resize 一次,符合设计约束)。 +struct TerminalScreen: View { + @ObservedObject var session: TerminalSession + @ObservedObject var manager: SessionManager + @ObservedObject var router: AppRouter + @ObservedObject var tailscaleStore: TailscaleStore + @EnvironmentObject var theme: TXThemeManager + @State private var sidebarSearch = "" + /// 「安装并启用 tmux」引导弹层。 + @State private var showTmuxInstall = false + private var p: TXPalette { theme.palette } + + var body: some View { + HStack(spacing: 0) { + if router.sidebar == .pinned { + sidebarPanel + } else { + RailView(manager: manager, router: router, session: session, + onNewHost: goHomeToAddHost, onSettings: goHomeToAddHost) + } + stage + } + .background(p.bgDeep.ignoresSafeArea()) + // 沉浸:内容顶到屏幕边缘(设计稿 1b/3a 顶部就是胶囊/logo)。状态栏的显隐归 RootView + // 统一控制 —— 本视图常驻挂载(SessionCanvas),在这里 hide 会连首页一起隐藏。 + .ignoresSafeArea(edges: .top) + // 遮罩态:面板浮在最上层盖住轨道,其余压 50% 遮罩,点终端任意处收起。 + // 动效:遮罩淡入 + 面板从左滑入(200–260ms easeOut,见 AppRouter 的动画包裹)。 + .overlay(alignment: .leading) { + if router.sidebar == .overlay { + ZStack(alignment: .leading) { + Color(hex: 0x0A0B12).opacity(0.5) + .ignoresSafeArea() + .contentShape(Rectangle()) + .onTapGesture { router.dismissOverlaySidebar() } + .transition(.opacity) + sidebarPanel + .transition(.move(edge: .leading)) + } + // 浮层要顶到屏幕边缘(面板是全高的),否则被 safe area 推低。 + .ignoresSafeArea() + } + } + // 关闭确认(3c)与分屏菜单(3b):自绘浮层,不用系统 alert/sheet —— 设计给的是精确尺寸的卡片。 + .overlay { + switch router.modal { + case .closeConfirm: + CloseConfirmDialog( + session: session, + onDetach: { router.dismissModal(); closeSession(kill: false) }, + onKill: { router.dismissModal(); closeSession(kill: true) }, + onCancel: { router.dismissModal() }) + case .splitMenu(let vertical): + SplitMenuView(session: session, vertical: vertical, + onPick: { target in + router.dismissModal() + performSplit(target, vertical: vertical) + }, + onCancel: { router.dismissModal() }) + case nil: + EmptyView() + } + if showTmuxInstall { + TmuxInstallSheet( + onRun: { cmd in + showTmuxInstall = false + session.runInTerminal(cmd) + }, + onCancel: { showTmuxInstall = false }) + } + } + // host key 告警挂在会话上(多会话各自一份):安全告警即使会话不在前台也应弹出。 .alert("⚠️ Host Key 已变化", isPresented: Binding( - get: { model.hostKeyMismatch != nil }, - set: { if !$0 { model.dismissHostKeyMismatch() } } - ), presenting: model.hostKeyMismatch) { _ in - Button("信任新密钥", role: .destructive) { model.trustNewHostKey() } - Button("取消", role: .cancel) { model.dismissHostKeyMismatch() } + get: { session.hostKeyMismatch != nil }, + set: { if !$0 { session.dismissHostKeyMismatch() } } + ), presenting: session.hostKeyMismatch) { _ in + Button("信任新密钥", role: .destructive) { session.trustNewHostKey() } + Button("取消", role: .cancel) { session.dismissHostKeyMismatch() } } message: { info in Text("\(info.egress)/\(info.host):\(info.port) 的 host key 与已记住的不同,可能是中间人攻击。\n\n已记住:\(info.storedFp)\n本次:\(info.presentedFp)") } } -} -/// 主屏:主机列表(对标 Moshi)。卡片式主机 + 右下 FAB 加主机 + 右上设置入口。 -struct HomeView: View { - @ObservedObject var model: SSHTerminalModel - @ObservedObject var store: HostStore - @EnvironmentObject var theme: TXThemeManager - @State private var showAdd = false - @State private var showSettings = false + private var sidebarPanel: SidebarPanel { + SidebarPanel(manager: manager, router: router, session: session, + tailscaleStore: tailscaleStore, + onNewHost: goHomeToAddHost, onSettings: goHomeToAddHost, + search: $sidebarSearch) + } - private var p: TXPalette { theme.palette } + /// 轨道 + 号 / 设置齿轮:首版回首页(那里有「新建连接」与设置入口)。 + /// 设计的「就地弹主机选择菜单」属 UI-4 的分屏/新建菜单一族,一起做。 + private func goHomeToAddHost() { + router.goHome() + } - var body: some View { - NavigationStack { - ZStack { - p.bg.ignoresSafeArea() + // MARK: - 舞台 - ScrollView { - VStack(alignment: .leading, spacing: TX.Space.m) { - sectionHeader("连接") - if store.hosts.isEmpty { - emptyState - } else { - ForEach(store.hosts) { host in - HostCard(host: host) { model.connect(to: host) } - .contextMenu { - Button(role: .destructive) { store.remove(host) } label: { - Label("删除", systemImage: "trash") - } - } - } - } - if isBusy || model.phaseText.hasPrefix("失败") { - statusRow - } - } - .padding(TX.Space.m) - .padding(.bottom, 90) // 给 FAB 留空 + private var stage: some View { + ZStack(alignment: .top) { + VStack(spacing: 0) { + // 无 tmux 时的行内引导横幅(设计 2c):占一行,把 pane 往下推,不遮内容。 + if session.tmuxUnavailable { + NoTmuxBanner(onInstall: { showTmuxInstall = true }, + onDismiss: { session.dismissTmuxBanner() }) + .padding(.top, TX.Layout.capsuleInset) + .transition(.opacity) } - - fab + // 沉浸:pane 容器**从舞台顶部就开始**(底色 + 边框铺满),胶囊悬浮其上; + // 只把 surface 内容下移 86pt 让出预留带 —— 预留带铺的是终端底色,故视觉连续。 + paneArea + // 横幅已占掉预留带时不再重复留白(设计 2c:多一条横幅时预留 122pt)。 + .padding(.top, session.tmuxUnavailable ? TX.Space.hairline : TX.Layout.capsuleInset) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(p.terminalBackground) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay( + RoundedRectangle(cornerRadius: 6).strokeBorder(p.border, lineWidth: 1) + ) + // 底部不留缝:pane 底边即与侧边栏脚同高的那条分割线。 + .padding([.leading, .trailing, .top], TX.Layout.paneGap) + TerminalHintBar(context: hintContext, hints: hints) } - .background(p.bg) - .toolbar { - ToolbarItem(placement: .principal) { - Text("terminalX") - .font(TX.Font.mono(17, .semibold)) - .foregroundStyle(p.textPrimary) - } - ToolbarItem(placement: .topBarTrailing) { - Button { showSettings = true } label: { - Image(systemName: "gearshape") - .foregroundStyle(p.textSecondary) - } - } + capsules + } + .background(p.bgDeep) + .contentShape(Rectangle()) + // 点终端任意处收起遮罩态侧边栏(Pin 态不受影响)。 + .simultaneousGesture(TapGesture().onEnded { router.dismissOverlaySidebar() }) + } + + @ViewBuilder private var paneArea: some View { + ZStack { + if let tmux = session.tmuxController { + TmuxStage(controller: tmux) + } else { + RawTerminalView(model: session) } - .toolbarBackground(p.bg, for: .navigationBar) - .toolbarBackground(.visible, for: .navigationBar) - } - .tint(p.accent) - .sheet(isPresented: $showAdd) { - AddHostSheet { store.add($0) } - } - .sheet(isPresented: $showSettings) { - SettingsStubView() + // 连接中 / 失败 / 已关闭时给出解释与出路(已连接时此层不渲染,不遮挡终端)。 + TerminalStateOverlay(session: session, + onRetry: { session.retry() }, + onBack: { router.goHome() }, + onCancel: { + let id = session.id + router.goHome() + manager.close(id) + }) } } - private func sectionHeader(_ text: String) -> some View { - Text(text) - .font(.system(size: 13, weight: .medium)) - .foregroundStyle(p.textSecondary) - .padding(.leading, 4) - } - - private var emptyState: some View { - VStack(spacing: TX.Space.s) { - Image(systemName: "server.rack") - .font(.system(size: 34, weight: .light)) - .foregroundStyle(p.textTertiary) - Text("还没有主机") - .font(.system(size: 15)) - .foregroundStyle(p.textSecondary) - Text("点击右下角 + 添加一台服务器") - .font(.system(size: 13)) - .foregroundStyle(p.textTertiary) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 48) - } - - /// 进行中(连接/认证/重连)——决定是否转圈。 - private var isBusy: Bool { - model.phaseText.hasSuffix("…") || model.phaseText.contains("重连") - } - - private var statusRow: some View { - HStack(spacing: TX.Space.s) { - if isBusy { ProgressView().controlSize(.small).tint(p.accent) } - Text(model.phaseText) - .font(.system(size: 13)) - .foregroundStyle(model.phaseText.hasPrefix("失败") ? .red : p.textSecondary) - } - .padding(.horizontal, 4) - } - - private var fab: some View { - VStack { - Spacer() - HStack { - Spacer() - Button { showAdd = true } label: { - Image(systemName: "plus") - .font(.system(size: 24, weight: .semibold)) - .foregroundStyle(p.bg) - .frame(width: 60, height: 60) - .background(p.accent, in: Circle()) - .shadow(color: p.accent.opacity(0.35), radius: 12, y: 4) - } - .padding(TX.Space.l) - } - } - } -} - -/// 主机卡片:服务器图标 + 状态点 / 名称 / user@ip:port(等宽)/ chevron。 -struct HostCard: View { - let host: SavedHost - let onTap: () -> Void - @EnvironmentObject var theme: TXThemeManager - private var p: TXPalette { theme.palette } - - var body: some View { - Button(action: onTap) { - HStack(spacing: TX.Space.m) { - ZStack(alignment: .topTrailing) { - Image(systemName: "server.rack") - .font(.system(size: 20, weight: .regular)) - .foregroundStyle(p.textSecondary) - .frame(width: 44, height: 44) - .background(p.surfaceElevated, in: RoundedRectangle(cornerRadius: TX.Radius.chip)) - if host.useMosh { - Circle().fill(p.mosh).frame(width: 9, height: 9) - .overlay(Circle().stroke(p.surface, lineWidth: 2)) - .offset(x: 3, y: -3) - } - } - VStack(alignment: .leading, spacing: 3) { - Text(host.name) - .font(.system(size: 16, weight: .semibold)) - .foregroundStyle(p.textPrimary) - Text(host.addressLine) - .font(TX.Font.mono(13)) - .foregroundStyle(p.textSecondary) - } - Spacer() - Image(systemName: "chevron.right") - .font(.system(size: 14, weight: .semibold)) - .foregroundStyle(p.textTertiary) - } - .padding(TX.Space.m) - .background(p.surface, in: RoundedRectangle(cornerRadius: TX.Radius.card)) - .overlay(RoundedRectangle(cornerRadius: TX.Radius.card).stroke(p.border, lineWidth: 1)) - } - .buttonStyle(.plain) - } -} - -/// 添加主机表单(深色样式)。 -struct AddHostSheet: View { - let onSave: (SavedHost) -> Void - @EnvironmentObject var theme: TXThemeManager - @Environment(\.dismiss) private var dismiss - @State private var name = "" - @State private var host = "" - @State private var port = "22" - @State private var username = "" - @State private var password = "" - @State private var useMosh = false - - private var p: TXPalette { theme.palette } - private var canSave: Bool { !host.isEmpty && !username.isEmpty } - - var body: some View { - NavigationStack { - ZStack { - p.bg.ignoresSafeArea() - Form { - Section { - field("名称", text: $name, placeholder: host.isEmpty ? "我的服务器" : host) - field("主机 / IP", text: $host) - field("端口", text: $port, keyboard: .numberPad) - field("用户名", text: $username) - SecureField("密码", text: $password) - .foregroundStyle(p.textPrimary) - .listRowBackground(p.surface) - Toggle("使用 mosh", isOn: $useMosh) - .tint(p.accent) - .foregroundStyle(p.textPrimary) - .listRowBackground(p.surface) - } - } - .scrollContentBackground(.hidden) - } - .navigationTitle("添加主机") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("取消") { dismiss() }.tint(p.textSecondary) - } - ToolbarItem(placement: .confirmationAction) { - Button("保存") { - onSave(SavedHost( - name: name.isEmpty ? host : name, - host: host, port: Int(port) ?? 22, - username: username, password: password, useMosh: useMosh)) - dismiss() - } - .disabled(!canSave) - .tint(p.accent) - } - } - .toolbarBackground(p.bg, for: .navigationBar) - .toolbarBackground(.visible, for: .navigationBar) - } - .tint(p.accent) - } - - private func field(_ title: String, text: Binding, - placeholder: String = "", keyboard: UIKeyboardType = .default) -> some View { - TextField(placeholder.isEmpty ? title : placeholder, text: text) - .keyboardType(keyboard) - .autocorrectionDisabled() - .textInputAutocapitalization(.never) - .foregroundStyle(p.textPrimary) - .listRowBackground(p.surface) - } -} - -/// 设置页占位(后续填充:终端主题/字体/工具栏/关于)。 -struct SettingsStubView: View { - @EnvironmentObject var theme: TXThemeManager - @Environment(\.dismiss) private var dismiss - private var p: TXPalette { theme.palette } - - var body: some View { - NavigationStack { - ZStack { - p.bg.ignoresSafeArea() - List { - Section("主题(app 与终端统一)") { - ForEach(TXPalette.all) { pal in - Button { theme.apply(pal) } label: { themeRow(pal) } - .buttonStyle(.plain) - } - } - .listRowBackground(p.surface) - Section("终端") { - row("textformat", "字体与大小", value: "系统等宽") - } - .listRowBackground(p.surface) - Section("关于") { - row("info.circle", "terminalX", value: "0.0.1") - } - .listRowBackground(p.surface) - } - .scrollContentBackground(.hidden) - .foregroundStyle(p.textPrimary) - } - .navigationTitle("设置") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("完成") { dismiss() }.tint(p.accent) - } - } - .toolbarBackground(p.bg, for: .navigationBar) - .toolbarBackground(.visible, for: .navigationBar) - } - .tint(p.accent) - } - - /// 主题行:色板预览(底/面/强调)+ 名称 + 选中勾。 - private func themeRow(_ pal: TXPalette) -> some View { - HStack(spacing: TX.Space.m) { - ZStack { - RoundedRectangle(cornerRadius: 7).fill(pal.bg) - HStack(spacing: 3) { - Circle().fill(pal.accent).frame(width: 7, height: 7) - Circle().fill(pal.mosh).frame(width: 7, height: 7) - } - } - .frame(width: 40, height: 28) - .overlay(RoundedRectangle(cornerRadius: 7).stroke(p.border, lineWidth: 1)) - - Text(pal.name).foregroundStyle(p.textPrimary) - Spacer() - if pal.id == theme.palette.id { - Image(systemName: "checkmark").foregroundStyle(p.accent).font(.system(size: 14, weight: .semibold)) - } - } - } - - private func row(_ icon: String, _ title: String, value: String) -> some View { - HStack { - Image(systemName: icon).foregroundStyle(p.textSecondary).frame(width: 26) - Text(title).foregroundStyle(p.textPrimary) - Spacer() - Text(value).foregroundStyle(p.textSecondary).font(.system(size: 14)) - } - } -} - -struct TerminalScreen: View { - @ObservedObject var model: SSHTerminalModel - @EnvironmentObject var theme: TXThemeManager - private var p: TXPalette { theme.palette } - - var body: some View { - VStack(spacing: 0) { - TerminalTopBar(model: model) - Divider().overlay(p.border) - Group { - if let tmux = model.tmuxController { - TmuxTabbedView(controller: tmux) // tmux -CC → 原生 tab - } else { - RawTerminalView(model: model) // 普通单终端 - } - } - } - .background(p.bg.ignoresSafeArea()) - } -} - -/// 终端顶部状态行:返回主屏 · 会话标题 · 状态提示 · 传输徽章(SSH/mosh)。 -struct TerminalTopBar: View { - @ObservedObject var model: SSHTerminalModel - @EnvironmentObject var theme: TXThemeManager - private var p: TXPalette { theme.palette } - - private var connected: Bool { model.phaseText == "已连接" || model.phaseText == "mosh 已连接" } - - var body: some View { - HStack(spacing: TX.Space.s) { - Button { model.close() } label: { - Image(systemName: "chevron.left") - .font(.system(size: 15, weight: .semibold)) - .foregroundStyle(p.textSecondary) - .frame(width: 34, height: 34) - .background(p.surface, in: Circle()) - } - .buttonStyle(.plain) - - VStack(alignment: .leading, spacing: 1) { - Text(model.connectionTitle.isEmpty ? "终端" : model.connectionTitle) - .font(.system(size: 15, weight: .semibold)) - .foregroundStyle(p.textPrimary) - .lineLimit(1) - if !connected { - Text(model.phaseText) - .font(.system(size: 11)) - .foregroundStyle(model.phaseText.hasPrefix("失败") ? .red : p.textSecondary) - .lineLimit(1) - } - } - Spacer() - transportBadge + private var capsules: some View { + HStack(alignment: .top) { + TerminalTitleCapsule(session: session, onClose: requestClose, onMinimize: minimizeSession) + Spacer(minLength: TX.Space.m) + TerminalStatusCapsule(session: session) } .padding(.horizontal, TX.Space.m) - .padding(.vertical, TX.Space.s) - .background(p.bg) + .padding(.top, 14) } - private var transportBadge: some View { - let isMosh = model.moshEngaged - let color = isMosh ? p.mosh : p.accent - return HStack(spacing: 5) { - Circle().fill(connected ? color : p.offline).frame(width: 6, height: 6) - Text(isMosh ? "mosh" : "SSH") - .font(TX.Font.mono(11, .medium)) - .foregroundStyle(color) + /// 红点:**必须**先确认(设计要求)。确认后的动作分两套,见 `closeSession(kill:)`。 + private func requestClose() { + router.present(.closeConfirm) + } + + /// 执行关闭。tmux 会话默认只 `detach-client`(服务端继续跑);`kill=true` 才 `kill-session`。 + /// 原生窗口没有服务端兜底,直接 teardown。 + private func closeSession(kill: Bool) { + let id = session.id + if let tmux = session.tmuxController { + if kill { tmux.killSession() } else { tmux.detach() } + } + withAnimation(AppRouter.sidebarAnimation) { router.goHome() } + // 给 detach/kill 命令一点时间落地再拆传输层(tmux 会回 %exit 走既有 onExit 收尾)。 + Task { @MainActor in + try? await Task.sleep(nanoseconds: 250_000_000) + manager.close(id) + } + } + + private func minimizeSession() { + session.captureSnapshot() // 缩回首页前抓一次镜像,卡片立刻有内容 + manager.minimize(session.id) + withAnimation(AppRouter.sidebarAnimation) { router.goHome() } + } + + /// 分屏菜单选择后立即分屏(设计:选完不再问方向)。 + private func performSplit(_ target: SplitMenuView.SplitTarget, vertical: Bool) { + guard let tmux = session.tmuxController else { return } + switch target { + case .currentSession: + tmux.splitWindow(vertical: vertical) + case .otherSession(let name): + tmux.splitWindow(vertical: vertical, joinSession: name) + case .newSession: + // 名字用会话数递增,避免与既有会话撞名(tmux 撞名会报错)。 + tmux.splitIntoNewSession(vertical: vertical, name: "tx\(tmux.sessions.count + 1)") + case .nativeWindow: + break // 首版不可用(菜单里已灰显),见 HANDOFF §12.4 + } + } + + /// ⌘D / ⌘⇧D 的入口:先拉一次 list-sessions 再弹菜单(菜单要列 detached 会话)。 + private func requestSplit(vertical: Bool) { + session.tmuxController?.listSessions() + router.present(.splitMenu(vertical: vertical)) + } + + private var hintContext: String { + switch router.sidebar { + case .pinned: "Pin 态 · 终端变窄,仅在钉住/解除时 resize 一次" + case .overlay: "遮罩态 · 点终端任意处收起,终端不 reflow" + case .collapsed: session.isLive ? "已连接妙控键盘 · 附加行已收起" : session.phaseText + } + } + + private var hints: [(key: String, label: String)] { + switch router.sidebar { + case .pinned: [("Pin", "解除钉住"), ("⌘\\", "收起为轨道"), ("⌘⇧H", "回首页")] + case .overlay: [("⌘\\", "收起"), ("Pin", "钉在左侧"), ("⌘⇧H", "回首页")] + case .collapsed: [("⌘K", "命令面板"), ("⌘D", "竖分屏"), ("⌘T", "新 window"), ("⌘⇧H", "回首页")] } - .padding(.horizontal, 9).padding(.vertical, 5) - .background(color.opacity(0.13), in: Capsule()) } } /// 普通(非 tmux)单终端视图。 struct RawTerminalView: View { - @ObservedObject var model: SSHTerminalModel + @ObservedObject var model: TerminalSession @FocusState private var focused: Bool var body: some View { @@ -449,88 +407,52 @@ struct RawTerminalView: View { } } -/// tmux control-mode:window 映射为原生 tab 条 + 活动 window 终端。 -struct TmuxTabbedView: View { +/// tmux control-mode 舞台:所有 window 常驻挂载、只显活动 window。 +/// window 切换已上移到轨道数字 / 侧边栏 window 行(设计稿去掉了 tab 条)。 +struct TmuxStage: View { @ObservedObject var controller: TmuxController @EnvironmentObject var theme: TXThemeManager @Environment(\.displayScale) private var displayScale private var p: TXPalette { theme.palette } var body: some View { - VStack(spacing: 0) { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: TX.Space.xs) { + Group { + if controller.windows.isEmpty { + VStack { + Spacer() + Text("正在枚举 tmux window…") + .font(TX.Font.mono(12)) + .foregroundStyle(p.textWeak) + Spacer() + } + } else { + // 所有窗口常驻挂载、仅激活窗口可见:切 window 不销毁其它窗口的 pane 视图, + // 故 ghostty surface 不被释放、grid 内容保留(否则切回旧窗口会空白), + // 且后台窗口持续接收 %output 保持实时。非激活窗口关掉命中测试,避免吞触摸。 + ZStack { ForEach(controller.windows) { win in - TmuxTabChip(window: win, - isActive: win.id == controller.activeWindowID) { - controller.selectWindow(win.id) - } - } - Button { controller.newWindow() } label: { - Image(systemName: "plus") - .font(.system(size: 14, weight: .semibold)) - .foregroundStyle(p.textSecondary) - .padding(.horizontal, 8).padding(.vertical, 6) - } - } - .padding(.horizontal, TX.Space.s).padding(.vertical, TX.Space.xs) - } - .background(p.bg) - Divider().overlay(p.border) - Group { - if controller.windows.isEmpty { - Spacer(); Text("无 tmux 窗口").foregroundStyle(p.textSecondary); Spacer() - } else { - // 所有窗口常驻挂载、仅激活窗口可见:切 tab 不销毁其它窗口的 pane 视图, - // 故 ghostty surface 不被释放、grid 内容保留(否则切回旧 tab 会空白), - // 且后台窗口持续接收 %output 保持实时。非激活窗口关掉命中测试,避免吞触摸。 - ZStack { - ForEach(controller.windows) { win in - TmuxWindowView(window: win, controller: controller) - .opacity(win.id == controller.activeWindowID ? 1 : 0) - .allowsHitTesting(win.id == controller.activeWindowID) - } + TmuxWindowView(window: win, controller: controller) + .opacity(win.id == controller.activeWindowID ? 1 : 0) + .allowsHitTesting(win.id == controller.activeWindowID) } } } - .frame(maxWidth: .infinity, maxHeight: .infinity) - // 容器几何测量放在这一层(windows 为空时同样占满剩余空间、尺寸相同): - // pane grid 由容器像素决定,须在 attach 前拿到真实容器格子;放 TmuxWindowView 会与 - // "windows 靠 list-windows 才有" 形成鸡生蛋死锁(见 TmuxController.kickoffIfReady)。 - .background(GeometryReader { geo in - Color.clear - .onAppear { controller.containerResized(widthPt: geo.size.width, heightPt: geo.size.height, scale: displayScale) } - .onChange(of: geo.size) { _, s in - controller.containerResized(widthPt: s.width, heightPt: s.height, scale: displayScale) - } - }) } + .frame(maxWidth: .infinity, maxHeight: .infinity) + // 容器几何测量放在这一层(windows 为空时同样占满剩余空间、尺寸相同): + // pane grid 由容器像素决定,须在 attach 前拿到真实容器格子;放 TmuxWindowView 会与 + // "windows 靠 list-windows 才有" 形成鸡生蛋死锁(见 TmuxController.kickoffIfReady)。 + .background(GeometryReader { geo in + Color.clear + .onAppear { controller.containerResized(widthPt: geo.size.width, heightPt: geo.size.height, scale: displayScale) } + .onChange(of: geo.size) { _, s in + controller.containerResized(widthPt: s.width, heightPt: s.height, scale: displayScale) + } + }) } } -struct TmuxTabChip: View { - @ObservedObject var window: TmuxWindow - let isActive: Bool - let onTap: () -> Void - @EnvironmentObject var theme: TXThemeManager - private var p: TXPalette { theme.palette } - - var body: some View { - Button(action: onTap) { - Text(window.title) - .font(TX.Font.mono(13, isActive ? .semibold : .regular)).lineLimit(1) - .foregroundStyle(isActive ? p.accent : p.textSecondary) - .padding(.horizontal, 12).padding(.vertical, 6) - .background(isActive ? p.accentSoft : p.surface, - in: RoundedRectangle(cornerRadius: TX.Radius.chip)) - .overlay(RoundedRectangle(cornerRadius: TX.Radius.chip) - .stroke(isActive ? p.accent.opacity(0.4) : p.border, lineWidth: 1)) - } - .buttonStyle(.plain) - } -} - -/// 一个 window:按 tmux 可见布局树把多 pane 绝对定位为分屏(每 pane 一 surface)。 +/// 一个 window:按 tmux 可见布局树把多 pane 绝对定位为分屏(每 pane 一 surface)+ pane 间拖拽条。 struct TmuxWindowView: View { @ObservedObject var window: TmuxWindow let controller: TmuxController @@ -557,12 +479,16 @@ struct TmuxWindowView: View { y: (CGFloat(item.rect.y) + CGFloat(item.rect.height) / 2) * ch) } } + // pane 之间的拖拽条(在 pane 之上,热区 12pt)。 + ForEach(splitters(layout.root), id: \.id) { s in + PaneSplitter(splitter: s, cellW: cw, cellH: ch, controller: controller) + } } } else { Color.clear } } - // 容器尺寸测量已上移到 TmuxTabbedView(单一来源),此处不再驱动 containerResized。 + // 容器尺寸测量已上移到 TmuxStage(单一来源),此处不再驱动 containerResized。 } } @@ -573,22 +499,147 @@ struct TmuxWindowView: View { case .horizontal(let cs, _), .vertical(let cs, _): return cs.flatMap(leaves) } } + + /// 收集所有相邻子节点之间的分隔位置。tmux 的 `resize-pane -x/-y` 作用于**包含该 pane 的那层分割**, + /// 故 resize 目标取"前一个子节点里的任一 pane"即可。 + private func splitters(_ node: TmuxLayout.Node) -> [SplitterSpec] { + var out: [SplitterSpec] = [] + func walk(_ n: TmuxLayout.Node) { + switch n { + case .leaf: + break + case .horizontal(let children, _): + // 左右并排 → 竖直分隔条,拖动改前一个子节点的**宽度**。 + for (i, child) in children.enumerated() where i < children.count - 1 { + let r = child.rect + if let pane = firstPane(child) { + out.append(SplitterSpec( + id: "h-\(pane.raw)-\(i)", isVertical: true, pane: pane, + x: r.x + r.width, y: r.y, length: r.height, currentSize: r.width)) + } + } + children.forEach(walk) + case .vertical(let children, _): + // 上下堆叠 → 水平分隔条,拖动改前一个子节点的**高度**。 + for (i, child) in children.enumerated() where i < children.count - 1 { + let r = child.rect + if let pane = firstPane(child) { + out.append(SplitterSpec( + id: "v-\(pane.raw)-\(i)", isVertical: false, pane: pane, + x: r.x, y: r.y + r.height, length: r.width, currentSize: r.height)) + } + } + children.forEach(walk) + } + } + walk(node) + return out + } + + private func firstPane(_ node: TmuxLayout.Node) -> TmuxPaneID? { + switch node { + case .leaf(let p, _): return p + case .horizontal(let cs, _), .vertical(let cs, _): return cs.compactMap(firstPane).first + } + } } -/// 单个 pane 的终端面 + 焦点边框 + 点击选中。 +/// 一条分隔位置(格子坐标)。 +struct SplitterSpec { + let id: String + /// true = 竖直条(左右分,拖动改宽);false = 水平条(上下分,拖动改高) + let isVertical: Bool + /// resize 目标 pane(前一个子节点里的任一 pane) + let pane: TmuxPaneID + let x: Int, y: Int + /// 条的长度(格子) + let length: Int + /// 前一个子节点当前的 cols / rows + let currentSize: Int +} + +/// pane 拖拽条:热区 12pt、把手 44×3pt。 +/// +/// **拖动中只动视觉**(把手位移 + 高亮线),不改 pane frame、不发命令;**松手才发一次 `resize-pane`**, +/// 然后等 tmux 回 `%layout-change` 更新真实 frame —— 与"路线 b"一致(客户端不自行重排)。 +struct PaneSplitter: View { + let splitter: SplitterSpec + let cellW: CGFloat + let cellH: CGFloat + let controller: TmuxController + @State private var drag: CGFloat = 0 + @State private var dragging = false + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + private var hit: CGFloat { TX.Layout.paneDragHit } + + var body: some View { + Group { + if splitter.isVertical { + bar(width: hit, height: CGFloat(splitter.length) * cellH) + .position(x: CGFloat(splitter.x) * cellW + drag, + y: (CGFloat(splitter.y) + CGFloat(splitter.length) / 2) * cellH) + } else { + bar(width: CGFloat(splitter.length) * cellW, height: hit) + .position(x: (CGFloat(splitter.x) + CGFloat(splitter.length) / 2) * cellW, + y: CGFloat(splitter.y) * cellH + drag) + } + } + .gesture( + DragGesture(minimumDistance: 2) + .onChanged { v in + dragging = true + drag = splitter.isVertical ? v.translation.width : v.translation.height + } + .onEnded { _ in + // 位移 → 格子增量;松手发一次命令,视觉位移立刻归零(等 tmux 回布局)。 + let cell = splitter.isVertical ? cellW : cellH + let delta = cell > 0 ? Int((drag / cell).rounded()) : 0 + let newSize = max(1, splitter.currentSize + delta) + if delta != 0 { + controller.resizePane(splitter.pane, + cols: splitter.isVertical ? newSize : nil, + rows: splitter.isVertical ? nil : newSize) + } + drag = 0 + dragging = false + } + ) + } + + /// 热区透明 + 居中 44×3pt 把手(拖动中把手转强调色)。 + private func bar(width: CGFloat, height: CGFloat) -> some View { + ZStack { + Color.clear.frame(width: width, height: height) + RoundedRectangle(cornerRadius: 2) + .fill(dragging ? TXAccent.base : p.borderStrong) + .frame(width: splitter.isVertical ? 3 : 44, + height: splitter.isVertical ? 44 : 3) + .opacity(dragging ? 1 : 0.55) + } + .contentShape(Rectangle()) + } +} + +/// 单个 pane 的终端面 + 焦点边框 + 点击选中。pane 之间留 2pt 缝(设计:舞台底色透出为缝)。 struct TmuxPaneView: View { @ObservedObject var surface: TmuxPaneSurface let isActive: Bool let onTap: () -> Void let onReady: () -> Void @FocusState private var focused: Bool + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } var body: some View { TerminalSurfaceView(context: surface.state) .terminalFocusOnAppear($focused) + .clipShape(RoundedRectangle(cornerRadius: 6)) .overlay( - Rectangle().stroke(isActive ? Color.accentColor : Color.gray.opacity(0.35), - lineWidth: isActive ? 2 : 0.5) + RoundedRectangle(cornerRadius: 6) + .strokeBorder(isActive ? TXAccent.border : p.divider, + lineWidth: isActive ? 1 : 0.5) ) // 非活动 pane 叠透明 tap-catcher(TerminalSurfaceView 会吞触摸);活动 pane 直通终端。 .overlay { @@ -596,6 +647,7 @@ struct TmuxPaneView: View { Color.black.opacity(0.04).contentShape(Rectangle()).onTapGesture { onTap() } } } + .padding(TX.Layout.paneGap / 2) // 相邻 pane 各让 1pt → 缝 2pt .onChange(of: isActive) { _, active in focused = active } .task { for _ in 0 ..< 300 where surface.state.surfaceSize == nil { diff --git a/apps/TerminalX/iOS/Credential.swift b/apps/TerminalX/iOS/Credential.swift new file mode 100644 index 0000000..73d9c92 --- /dev/null +++ b/apps/TerminalX/iOS/Credential.swift @@ -0,0 +1,92 @@ +import Foundation + +/// 一条 Keychain 凭据(元数据部分,**不含机密**)。 +/// 机密(密码/私钥材料/passphrase)经 `CredentialStore.secret(for:)` 按需单条取, +/// 列表 UI 只碰这里的 meta,永不为渲染而加载机密(对齐签名后 Keychain 后端的天然分层)。 +struct Credential: Identifiable, Codable, Equatable { + var id: UUID = UUID() + var name: String + var username: String + var kind: Kind + var createdAt: Date = Date() + + enum Kind: Codable, Equatable { + /// 用户名 + 密码。 + case password + /// SSH 私钥。`publicKeyBlob` 冗余存 SSH 线格式公钥,供列表展示指纹/复制 authorized_keys 而不碰机密。 + case privateKey(keyType: KeyType, source: Source, hasPassphrase: Bool, publicKeyBlob: Data) + /// Secure Enclave(Face ID)密钥——机密不出 SE,只存 tag 引用。未签名构建下建不出,UI 占位。 + case secureEnclave(keyTag: String) + } + + /// 私钥来源。 + enum Source: String, Codable { case pasted, imported, generated } + + /// 私钥算法。libssh2 无 Ed25519、有 ECDSA;"生成新的"默认 P-256。 + enum KeyType: String, Codable { + case ecdsaP256, ecdsaP384, ecdsaP521, rsa, unknown + + var displayName: String { + switch self { + case .ecdsaP256: "ECDSA P-256" + case .ecdsaP384: "ECDSA P-384" + case .ecdsaP521: "ECDSA P-521" + case .rsa: "RSA" + case .unknown: "SSH 私钥" + } + } + } + + // MARK: - 展示辅助 + + var kindLabel: String { + switch kind { + case .password: "密码" + case .privateKey(let t, _, _, _): t.displayName + case .secureEnclave: "Face ID 密钥" + } + } + + var iconName: String { + switch kind { + case .password: "key.fill" + case .privateKey: "lock.doc.fill" + case .secureEnclave: "faceid" + } + } + + /// SSH 线格式公钥 blob(仅私钥类凭据有)。 + var publicKeyBlob: Data? { + if case .privateKey(_, _, _, let blob) = kind { return blob } + return nil + } + + /// authorized_keys 行(生成/导入的私钥可复制到服务器)。 + var authorizedKeysLine: String? { + guard let blob = publicKeyBlob, !blob.isEmpty else { return nil } + // 目前生成的都是 ecdsa-sha2-nistp256;导入的按 keyType 推断算法名。 + let algo: String + if case .privateKey(let t, _, _, _) = kind { + switch t { + case .ecdsaP256: algo = "ecdsa-sha2-nistp256" + case .ecdsaP384: algo = "ecdsa-sha2-nistp384" + case .ecdsaP521: algo = "ecdsa-sha2-nistp521" + case .rsa: algo = "ssh-rsa" + case .unknown: return nil + } + } else { return nil } + return "\(algo) \(blob.base64EncodedString()) \(username)@terminalx" + } +} + +/// 凭据机密(按需加载,自描述解释方式)。 +enum CredentialSecret: Codable, Equatable { + /// 密码 → `.password` 认证。 + case password(String) + /// PEM / PKCS#8 私钥文本(导入/粘贴)→ libssh2 `publickey_frommemory`(路径 B)。 + case pemPrivateKey(pem: Data, passphrase: String?) + /// SecKey 外部表示(生成的软件 P-256 私钥)→ 重建 SecKey → SSHSigner(路径 A)。 + case rawECPrivateKey(x963: Data) + /// 无机密(Secure Enclave:机密不出安全域)。 + case none +} diff --git a/apps/TerminalX/iOS/CredentialAuth.swift b/apps/TerminalX/iOS/CredentialAuth.swift new file mode 100644 index 0000000..85d04a8 --- /dev/null +++ b/apps/TerminalX/iOS/CredentialAuth.swift @@ -0,0 +1,84 @@ +import Foundation +import TXTransport + +/// 凭据 ↔ SSH 认证 的桥。三条各自很薄的路径: +/// - 密码 → `.password` +/// - 生成的软件 P-256 → 重建 SecKey → `SSHSigner` → `.publicKeyCallback`(路径 A) +/// - 粘贴/导入 的 PEM/PKCS#8 → `.privateKey`(libssh2 `publickey_frommemory`,路径 B) +/// +/// 不自造私钥格式解析器:导入的私钥直接透传 libssh2/mbedTLS。只做**轻量头部嗅探**推断类型/加密/警告。 +enum CredentialAuth { + + // MARK: - 生成 + + /// 生成一把软件 P-256 私钥 → 返回可存入 CredentialStore 的 (kind, secret)。 + static func generateECDSAP256() -> (kind: Credential.Kind, secret: CredentialSecret)? { + guard let (provider, x963) = SigningKeyProvider.generateSoftwareP256() else { return nil } + let kind = Credential.Kind.privateKey( + keyType: .ecdsaP256, source: .generated, hasPassphrase: false, + publicKeyBlob: Data(provider.publicKeyBlob)) + return (kind, .rawECPrivateKey(x963: x963)) + } + + // MARK: - 导入 / 粘贴 + + struct ParsedKey { + var kind: Credential.Kind + var secret: CredentialSecret + /// 人类可读的格式标签(UI 实时提示,如 "PEM · EC 私钥")。 + var formatLabel: String + /// 非致命警告(如 openssh-key-v1 mbedTLS 不认)。nil = 无。 + var warning: String? + } + + /// 嗅探私钥文本头部,产出凭据 kind + 机密(路径 B)。识别不了也照存(交由 libssh2 定夺)。 + static func parsePrivateKey(_ text: String, source: Credential.Source) -> ParsedKey { + let t = text.trimmingCharacters(in: .whitespacesAndNewlines) + let encrypted = t.contains("Proc-Type: 4,ENCRYPTED") + || t.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----") + + var keyType: Credential.KeyType = .unknown + var label = "SSH 私钥" + var warning: String? + + if t.contains("-----BEGIN OPENSSH PRIVATE KEY-----") { + label = "OpenSSH 新格式" + warning = "mbedTLS 不识别 openssh-key-v1,可能认证失败。建议先 `ssh-keygen -p -m PEM` 转为 PEM。" + } else if t.contains("-----BEGIN RSA PRIVATE KEY-----") { + keyType = .rsa; label = "PEM · RSA (PKCS#1)" + } else if t.contains("-----BEGIN EC PRIVATE KEY-----") { + keyType = .ecdsaP256; label = "PEM · EC (SEC1)" + } else if t.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----") { + label = "PKCS#8(加密)" + } else if t.contains("-----BEGIN PRIVATE KEY-----") { + label = "PKCS#8" + } else { + warning = "未识别到 PEM 私钥头,请确认粘贴的是私钥内容。" + } + + let kind = Credential.Kind.privateKey( + keyType: keyType, source: source, hasPassphrase: encrypted, publicKeyBlob: Data()) + let secret = CredentialSecret.pemPrivateKey(pem: Data(text.utf8), passphrase: nil) + return ParsedKey(kind: kind, secret: secret, formatLabel: label, warning: warning) + } + + // MARK: - 构造 Authentication + + /// 由凭据 meta + 机密构造 SSH 认证。返回 nil = 无法用于连接(如 SE 未签名建不出、机密缺失)。 + /// `passphraseOverride` 供连接时按需补输加密私钥口令。 + static func authentication(for cred: Credential, secret: CredentialSecret, + passphraseOverride: String? = nil) -> SSHConfig.Authentication? { + switch secret { + case .password(let pw): + return .password(pw) + case .pemPrivateKey(let pem, let storedPass): + let pem = String(decoding: pem, as: UTF8.self) + return .privateKey(pem: pem, passphrase: passphraseOverride ?? storedPass) + case .rawECPrivateKey(let x963): + guard let signer = SigningKeyProvider.from(x963: x963) else { return nil } + return .publicKeyCallback(signer) + case .none: + return nil // Secure Enclave 占位:未签名无法签名 + } + } +} diff --git a/apps/TerminalX/iOS/CredentialStore.swift b/apps/TerminalX/iOS/CredentialStore.swift new file mode 100644 index 0000000..1515625 --- /dev/null +++ b/apps/TerminalX/iOS/CredentialStore.swift @@ -0,0 +1,78 @@ +import Foundation + +/// 凭据存储抽象:meta 与机密分离。 +/// 让「未签名文件后端」与「签名后 Keychain 后端」可无缝替换——列表只用 `list()`(纯 meta), +/// 机密经 `secret(for:)` 按需单条取(Keychain 后端天然 meta 在 JSON、机密在 SecItem)。 +@MainActor +protocol CredentialStore: AnyObject { + func list() -> [Credential] + func secret(for id: UUID) -> CredentialSecret? + func upsert(_ cred: Credential, secret: CredentialSecret?) // secret == nil 时仅改 meta + func delete(_ id: UUID) +} + +/// 未签名构建的文件后端。 +/// +/// 为何不自造加密:无根信任(无 Keychain/SE 兜底存主密钥)则本地对称加密是伪安全—— +/// 能读机密文件者同样能读密钥文件。威胁模型为开发自用工具 / 模拟器 / 非越狱设备, +/// 沙盒明文与现有 host 明文密码一致即可。做三件几乎免费的加固: +/// 0600 权限、`isExcludedFromBackup`(防机密进 iCloud/iTunes 备份)、`FileProtectionType.complete`(锁屏加密)。 +/// 信封版本化(`scheme: "plaintext-v1"`)便于签名后按 scheme 分派迁移。 +@MainActor +final class FileCredentialStore: ObservableObject, CredentialStore { + @Published private(set) var credentials: [Credential] = [] + + private let url: URL + private var secrets: [UUID: CredentialSecret] = [:] + + private struct Entry: Codable { var meta: Credential; var secret: CredentialSecret } + private struct Envelope: Codable { var scheme: String; var entries: [Entry] } + private static let scheme = "plaintext-v1" + + init() { + let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + try? FileManager.default.createDirectory(at: base, withIntermediateDirectories: true) + url = base.appendingPathComponent("credentials.json") + load() + } + + func list() -> [Credential] { credentials } + + func secret(for id: UUID) -> CredentialSecret? { secrets[id] } + + func upsert(_ cred: Credential, secret: CredentialSecret?) { + if let i = credentials.firstIndex(where: { $0.id == cred.id }) { + credentials[i] = cred + } else { + credentials.append(cred) + } + if let secret { secrets[cred.id] = secret } + persist() + } + + func delete(_ id: UUID) { + credentials.removeAll { $0.id == id } + secrets[id] = nil + persist() + } + + // MARK: - 持久化 + + private func load() { + guard let data = try? Data(contentsOf: url), + let env = try? JSONDecoder().decode(Envelope.self, from: data) else { return } + credentials = env.entries.map(\.meta) + secrets = Dictionary(uniqueKeysWithValues: env.entries.map { ($0.meta.id, $0.secret) }) + } + + private func persist() { + let entries = credentials.map { Entry(meta: $0, secret: secrets[$0.id] ?? .none) } + let env = Envelope(scheme: Self.scheme, entries: entries) + guard let data = try? JSONEncoder().encode(env) else { return } + try? data.write(to: url, options: [.atomic, .completeFileProtection]) + try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path) + var u = url + var rv = URLResourceValues(); rv.isExcludedFromBackup = true + try? u.setResourceValues(rv) + } +} diff --git a/apps/TerminalX/iOS/HomeView.swift b/apps/TerminalX/iOS/HomeView.swift new file mode 100644 index 0000000..26ab866 --- /dev/null +++ b/apps/TerminalX/iOS/HomeView.swift @@ -0,0 +1,632 @@ +import SwiftUI + +/// 首页 · 主机管理(设计稿 `1e 主机-会话卡`)。 +/// +/// App 冷启动落点。首要动作是**「回到刚才那个会话」**,所以活动会话卡在最上面、占最大面积; +/// 「全部主机」在下面按内容高度铺 4 列。左右留白 40pt。 +struct HomeView: View { + @ObservedObject var manager: SessionManager + @ObservedObject var router: AppRouter + @ObservedObject var hostStore: HostStore + @ObservedObject var credStore: FileCredentialStore + @ObservedObject var tailscaleStore: TailscaleStore + @EnvironmentObject var theme: TXThemeManager + + @State private var search = "" + @State private var groupFilter: String? = nil + @State private var showEditor = false + @State private var editing: SavedHost? + @State private var showSettings = false + + private var p: TXPalette { theme.palette } + + /// 分组 chips:「全部」+ 主机里出现过的分组(按首次出现顺序,不排序以免抖动)。 + private var groups: [String] { + var seen: [String] = [] + for h in hostStore.hosts { + if let g = h.group, !seen.contains(g) { seen.append(g) } + } + return seen + } + + private var filteredHosts: [SavedHost] { + hostStore.hosts.filter { h in + let matchesGroup = groupFilter == nil || h.group == groupFilter + guard matchesGroup else { return false } + guard !search.isEmpty else { return true } + let q = search.lowercased() + return h.name.lowercased().contains(q) || h.host.lowercased().contains(q) + || (h.os?.lowercased().contains(q) ?? false) + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + header + if hostStore.hosts.isEmpty && manager.sessions.isEmpty { + firstRunEmptyState + } else { + ScrollView { + VStack(alignment: .leading, spacing: TX.Space.m) { + // 点主机后先留在首页探测 tmux → 这里给出进度,不让用户对着不动的界面等。 + ForEach(manager.connectingSessions) { s in + ConnectingRow(session: s) { manager.close(s.id) } + } + if liveSessions.isEmpty { + if manager.connectingSessions.isEmpty { noSessionHint } + } else { + TXSectionLabel(text: "活动会话") + .padding(.top, TX.Space.xs) + sessionCards + } + HStack(alignment: .center) { + TXSectionLabel(text: "全部主机") + Spacer() + if !groups.isEmpty { chips } + } + .padding(.top, manager.sessions.isEmpty ? TX.Space.xs : TX.Space.s) + if filteredHosts.isEmpty { + noMatchState + } else { + hostGrid + } + } + .padding(.horizontal, TX.Space.screenGutter) + .padding(.bottom, TX.Space.xxl) + } + } + } + .background(p.bg.ignoresSafeArea()) + // 首页可见时轮询会话内容做文本镜像(缩略卡「真实回滚」的落地方式)。 + .task(id: manager.sessions.count) { + while !Task.isCancelled { + for s in manager.sessions { s.captureSnapshot() } + try? await Task.sleep(nanoseconds: 1_500_000_000) + } + } + .sheet(isPresented: $showEditor) { + HostEditorView(existing: editing, credStore: credStore, tailscaleStore: tailscaleStore, + existingGroups: groups) { host in + if hostStore.hosts.contains(where: { $0.id == host.id }) { hostStore.update(host) } + else { hostStore.add(host) } + } + } + .sheet(isPresented: $showSettings) { + SettingsHost(credStore: credStore, tailscaleStore: tailscaleStore) + } + } + + // MARK: - 空状态 + + /// 首次运行:一台主机都没有。给一条明确的下一步,而不是空网格。 + private var firstRunEmptyState: some View { + VStack(spacing: TX.Space.m) { + Spacer() + Image(systemName: "terminal") + .font(.system(size: 40, weight: .ultraLight)) + .foregroundStyle(TXAccent.mid) + VStack(spacing: 6) { + Text("还没有主机") + .font(TX.Font.mono(15, .medium)) + .foregroundStyle(p.text) + Text("添加一台服务器就能开始:SSH 直连,或经 Tailscale 出口访问内网。\n开启 mosh 后断线自动接回。") + .font(TX.Font.mono(12)) + .foregroundStyle(p.textWeak) + .multilineTextAlignment(.center) + .lineSpacing(4) + } + HStack(spacing: TX.Space.s) { + TXButton(title: "新建连接", emphasized: true) { + editing = nil + showEditor = true + } + TXButton(title: "先配置 Tailscale") { showSettings = true } + } + .padding(.top, TX.Space.xs) + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + /// 有主机但没有会话:不留白,提示从下面的主机开始。 + private var noSessionHint: some View { + HStack(spacing: TX.Space.s) { + Image(systemName: "arrow.down") + .font(.system(size: 12)) + .foregroundStyle(p.textWeak) + Text("还没有在跑的会话 —— 点下面任意主机即可连接") + .font(TX.Font.mono(12)) + .foregroundStyle(p.textWeak) + } + .padding(.horizontal, TX.Space.s) + .frame(height: 40) + .frame(maxWidth: .infinity, alignment: .leading) + .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.card) + .strokeBorder(style: StrokeStyle(lineWidth: 1, dash: [4, 4])) + .foregroundStyle(p.divider) + ) + .padding(.top, TX.Space.xs) + } + + /// 搜索/筛选无命中:说明命中为空的原因(是筛选还是搜索),并给一键清除。 + private var noMatchState: some View { + VStack(spacing: TX.Space.s) { + Text(search.isEmpty ? "该分组下没有主机" : "没有匹配「\(search)」的主机") + .font(TX.Font.mono(12.5, .medium)) + .foregroundStyle(p.text3) + TXButton(title: "清除筛选") { + search = "" + groupFilter = nil + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, TX.Space.xl) + } + + // MARK: - 页头 + + private var header: some View { + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 4) { + Text("terminalX") + .font(TX.Font.mono(34, .medium)) + .tracking(-0.85) + .foregroundStyle(p.text) + Text(subtitle) + .font(TX.Font.mono(12)) + .foregroundStyle(p.textWeak) + } + Spacer() + HStack(spacing: TX.Space.s) { + TXSearchField(text: $search, placeholder: "搜索", shortcut: "⌘K") + .frame(width: 220) + TXButton(title: "新建连接", emphasized: true) { + editing = nil + showEditor = true + } + TXIconButton(icon: "gearshape", label: "设置") { showSettings = true } + } + } + .padding(.horizontal, TX.Space.screenGutter) + .padding(.top, TX.Space.l) + .padding(.bottom, TX.Space.m) + } + + private var subtitle: String { + var parts: [String] = [] + if !manager.sessions.isEmpty { parts.append("\(manager.sessions.count) 个会话在跑") } + parts.append("\(hostStore.hosts.count) 台主机") + if let tailnet = tailscaleStore.connections.first { + parts.append("Tailscale \(tailnet.name)") + } + return parts.joined(separator: " · ") + } + + private var chips: some View { + HStack(spacing: TX.Space.xs) { + TXChip(text: "全部", isSelected: groupFilter == nil) { groupFilter = nil } + ForEach(groups, id: \.self) { g in + TXChip(text: g, isSelected: groupFilter == g) { groupFilter = g } + } + } + } + + /// 首页的「正在连接」行:点主机后到进终端之间的反馈(探测 tmux、等选择、失败)。 +/// 独立视图以直接订阅该会话(`SessionManager` 的转发只保证 manager 自身刷新)。 +struct ConnectingRow: View { + @ObservedObject var session: TerminalSession + let onCancel: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + private var failed: Bool { session.phaseText.hasPrefix("失败") } + + var body: some View { + HStack(spacing: TX.Space.s) { + if failed { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 13)) + .foregroundStyle(p.danger) + } else { + ProgressView().controlSize(.small).tint(TXAccent.base) + } + VStack(alignment: .leading, spacing: 2) { + Text(failed ? "连接 \(session.displayName) 失败" : "正在连接 \(session.displayName)") + .font(TX.Font.mono(13, .medium)) + .foregroundStyle(p.text) + Text(detail) + .font(TX.Font.mono(11)) + .foregroundStyle(failed ? p.danger : p.textWeak) + .lineLimit(1) + } + Spacer(minLength: TX.Space.s) + if failed { + TXButton(title: "重试") { session.retry() } + } + TXButton(title: failed ? "移除" : "取消", action: onCancel) + } + .padding(.horizontal, TX.Space.m) + .padding(.vertical, TX.Space.s) + .frame(maxWidth: .infinity, alignment: .leading) + .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.card) + .strokeBorder(failed ? p.danger.opacity(0.4) : p.border, lineWidth: 1) + ) + .padding(.top, TX.Space.xs) + } + + private var detail: String { + if failed { return String(session.phaseText.dropFirst(3)) } + if session.pendingSessionChoice != nil { return "等待选择 tmux 会话" } + if !session.banner.isEmpty { return session.banner } + return session.phaseText + } +} + +// MARK: - 活动会话卡(3 列等宽) + + /// 已建立的会话。**排除正在连接的**(它们走 ConnectingRow)—— 否则同一会话既有进度行又有卡片, + /// 而且卡片里的 tmux 信息此刻还没定(会先显示成「原生窗口」再跳变)。 + private var liveSessions: [TerminalSession] { + let connecting = Set(manager.connectingSessions.map(\.id)) + return manager.activeSessions.filter { + ($0.isLive || $0.isMinimized) && !connecting.contains($0.id) + } + } + + private var sessionCards: some View { + HStack(alignment: .top, spacing: TX.Space.m) { + ForEach(liveSessions) { s in + SessionCard( + session: s, + shortcutIndex: (liveSessions.firstIndex { $0.id == s.id } ?? 0) + 1, + isFocused: manager.focusedID == s.id + ) { + manager.focus(s) + router.enter(s.id) + } + } + } + .frame(height: 560) + } + + // MARK: - 全部主机(4 列,按内容高度) + + private var hostGrid: some View { + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: TX.Space.m), count: 4), + spacing: TX.Space.m) { + ForEach(filteredHosts) { host in + HostTile(host: host, + credName: credName(for: host), + hasTsnet: host.tailscaleID != nil, + liveSession: manager.sessions.first { $0.host?.id == host.id }) { + // 只发起连接:探测 tmux → 弹会话选择器 → 选完由 manager.enterRequest 进终端。 + manager.open(host: host) + } + .contextMenu { + Button { editing = host; showEditor = true } label: { + Label("编辑", systemImage: "pencil") + } + Button(role: .destructive) { hostStore.remove(host) } label: { + Label("删除", systemImage: "trash") + } + } + } + addHostTile + } + } + + private var addHostTile: some View { + Button { + editing = nil + showEditor = true + } label: { + VStack { + Text("添加主机") + .font(TX.Font.mono(12, .medium)) + .foregroundStyle(p.textWeak) + } + .frame(maxWidth: .infinity, minHeight: 96) + .background( + RoundedRectangle(cornerRadius: TX.Radius.card) + .strokeBorder(style: StrokeStyle(lineWidth: 1, dash: [4, 4])) + .foregroundStyle(p.border) + ) + } + .buttonStyle(.plain) + } + + private func credName(for host: SavedHost) -> String? { + if case .credential(let id) = host.auth { + return credStore.list().first { $0.id == id }?.name + } + return nil + } +} + +/// 首页的「正在连接」行:点主机后到进终端之间的反馈(探测 tmux、等选择、失败)。 +/// 独立视图以直接订阅该会话(`SessionManager` 的转发只保证 manager 自身刷新)。 +struct ConnectingRow: View { + @ObservedObject var session: TerminalSession + let onCancel: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + private var failed: Bool { session.phaseText.hasPrefix("失败") } + + var body: some View { + HStack(spacing: TX.Space.s) { + if failed { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 13)) + .foregroundStyle(p.danger) + } else { + ProgressView().controlSize(.small).tint(TXAccent.base) + } + VStack(alignment: .leading, spacing: 2) { + Text(failed ? "连接 \(session.displayName) 失败" : "正在连接 \(session.displayName)") + .font(TX.Font.mono(13, .medium)) + .foregroundStyle(p.text) + Text(detail) + .font(TX.Font.mono(11)) + .foregroundStyle(failed ? p.danger : p.textWeak) + .lineLimit(1) + } + Spacer(minLength: TX.Space.s) + if failed { + TXButton(title: "重试") { session.retry() } + } + TXButton(title: failed ? "移除" : "取消", action: onCancel) + } + .padding(.horizontal, TX.Space.m) + .padding(.vertical, TX.Space.s) + .frame(maxWidth: .infinity, alignment: .leading) + .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.card) + .strokeBorder(failed ? p.danger.opacity(0.4) : p.border, lineWidth: 1) + ) + .padding(.top, TX.Space.xs) + } + + private var detail: String { + if failed { return String(session.phaseText.dropFirst(3)) } + if session.pendingSessionChoice != nil { return "等待选择 tmux 会话" } + if !session.banner.isEmpty { return session.banner } + return session.phaseText + } +} + +// MARK: - 活动会话卡 + +/// 会话缩略卡:卡头(状态 + 主机名 + tmux + 链路)· 终端文本镜像(底对齐)· 卡脚(元信息 + 主动作)。 +/// 离线/重连中的卡片压一层遮罩 + 说明(设计要求遮罩是内容的最后一层,故用 overlay)。 +struct SessionCard: View { + @ObservedObject var session: TerminalSession + let shortcutIndex: Int + let isFocused: Bool + let onOpen: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + private var statusColor: Color { + switch session.link { + case .direct, .relay: p.online + case .reconnecting: p.reconnecting + case .connecting: p.text4 + case .offline: p.offline + } + } + + private var isTroubled: Bool { + switch session.link { + case .reconnecting, .offline: true + default: false + } + } + + var body: some View { + VStack(spacing: 0) { + cardHead + Rectangle().fill(p.divider).frame(height: 1) + // 离线/重连时缓存内容轻微模糊 + 压遮罩(设计:blur(1.5px) + rgba(24,26,40,.72)), + // 说明「内容还在,只是链路断了」。 + TXTerminalMirror(lines: session.snapshotLines, background: p.terminalBackground) + .blur(radius: isTroubled ? 1.5 : 0) + .overlay { if isTroubled { troubleVeil } } + Rectangle().fill(p.divider).frame(height: 1) + cardFoot + } + .background(p.surfaceDark) + .clipShape(RoundedRectangle(cornerRadius: 13)) + .overlay( + RoundedRectangle(cornerRadius: 13) + .strokeBorder(isFocused ? TXAccent.selectedStroke : p.border, lineWidth: 1) + ) + .shadow(color: .black.opacity(isFocused ? 0.4 : 0.2), radius: isFocused ? 9 : 5, y: 6) + .contentShape(Rectangle()) + .onTapGesture(perform: onOpen) + } + + private var cardHead: some View { + HStack(spacing: TX.Space.xs) { + TXStatusDot(color: statusColor) + Text(session.displayName) + .font(TX.Font.mono(13, .semibold)) + .foregroundStyle(p.text) + .lineLimit(1) + if let t = session.tmuxSummary { + Text(t.name.isEmpty ? "tmux" : "tmux \(t.name)") + .font(TX.Font.mono(11)) + .foregroundStyle(p.textWeak) + .lineLimit(1) + } + Spacer(minLength: TX.Space.xs) + linkTag + } + .padding(.horizontal, TX.Space.s) + .frame(height: 36) + } + + /// 链路标:设计里这格是 RTT 数字,首版无 RTT 管道 → 显示链路类型/重连次数(见 HANDOFF §12.4)。 + @ViewBuilder private var linkTag: some View { + switch session.link { + case .direct: + tag("direct", p.online) + case .relay: + tag("tsnet", p.online) + case .connecting: + tag("连接中", p.text4) + case .reconnecting(let n): + tag("重连中 · 第 \(n) 次", p.reconnecting) + case .offline: + tag("离线", p.offline) + } + } + + private func tag(_ s: String, _ c: Color) -> some View { + Text(s).font(TX.Font.mono(11, .medium)).foregroundStyle(c).lineLimit(1) + } + + /// 遮罩:转圈 + 两行说明。压在终端镜像之上,说明「屏幕内容已缓存,恢复后原样接回」。 + private var troubleVeil: some View { + ZStack { + Color(hex: 0x181A28).opacity(0.72) + VStack(spacing: TX.Space.s) { + if case .reconnecting = session.link { + ProgressView().controlSize(.regular).tint(p.reconnecting) + } + Text(veilTitle) + .font(TX.Font.mono(12, .medium)) + .foregroundStyle(p.reconnecting) + Text("屏幕内容已缓存,恢复后原样接回") + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.text4) + } + .multilineTextAlignment(.center) + .padding(TX.Space.m) + } + } + + private var veilTitle: String { + switch session.link { + case .reconnecting(let n): "\(session.moshEngaged ? "mosh " : "")会话保活中 · 第 \(n) 次重连" + default: "已断开" + } + } + + private var cardFoot: some View { + HStack(spacing: TX.Space.xs) { + Text(footMeta) + .font(TX.Font.mono(11)) + .foregroundStyle(p.textWeak) + .lineLimit(1) + Spacer(minLength: TX.Space.xs) + Text("⌘\(shortcutIndex)") + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.textFaint) + Button(action: onOpen) { + Text(isTroubled ? "立即重试" : "回到会话 →") + .font(TX.Font.mono(11.5, .medium)) + .foregroundStyle(TXAccent.textBright) + } + .buttonStyle(.plain) + } + .padding(.horizontal, TX.Space.s) + .frame(height: 38) + } + + private var footMeta: String { + var parts: [String] = [] + if let t = session.tmuxSummary { + parts.append("\(t.windows) window\(t.windows == 1 ? "" : "s")") + if t.panes > 0 { parts.append("\(t.panes) panes") } + } else { + parts.append("原生窗口") + } + if session.isMinimized { + parts.append("最小化中") + } else { + parts.append(Self.relative(session.lastActiveAt)) + } + return parts.joined(separator: " · ") + } + + /// 「上次活跃」相对时间(秒/分/小时/天)。 + static func relative(_ date: Date) -> String { + let s = Int(Date().timeIntervalSince(date)) + if s < 60 { return "\(max(1, s)) 秒前" } + if s < 3600 { return "\(s / 60) 分钟前" } + if s < 86400 { return "\(s / 3600) 小时前" } + return "\(s / 86400) 天前" + } +} + +// MARK: - 主机卡 + +/// 主机卡(4 列网格):图标 + 名称 + os 副行 + 徽章行 + 右上状态点。 +struct HostTile: View { + let host: SavedHost + let credName: String? + let hasTsnet: Bool + /// 该主机是否已有活动会话(有则右上点亮,点击复用而非新建)。 + let liveSession: TerminalSession? + let onTap: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + private var icon: String { + let os = (host.os ?? "").lowercased() + if os.contains("macos") { return "desktopcomputer" } + if os.contains("raspberry") { return "cpu" } + if os.contains("nas") || host.name.lowercased().contains("nas") { return "externaldrive" } + return "server.rack" + } + + var body: some View { + Button(action: onTap) { + VStack(alignment: .leading, spacing: TX.Space.s) { + HStack(alignment: .top, spacing: TX.Space.s) { + Image(systemName: icon) + .font(.system(size: 15, weight: .light)) + .foregroundStyle(p.text4) + .frame(width: 30, height: 30) + .background(p.surface, in: RoundedRectangle(cornerRadius: 8)) + VStack(alignment: .leading, spacing: 2) { + Text(host.name) + .font(TX.Font.mono(13, .semibold)) + .foregroundStyle(p.text) + .lineLimit(1) + Text(host.os ?? host.addressLine) + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.textWeak) + .lineLimit(1) + } + Spacer(minLength: 0) + if let s = liveSession { + TXStatusDot(color: s.link == .offline ? p.offline + : (s.isMinimized ? TXAccent.mid : p.online)) + } else { + TXStatusDot(color: p.offline.opacity(0.5)) + } + } + HStack(spacing: 5) { + if host.useMosh { TXBadge(text: "mosh") } else { TXBadge(text: "SSH") } + if hasTsnet { TXBadge(text: "tsnet") } + if let credName { TXBadge(text: credName, emphasized: true) } + } + } + .padding(TX.Space.s) + .frame(maxWidth: .infinity, alignment: .leading) + .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.card) + .strokeBorder(p.border, lineWidth: 1) + ) + } + .buttonStyle(.plain) + } +} diff --git a/apps/TerminalX/iOS/HostStore.swift b/apps/TerminalX/iOS/HostStore.swift index a13a2db..d7bfa6e 100644 --- a/apps/TerminalX/iOS/HostStore.swift +++ b/apps/TerminalX/iOS/HostStore.swift @@ -1,18 +1,103 @@ import Foundation -/// 已保存的主机(连接目标)。 -/// 注:`password` 暂存本地 JSON 仅供 demo;产品化后应移入 Keychain(未签名时 SecItem 不可用,见 HANDOFF)。 +/// 已保存的主机(连接目标)。认证经 `AuthRef` 引用凭据或内联密码;出口经可选 `tailscaleID`。 struct SavedHost: Identifiable, Codable, Equatable { var id: UUID = UUID() var name: String var host: String var port: Int = 22 - var username: String - var password: String = "" /// 登录后引导 mosh 承载会话。 var useMosh: Bool = false + /// 登录后自动进入 tmux control mode(`tmux -CC new -A`)。默认开——tmux 是本项目四大核心之一, + /// 会话复用/断线原样接回都靠它。服务器没装 tmux 时会自动降级为原生窗口并给安装引导。 + var useTmux: Bool = true + /// tmux 会话名(`-s`)。nil = 用默认 `main`。 + var tmuxSession: String? + /// 认证方式:内联用户名密码(快捷/旧数据归宿)或引用 Keychain 凭据。 + var auth: AuthRef + /// 出口:nil = Direct 直连;否则经该 Tailscale 连接(egress profile,架构决策 §2.6)。 + var tailscaleID: UUID? + /// 分组(首页「全部主机」的筛选 chips:生产 / 家里 / 云…)。nil = 未分组。 + var group: String? + /// 系统描述(主机卡副行,如「macOS · M4 Max」)。纯展示,不参与连接。 + var os: String? - var addressLine: String { "\(username)@\(host):\(port)" } + /// 认证引用。 + enum AuthRef: Codable, Equatable { + case inlinePassword(username: String, password: String) + case credential(id: UUID) + } + + init(id: UUID = UUID(), name: String, host: String, port: Int = 22, + useMosh: Bool = false, useTmux: Bool = true, tmuxSession: String? = nil, + auth: AuthRef, tailscaleID: UUID? = nil, + group: String? = nil, os: String? = nil) { + self.id = id + self.name = name + self.host = host + self.port = port + self.useMosh = useMosh + self.useTmux = useTmux + self.tmuxSession = tmuxSession + self.auth = auth + self.tailscaleID = tailscaleID + self.group = group + self.os = os + } + + /// 展示地址行;内联凭据带用户名,引用凭据时用户名在凭据里,故只显示 host:port。 + var addressLine: String { + if case .inlinePassword(let user, _) = auth, !user.isEmpty { + return "\(user)@\(host):\(port)" + } + return "\(host):\(port)" + } + + // MARK: - Codable(含旧 schema 迁移) + + enum CodingKeys: String, CodingKey { + case id, name, host, port, useMosh, useTmux, tmuxSession, auth, tailscaleID, group, os + // 旧字段 + case username, password + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decodeIfPresent(UUID.self, forKey: .id) ?? UUID() + name = try c.decode(String.self, forKey: .name) + host = try c.decode(String.self, forKey: .host) + port = try c.decodeIfPresent(Int.self, forKey: .port) ?? 22 + useMosh = try c.decodeIfPresent(Bool.self, forKey: .useMosh) ?? false + // 旧数据没有这两个键:tmux 默认开(此前是"从不自动进 tmux",升级后按设计默认启用)。 + useTmux = try c.decodeIfPresent(Bool.self, forKey: .useTmux) ?? true + tmuxSession = try c.decodeIfPresent(String.self, forKey: .tmuxSession) + tailscaleID = try c.decodeIfPresent(UUID.self, forKey: .tailscaleID) + group = try c.decodeIfPresent(String.self, forKey: .group) + os = try c.decodeIfPresent(String.self, forKey: .os) + if let auth = try c.decodeIfPresent(AuthRef.self, forKey: .auth) { + self.auth = auth + } else { + // 旧 schema:inline username/password → inlinePassword(不自动造幽灵凭据)。 + let user = try c.decodeIfPresent(String.self, forKey: .username) ?? "" + let pw = try c.decodeIfPresent(String.self, forKey: .password) ?? "" + self.auth = .inlinePassword(username: user, password: pw) + } + } + + func encode(to encoder: Encoder) throws { + var c = encoder.container(keyedBy: CodingKeys.self) + try c.encode(id, forKey: .id) + try c.encode(name, forKey: .name) + try c.encode(host, forKey: .host) + try c.encode(port, forKey: .port) + try c.encode(useMosh, forKey: .useMosh) + try c.encode(useTmux, forKey: .useTmux) + try c.encodeIfPresent(tmuxSession, forKey: .tmuxSession) + try c.encode(auth, forKey: .auth) + try c.encodeIfPresent(tailscaleID, forKey: .tailscaleID) + try c.encodeIfPresent(group, forKey: .group) + try c.encodeIfPresent(os, forKey: .os) + } } /// 主机列表持久化(Application Support 下 JSON)。凭据不入仓库;此文件只在设备本地。 @@ -45,6 +130,13 @@ final class HostStore: ObservableObject { save() } + #if DEBUG + /// 无头 UI 验证:把示意主机塞进内存(**不写盘**,不污染真实 hosts.json)。 + func injectFixtureHosts(_ hosts: [SavedHost]) { + self.hosts = hosts + } + #endif + private func load() { guard let data = try? Data(contentsOf: url), let decoded = try? JSONDecoder().decode([SavedHost].self, from: data) else { return } diff --git a/apps/TerminalX/iOS/HostsView.swift b/apps/TerminalX/iOS/HostsView.swift new file mode 100644 index 0000000..1dec430 --- /dev/null +++ b/apps/TerminalX/iOS/HostsView.swift @@ -0,0 +1,350 @@ +import SwiftUI + +/// 主机列表页(split 详情)。点卡片连接;右上 + 新建;上下文菜单编辑/删除。 +struct HostsListView: View { + @ObservedObject var manager: SessionManager + @ObservedObject var router: AppRouter + @ObservedObject var hostStore: HostStore + @ObservedObject var credStore: FileCredentialStore + @ObservedObject var tailscaleStore: TailscaleStore + @EnvironmentObject var theme: TXThemeManager + @State private var editing: SavedHost? + @State private var showEditor = false + private var p: TXPalette { theme.palette } + + var body: some View { + ZStack { + p.bg.ignoresSafeArea() + ScrollView { + VStack(alignment: .leading, spacing: TX.Space.m) { + if hostStore.hosts.isEmpty { + TXEmptyState(icon: "server.rack", title: "还没有主机", + subtitle: "点击右上角 + 添加一台服务器") + } else { + ForEach(hostStore.hosts) { host in + HostCard(host: host, + credName: credName(for: host), + tailscaleName: tailscaleName(for: host)) { + // 设计定稿:建好即进终端(连接进度显示在终端页);已有该主机会话则复用。 + let s = manager.open(host: host) + router.enter(s.id) + } + .contextMenu { + Button { editing = host; showEditor = true } label: { + Label("编辑", systemImage: "pencil") + } + Button(role: .destructive) { hostStore.remove(host) } label: { + Label("删除", systemImage: "trash") + } + } + } + } + // 独立订阅焦点会话:manager 是嵌套 ObservableObject,会话内部变化不冒泡到这里。 + if let s = manager.focused { SessionStatusRow(session: s) } + } + .padding(TX.Space.m) + } + } + .navigationTitle("主机") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(p.bg, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { editing = nil; showEditor = true } label: { Image(systemName: "plus") } + } + } + .sheet(isPresented: $showEditor) { + HostEditorView(existing: editing, credStore: credStore, tailscaleStore: tailscaleStore) { host in + if hostStore.hosts.contains(where: { $0.id == host.id }) { hostStore.update(host) } + else { hostStore.add(host) } + } + } + } + + private func credName(for host: SavedHost) -> String? { + if case .credential(let id) = host.auth { + return credStore.list().first { $0.id == id }?.name ?? "(凭据已删除)" + } + return nil + } + + private func tailscaleName(for host: SavedHost) -> String? { + guard let id = host.tailscaleID else { return nil } + return tailscaleStore.connection(id: id)?.name ?? "(连接已删除)" + } + +} + +/// 焦点会话的连接进度/失败提示(连接中或失败时才出现)。 +/// 单独成视图以 `@ObservedObject` 直接订阅该会话——`SessionManager` 不会转发会话内部变化。 +struct SessionStatusRow: View { + @ObservedObject var session: TerminalSession + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + private var isBusy: Bool { + session.phaseText.hasSuffix("…") || session.phaseText.contains("重连") + } + + var body: some View { + if isBusy || session.phaseText.hasPrefix("失败") { + HStack(spacing: TX.Space.s) { + if isBusy { ProgressView().controlSize(.small).tint(p.accent) } + Text(session.phaseText) + .font(.system(size: 13)) + .foregroundStyle(session.phaseText.hasPrefix("失败") ? p.danger : p.textSecondary) + } + .padding(.horizontal, 4) + } + } +} + +/// 主机卡片:图标 + mosh 点 / 名称 / 地址(等宽)/ 凭据·Tailscale 徽章 / chevron。 +struct HostCard: View { + let host: SavedHost + let credName: String? + let tailscaleName: String? + let onTap: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + Button(action: onTap) { + HStack(spacing: TX.Space.m) { + ZStack(alignment: .topTrailing) { + Image(systemName: "server.rack") + .font(.system(size: 20, weight: .regular)) + .foregroundStyle(p.textSecondary) + .frame(width: 44, height: 44) + .background(p.surfaceElevated, in: RoundedRectangle(cornerRadius: TX.Radius.chip)) + if host.useMosh { + Circle().fill(p.mosh).frame(width: 9, height: 9) + .overlay(Circle().stroke(p.surface, lineWidth: 2)) + .offset(x: 3, y: -3) + } + } + VStack(alignment: .leading, spacing: 3) { + Text(host.name) + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(p.textPrimary) + Text(host.addressLine) + .font(TX.Font.mono(13)) + .foregroundStyle(p.textSecondary) + if credName != nil || tailscaleName != nil { + HStack(spacing: TX.Space.xs) { + if let c = credName { badge("key.fill", c, p.accent) } + if let t = tailscaleName { badge("network", t, p.mosh) } + } + } + } + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(p.textTertiary) + } + .padding(TX.Space.m) + .background(p.surface, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + .overlay(RoundedRectangle(cornerRadius: TX.Radius.card).stroke(p.border, lineWidth: 1)) + } + .buttonStyle(.plain) + } + + private func badge(_ icon: String, _ text: String, _ color: Color) -> some View { + HStack(spacing: 3) { + Image(systemName: icon).font(.system(size: 9)) + Text(text).font(.system(size: 11, weight: .medium)).lineLimit(1) + } + .foregroundStyle(color) + .padding(.horizontal, 7).padding(.vertical, 3) + .background(color.opacity(0.13), in: Capsule()) + } +} + +/// 主机编辑页:基本信息 + 认证(直接输入 / 选 Keychain)+ 网络(Direct / Tailscale)+ 选项。 +struct HostEditorView: View { + let existing: SavedHost? + @ObservedObject var credStore: FileCredentialStore + @ObservedObject var tailscaleStore: TailscaleStore + /// 已有分组(供一键选,避免录入同义不同名)。 + var existingGroups: [String] = [] + let onSave: (SavedHost) -> Void + + @EnvironmentObject var theme: TXThemeManager + @Environment(\.dismiss) private var dismiss + + private enum AuthMode: Hashable { case inline, credential } + + @State private var name = "" + @State private var host = "" + @State private var port = "22" + @State private var authMode: AuthMode = .inline + @State private var inlineUser = "" + @State private var inlinePassword = "" + @State private var selectedCredentialID: UUID? + @State private var tailscaleID: UUID? + @State private var useMosh = false + @State private var showNewCredential = false + /// 分组(首页筛选 chips)与系统描述(主机卡副行)——真实数据只能从这里录入。 + @State private var group = "" + @State private var os = "" + @State private var useTmux = true + @State private var tmuxSession = "" + + private var p: TXPalette { theme.palette } + private var canSave: Bool { + guard !host.isEmpty else { return false } + switch authMode { + case .inline: return !inlineUser.isEmpty + case .credential: return selectedCredentialID != nil + } + } + + var body: some View { + NavigationStack { + ZStack { + p.bg.ignoresSafeArea() + Form { + Section("基本") { + TXTextField(title: "名称", text: $name, placeholder: host.isEmpty ? "我的服务器" : host) + TXTextField(title: "主机 / IP", text: $host) + TXTextField(title: "端口", text: $port, keyboard: .numberPad) + } + Section { + TXTextField(title: "分组", text: $group, placeholder: "生产 / 家里 / 云(可空)") + if !existingGroups.isEmpty { + // 已有分组一键选,避免同义不同名把 chips 撑爆。 + HStack(spacing: TX.Space.xs) { + ForEach(existingGroups, id: \.self) { g in + TXChip(text: g, isSelected: group == g) { + group = (group == g) ? "" : g + } + } + } + .txRow(p) + } + TXTextField(title: "系统", text: $os, placeholder: "macOS · M4 Max(可空)") + } header: { + Text("归类(首页展示用)") + } footer: { + Text("分组决定首页「全部主机」的筛选 chips;系统只作卡片副行展示,不参与连接。") + } + authSection + networkSection + Section { + Toggle("自动进入 tmux", isOn: $useTmux).tint(p.accent).txRow(p) + if useTmux { + TXTextField(title: "tmux 会话名", text: $tmuxSession, placeholder: "main") + } + Toggle("使用 mosh", isOn: $useMosh).tint(p.accent).txRow(p) + } header: { + Text("会话") + } footer: { + Text(useTmux + ? "登录后执行 tmux -CC new -A -s <会话名>:同名会话存在就接回,否则新建。服务器没装 tmux 会自动降级为客户端窗口并给安装引导。" + : "不进 tmux:会话由 App 维持,关掉 App 后服务端不会继续跑(mosh 仍能兜断线)。") + } + } + .scrollContentBackground(.hidden) + } + .navigationTitle(existing == nil ? "添加主机" : "编辑主机") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("取消") { dismiss() }.tint(p.textSecondary) + } + ToolbarItem(placement: .confirmationAction) { + Button("保存") { save() }.disabled(!canSave).tint(p.accent) + } + } + .toolbarBackground(p.bg, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .sheet(isPresented: $showNewCredential) { + CredentialEditorView(existing: nil, store: credStore) { newCred in + selectedCredentialID = newCred.id + authMode = .credential + } + } + .onAppear(perform: populate) + } + .tint(p.accent) + } + + @ViewBuilder private var authSection: some View { + Section("认证") { + Picker("方式", selection: $authMode) { + Text("直接输入").tag(AuthMode.inline) + Text("从 Keychain 选择").tag(AuthMode.credential) + } + .pickerStyle(.segmented) + .txRow(p) + + if authMode == .inline { + TXTextField(title: "用户名", text: $inlineUser) + TXTextField(title: "密码", text: $inlinePassword, secure: true) + } else { + Picker("凭据", selection: $selectedCredentialID) { + Text("未选择").tag(UUID?.none) + ForEach(credStore.credentials) { c in + Text("\(c.name) · \(c.username)").tag(UUID?.some(c.id)) + } + } + .txRow(p) + Button { showNewCredential = true } label: { + Label("新建凭据", systemImage: "plus.circle") + } + .txRow(p) + } + } + } + + @ViewBuilder private var networkSection: some View { + Section("网络(出口)") { + Picker("经由", selection: $tailscaleID) { + Text("Direct(直连)").tag(UUID?.none) + ForEach(tailscaleStore.connections) { conn in + Text(conn.name).tag(UUID?.some(conn.id)) + } + } + .txRow(p) + if let id = tailscaleID, let conn = tailscaleStore.connection(id: id), conn.needsAuthKey { + Text("该 Tailscale 连接尚未认证,请先在 Tailscale 页完成。") + .font(.system(size: 12)).foregroundStyle(p.warning).txRow(p) + } + } + } + + private func populate() { + guard let h = existing else { return } + name = h.name; host = h.host; port = String(h.port) + useMosh = h.useMosh; tailscaleID = h.tailscaleID + group = h.group ?? ""; os = h.os ?? "" + useTmux = h.useTmux; tmuxSession = h.tmuxSession ?? "" + switch h.auth { + case .inlinePassword(let u, let pw): + authMode = .inline; inlineUser = u; inlinePassword = pw + case .credential(let id): + authMode = .credential; selectedCredentialID = id + } + } + + private func save() { + let auth: SavedHost.AuthRef + switch authMode { + case .inline: auth = .inlinePassword(username: inlineUser, password: inlinePassword) + case .credential: + guard let id = selectedCredentialID else { return } + auth = .credential(id: id) + } + let saved = SavedHost( + id: existing?.id ?? UUID(), + name: name.isEmpty ? host : name, + host: host, port: Int(port) ?? 22, + useMosh: useMosh, useTmux: useTmux, + tmuxSession: tmuxSession.isEmpty ? nil : tmuxSession.trimmingCharacters(in: .whitespaces), + auth: auth, tailscaleID: tailscaleID, + group: group.isEmpty ? nil : group.trimmingCharacters(in: .whitespaces), + os: os.isEmpty ? nil : os.trimmingCharacters(in: .whitespaces)) + onSave(saved) + dismiss() + } +} diff --git a/apps/TerminalX/iOS/KeychainKnownHosts.swift b/apps/TerminalX/iOS/KeychainKnownHosts.swift index baf9c92..015555f 100644 --- a/apps/TerminalX/iOS/KeychainKnownHosts.swift +++ b/apps/TerminalX/iOS/KeychainKnownHosts.swift @@ -1,6 +1,15 @@ import Foundation import TXCore +/// 已信任 host key 的展示条目(设置 → Known Hosts)。 +struct KnownHostEntry: Identifiable { + let id: String // accountKey "egress|host|port" + let egress: String + let host: String + let port: Int + let fingerprint: String +} + /// known_hosts pin 存储(文件后端)。 /// /// 为何用文件而非 Keychain:本 app 当前未签名(CODE_SIGNING_ALLOWED=NO),无 keychain-access-group @@ -47,6 +56,27 @@ final class KeychainKnownHostsStore: KnownHostsStore, @unchecked Sendable { persist() } + /// 已信任 host key 列表(供设置里查看/删除)。 + func entries() -> [KnownHostEntry] { + lock.lock(); defer { lock.unlock() } + return cache.compactMap { key, entry in + guard let b64 = entry["blob"] as? String, let blob = Data(base64Encoded: b64) else { return nil } + let parts = key.split(separator: "|", maxSplits: 2).map(String.init) + return KnownHostEntry( + id: key, + egress: parts.count > 0 ? parts[0] : "", + host: parts.count > 1 ? parts[1] : "", + port: parts.count > 2 ? (Int(parts[2]) ?? 0) : 0, + fingerprint: HostKey.opensshFingerprint(blob)) + }.sorted { $0.host < $1.host } + } + + func removeKey(_ id: String) { + lock.lock(); defer { lock.unlock() } + cache[id] = nil + persist() + } + private func persist() { guard let data = try? JSONSerialization.data(withJSONObject: cache) else { return } try? data.write(to: url, options: .atomic) diff --git a/apps/TerminalX/iOS/KeychainView.swift b/apps/TerminalX/iOS/KeychainView.swift new file mode 100644 index 0000000..67547da --- /dev/null +++ b/apps/TerminalX/iOS/KeychainView.swift @@ -0,0 +1,358 @@ +import SwiftUI +import UniformTypeIdentifiers +import TXCore + +/// Keychain 凭据列表页。 +struct KeychainListView: View { + @ObservedObject var store: FileCredentialStore + @EnvironmentObject var theme: TXThemeManager + @State private var editing: Credential? + @State private var showEditor = false + private var p: TXPalette { theme.palette } + + var body: some View { + ZStack { + p.bg.ignoresSafeArea() + ScrollView { + VStack(alignment: .leading, spacing: TX.Space.s) { + if store.credentials.isEmpty { + TXEmptyState(icon: "key.fill", title: "还没有凭据", + subtitle: "添加密码或 SSH 私钥,供主机复用") + } else { + ForEach(store.credentials) { cred in + credRow(cred) + .contextMenu { + Button { editing = cred; showEditor = true } label: { + Label("编辑", systemImage: "pencil") + } + Button(role: .destructive) { store.delete(cred.id) } label: { + Label("删除", systemImage: "trash") + } + } + } + } + } + .padding(TX.Space.m) + } + } + .navigationTitle("Keychain 凭据") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(p.bg, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { editing = nil; showEditor = true } label: { Image(systemName: "plus") } + } + } + .sheet(isPresented: $showEditor) { + CredentialEditorView(existing: editing, store: store) { _ in } + } + } + + private func credRow(_ cred: Credential) -> some View { + HStack(spacing: TX.Space.m) { + Image(systemName: cred.iconName) + .font(.system(size: 18)) + .foregroundStyle(p.accent) + .frame(width: 40, height: 40) + .background(p.surfaceElevated, in: RoundedRectangle(cornerRadius: TX.Radius.chip)) + VStack(alignment: .leading, spacing: 2) { + Text(cred.name).font(.system(size: 15, weight: .semibold)).foregroundStyle(p.textPrimary) + Text("\(cred.kindLabel) · \(cred.username)") + .font(TX.Font.mono(12)).foregroundStyle(p.textSecondary) + } + Spacer() + } + .padding(TX.Space.m) + .background(p.surface, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + .overlay(RoundedRectangle(cornerRadius: TX.Radius.card).stroke(p.border, lineWidth: 1)) + } +} + +/// 凭据编辑页:类型(密码 / SSH 密钥 / Face ID);SSH 密钥支持 粘贴 / 导入 / 生成。 +struct CredentialEditorView: View { + let existing: Credential? + @ObservedObject var store: FileCredentialStore + let onSave: (Credential) -> Void + + @EnvironmentObject var theme: TXThemeManager + @Environment(\.dismiss) private var dismiss + + private enum Kind: Hashable { case password, sshKey, faceID } + private enum KeySource: Hashable { case generate, paste, importFile } + + @State private var name = "" + @State private var username = "" + @State private var kind: Kind = .password + @State private var password = "" + @State private var keySource: KeySource = .generate + @State private var keyText = "" + @State private var passphrase = "" + @State private var parsed: CredentialAuth.ParsedKey? + // 生成结果(暂存直到保存) + @State private var generatedKind: Credential.Kind? + @State private var generatedSecret: CredentialSecret? + @State private var generatedFingerprint = "" + @State private var generatedAuthLine = "" + @State private var showFileImporter = false + @State private var importError: String? + + private let seAvailable = SigningKeyProvider.secureEnclaveAvailable() + private let isEditing: Bool + private var p: TXPalette { theme.palette } + + init(existing: Credential?, store: FileCredentialStore, onSave: @escaping (Credential) -> Void) { + self.existing = existing + self.store = store + self.onSave = onSave + self.isEditing = existing != nil + } + + private var canSave: Bool { + guard !username.isEmpty else { return false } + switch kind { + case .password: return true + case .sshKey: + if isEditing { return true } // 编辑时允许只改名/用户名 + switch keySource { + case .generate: return generatedSecret != nil + case .paste, .importFile: return !keyText.isEmpty + } + case .faceID: return false + } + } + + var body: some View { + NavigationStack { + ZStack { + p.bg.ignoresSafeArea() + Form { + Section("基本") { + TXTextField(title: "名称", text: $name, placeholder: "我的密钥") + TXTextField(title: "用户名", text: $username, placeholder: "登录服务器的用户名") + } + Section("类型") { + Picker("类型", selection: $kind) { + Text("密码").tag(Kind.password) + Text("SSH 密钥").tag(Kind.sshKey) + Text("Face ID").tag(Kind.faceID) + } + .pickerStyle(.segmented) + .txRow(p) + } + switch kind { + case .password: passwordSection + case .sshKey: sshKeySection + case .faceID: faceIDSection + } + } + .scrollContentBackground(.hidden) + } + .navigationTitle(isEditing ? "编辑凭据" : "添加凭据") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("取消") { dismiss() }.tint(p.textSecondary) + } + ToolbarItem(placement: .confirmationAction) { + Button("保存") { save() }.disabled(!canSave).tint(p.accent) + } + } + .toolbarBackground(p.bg, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .fileImporter(isPresented: $showFileImporter, allowedContentTypes: [.data, .text]) { result in + handleImport(result) + } + .onAppear(perform: populate) + } + .tint(p.accent) + } + + // MARK: - 各类型区块 + + @ViewBuilder private var passwordSection: some View { + Section("密码") { + TXTextField(title: "密码", text: $password, secure: true) + } + } + + @ViewBuilder private var sshKeySection: some View { + Section("SSH 私钥") { + Picker("来源", selection: $keySource) { + Text("生成").tag(KeySource.generate) + Text("粘贴").tag(KeySource.paste) + Text("导入").tag(KeySource.importFile) + } + .pickerStyle(.segmented) + .txRow(p) + + switch keySource { + case .generate: generateContent + case .paste: pasteContent + case .importFile: importContent + } + } + } + + @ViewBuilder private var generateContent: some View { + if generatedSecret == nil { + Button { + generate() + } label: { + Label("生成 ECDSA P-256 密钥", systemImage: "sparkles") + } + .txRow(p) + } else { + VStack(alignment: .leading, spacing: 6) { + Text("已生成 ECDSA P-256").font(.system(size: 13, weight: .medium)).foregroundStyle(p.accent) + Text("公钥指纹:\(generatedFingerprint)").font(TX.Font.mono(11)).foregroundStyle(p.textSecondary) + Text("authorized_keys(复制到服务器):").font(.system(size: 12)).foregroundStyle(p.textSecondary) + Text(generatedAuthLine).font(TX.Font.mono(10)).foregroundStyle(p.textPrimary) + .textSelection(.enabled).lineLimit(3) + Button { UIPasteboard.general.string = generatedAuthLine } label: { + Label("复制 authorized_keys 行", systemImage: "doc.on.doc") + } + .font(.system(size: 13)).padding(.top, 4) + } + .txRow(p) + } + } + + @ViewBuilder private var pasteContent: some View { + VStack(alignment: .leading, spacing: 6) { + Text("粘贴 PEM / PKCS#8 私钥内容").font(.system(size: 12)).foregroundStyle(p.textSecondary) + TextEditor(text: $keyText) + .font(TX.Font.mono(11)).frame(minHeight: 120) + .foregroundStyle(p.textPrimary).scrollContentBackground(.hidden) + .background(p.bg, in: RoundedRectangle(cornerRadius: 8)) + .onChange(of: keyText) { _, t in parsed = CredentialAuth.parsePrivateKey(t, source: .pasted) } + detectionLabel + } + .txRow(p) + if parsedNeedsPassphrase { + TXTextField(title: "私钥口令(passphrase)", text: $passphrase, secure: true) + } + } + + @ViewBuilder private var importContent: some View { + VStack(alignment: .leading, spacing: 6) { + Button { showFileImporter = true } label: { + Label("从文件导入私钥", systemImage: "folder") + } + if let e = importError { + Text(e).font(.system(size: 12)).foregroundStyle(.red) + } + if !keyText.isEmpty { detectionLabel } + } + .txRow(p) + if parsedNeedsPassphrase { + TXTextField(title: "私钥口令(passphrase)", text: $passphrase, secure: true) + } + } + + @ViewBuilder private var faceIDSection: some View { + Section { + HStack { + Image(systemName: "faceid").foregroundStyle(seAvailable ? p.accent : p.textTertiary) + Text(seAvailable ? "Face ID 密钥(Secure Enclave)" : "Face ID 密钥(需签名构建)") + .foregroundStyle(seAvailable ? p.textPrimary : p.textTertiary) + } + .txRow(p) + } footer: { + Text(seAvailable + ? "私钥生成于 Secure Enclave,不可导出,签名时经 Face ID 授权。" + : "当前构建未签名,Secure Enclave 不可用。签名真机构建后此选项自动可用。") + } + } + + @ViewBuilder private var detectionLabel: some View { + if let parsed { + VStack(alignment: .leading, spacing: 3) { + Text("检测到:\(parsed.formatLabel)").font(.system(size: 12)).foregroundStyle(p.accent) + if let w = parsed.warning { + Text(w).font(.system(size: 11)).foregroundStyle(p.warning) + } + } + } + } + + private var parsedNeedsPassphrase: Bool { + if case .privateKey(_, _, let hasPass, _) = parsed?.kind { return hasPass } + return false + } + + // MARK: - 动作 + + private func generate() { + guard let (k, s) = CredentialAuth.generateECDSAP256() else { return } + generatedKind = k; generatedSecret = s + if case .privateKey(_, _, _, let blob) = k { + generatedAuthLine = "ecdsa-sha2-nistp256 \(blob.base64EncodedString()) \(username.isEmpty ? "terminalx" : username)@terminalx" + generatedFingerprint = HostKey.opensshFingerprint(blob) + } + } + + private func handleImport(_ result: Result) { + switch result { + case .failure(let e): importError = e.localizedDescription + case .success(let url): + let scoped = url.startAccessingSecurityScopedResource() + defer { if scoped { url.stopAccessingSecurityScopedResource() } } + guard let data = try? Data(contentsOf: url), let text = String(data: data, encoding: .utf8) else { + importError = "无法读取文件(需 UTF-8 文本私钥)"; return + } + importError = nil + keyText = text + parsed = CredentialAuth.parsePrivateKey(text, source: .imported) + } + } + + private func populate() { + guard let c = existing else { return } + name = c.name; username = c.username + switch c.kind { + case .password: + kind = .password + if case .password(let pw)? = store.secret(for: c.id) { password = pw } + case .privateKey: + kind = .sshKey; keySource = .generate // 编辑既有密钥仅改名/用户名 + case .secureEnclave: + kind = .faceID + } + } + + private func save() { + let credID = existing?.id ?? UUID() + var credKind: Credential.Kind + var secret: CredentialSecret? + + switch kind { + case .password: + credKind = .password + secret = .password(password) + case .sshKey: + if isEditing, let c = existing, case .privateKey = c.kind { + credKind = c.kind; secret = nil // 保留原机密,仅改 meta + } else if keySource == .generate, let gk = generatedKind, let gs = generatedSecret { + credKind = gk; secret = gs + } else { + let parsedKey = parsed ?? CredentialAuth.parsePrivateKey(keyText, source: keySource == .paste ? .pasted : .imported) + // 注入用户填写的 passphrase + if case .privateKey(let t, let src, _, let blob) = parsedKey.kind { + credKind = .privateKey(keyType: t, source: src, hasPassphrase: !passphrase.isEmpty, publicKeyBlob: blob) + } else { + credKind = parsedKey.kind + } + secret = .pemPrivateKey(pem: Data(keyText.utf8), passphrase: passphrase.isEmpty ? nil : passphrase) + } + case .faceID: + return + } + + let cred = Credential(id: credID, name: name.isEmpty ? username : name, + username: username, kind: credKind, + createdAt: existing?.createdAt ?? Date()) + store.upsert(cred, secret: secret) + onSave(cred) + dismiss() + } +} diff --git a/apps/TerminalX/iOS/RailSidebar.swift b/apps/TerminalX/iOS/RailSidebar.swift new file mode 100644 index 0000000..5bea47b --- /dev/null +++ b/apps/TerminalX/iOS/RailSidebar.swift @@ -0,0 +1,529 @@ +import SwiftUI + +// 设计稿 `1b 终端-沉浸轨道` 的左侧 56pt 轨道 + `3a 侧边栏-遮罩态/Pin 态` 的 288pt 面板。 +// +// **开合语义(唯一、不歧义)**:`›` 方块是 App logo,不可点。开合只有一枚按钮,两态方向相反—— +// 折叠态在轨道底部(»展开),展开态在标题右侧(«收起)。Pin 是独立按钮,只在展开态出现。 +// 三态元素不增不减,只是展示密度不同。 + +/// 折叠轨道(56pt,竖屏 48pt)。元素中心线统一在轨道正中。 +struct RailView: View { + @ObservedObject var manager: SessionManager + @ObservedObject var router: AppRouter + /// 当前会话(TMUX 段列它的 window)。 + @ObservedObject var session: TerminalSession + let onNewHost: () -> Void + let onSettings: () -> Void + @EnvironmentObject var theme: TXThemeManager + @Environment(\.horizontalSizeClass) private var hSize + private var p: TXPalette { theme.palette } + + private var width: CGFloat { + hSize == .compact ? TX.Layout.railWidthCompact : TX.Layout.railWidth + } + private var avatarSize: CGFloat { hSize == .compact ? 34 : 38 } + + // 纵向节奏取自设计稿 `1b` 的实测坐标(1366×1024 画板扫描): + // logo 28..62 · 搜索 75..109 · 首页 118..152 · 分隔 160 · 头像 174/220/266(间距 8) + // · 虚线+ 313..351 · 分隔 370 · TMUX 标签 382 · window 数字 408 起(间距 7) + // · 底部自下而上:齿轮 985..1019 · 展开键 932..967 · 状态点 914。 + var body: some View { + VStack(spacing: 0) { + logo + .padding(.top, 28) + .padding(.bottom, 13) + // 搜索 / 首页 / 设置在设计稿里是**裸图标**(无底无框),只有 logo 与展开键有框。 + TXIconButton(icon: "magnifyingglass", bare: true, hitSize: 34, label: "搜索") { + router.toggleSidebar() + } + .padding(.bottom, 9) + TXIconButton(icon: "house", bare: true, hitSize: 34, label: "首页") { router.goHome() } + .padding(.bottom, 8) + divider + .padding(.bottom, 13) + hostAvatars + newHostButton + divider + .padding(.top, 9) + .padding(.bottom, 12) + tmuxSection + Spacer(minLength: TX.Space.s) + TXStatusDot(color: linkColor, punchOut: p.surfaceDark) + .padding(.bottom, 11) + // 折叠态的开合键:方向朝右(展开)。设计稿里这一枚**有框**。 + TXIconButton(icon: "chevron.right.2", hitSize: 34, label: "展开侧边栏") { + router.toggleSidebar() + } + .padding(.bottom, 18) + TXIconButton(icon: "gearshape", bare: true, hitSize: 34, label: "设置", action: onSettings) + .padding(.bottom, 5) + } + .frame(width: width) + .background(p.surfaceDark) + } + + /// App logo(`›`)。**不可点** —— 开合是另一枚按钮的职责。 + private var logo: some View { + Text("›") + .font(TX.Font.mono(16, .bold)) + .foregroundStyle(TXAccent.text) + .frame(width: 34, height: 34) + .background(p.surface, in: RoundedRectangle(cornerRadius: 11)) + .overlay(RoundedRectangle(cornerRadius: 11).strokeBorder(p.border, lineWidth: 1)) + .accessibilityLabel("terminalX") + } + + private var divider: some View { + Rectangle() + .fill(p.divider) + .frame(width: 24, height: 1) + } + + private var hostAvatars: some View { + VStack(spacing: 8) { + ForEach(manager.activeSessions) { s in + Button { + manager.focus(s) + router.enter(s.id) + } label: { + TXInitialsAvatar(initials: s.initials, + size: avatarSize, + isSelected: router.route.sessionID == s.id, + status: RailSidebarStyle.statusColor(s, p), + punchOut: p.surfaceDark) + } + .buttonStyle(.plain) + } + } + } + + /// 虚线 + 号:在当前主机之外新开会话。 + private var newHostButton: some View { + Button(action: onNewHost) { + Image(systemName: "plus") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(p.textWeak) + .frame(width: avatarSize, height: avatarSize) + .background( + RoundedRectangle(cornerRadius: 11) + .strokeBorder(style: StrokeStyle(lineWidth: 1, dash: [3, 3])) + .foregroundStyle(p.border) + ) + } + .buttonStyle(.plain) + .padding(.top, 9) + } + + /// TMUX 段:标签 + window 数字。无 tmux 的主机换成「窗口」段(客户端窗口)。 + private var tmuxSection: some View { + VStack(spacing: 7) { + Text(session.tmuxSummary == nil ? "窗口" : "TMUX") + .font(TX.Font.mono(9, .semibold)) + .tracking(0.9) + // 设计稿:轨道的 TMUX 段标签用 accent-500(弱化强调),不是中性灰。 + .foregroundStyle(TXAccent.mid) + .padding(.bottom, 9) + if let tmux = session.tmuxController { + ForEach(tmux.windows) { win in + Button { tmux.selectWindow(win.id) } label: { + windowNumber("\(win.index)", isActive: win.id == tmux.activeWindowID) + } + .buttonStyle(.plain) + } + } else { + // 还没 attach(或假会话)时按概览里的 window 数占位,保持轨道高度稳定。 + ForEach(0 ..< max(1, session.tmuxSummary?.windows ?? 1), id: \.self) { i in + windowNumber("\(i)", isActive: i == 0) + } + } + } + } + + /// window 数字格(34×30,选中态填控件底 + 中等描边)。 + private func windowNumber(_ text: String, isActive: Bool) -> some View { + Text(text) + .font(TX.Font.mono(12, isActive ? .semibold : .regular)) + .foregroundStyle(isActive ? p.text : p.text4) + .frame(width: 34, height: 30) + .background(isActive ? p.surface : .clear, in: RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .strokeBorder(isActive ? p.borderDialog : .clear, lineWidth: 1) + ) + } + + private var linkColor: Color { RailSidebarStyle.statusColor(session, p) } +} + +/// 轨道与面板共用的状态色映射。 +@MainActor +enum RailSidebarStyle { + static func statusColor(_ s: TerminalSession, _ p: TXPalette) -> Color { + switch s.link { + case .direct, .relay: s.isMinimized ? TXAccent.mid : p.online + case .reconnecting: p.reconnecting + case .connecting: p.text4 + case .offline: p.offline + } + } +} + +// MARK: - 288pt 侧边栏面板 + +/// 展开态面板(遮罩 / Pin 共用同一套内容)。 +/// +/// 行规格:会话行 = 26pt 头像列 + 10pt gap;window 行 = 26×22pt 徽标列 + 10pt gap +/// → 两者标题左缘对齐。选中态用 **inset 描边**(`strokeBorder`)而非 border,避免内容位移 1pt。 +struct SidebarPanel: View { + @ObservedObject var manager: SessionManager + @ObservedObject var router: AppRouter + @ObservedObject var session: TerminalSession + @ObservedObject var tailscaleStore: TailscaleStore + let onNewHost: () -> Void + let onSettings: () -> Void + @Binding var search: String + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + header + TXSearchField(text: $search, placeholder: "搜索主机 · 会话") + .padding(.horizontal, TX.Space.m) + .padding(.bottom, TX.Space.s) + homeRow + sessionsSection + // 设计:1pt **渐隐**分隔(两端淡出,不是通栏实线)。 + LinearGradient(colors: [p.divider.opacity(0), p.divider, p.divider.opacity(0)], + startPoint: .leading, endPoint: .trailing) + .frame(height: 1) + .padding(.horizontal, TX.Space.m) + .padding(.vertical, TX.Space.s) + tmuxSection + Spacer(minLength: 0) + tailscaleFoot + } + .frame(width: TX.Layout.sidebarWidth) + .background(p.surfaceDark) + } + + private var header: some View { + HStack(spacing: TX.Space.s) { + Text("›") + .font(TX.Font.mono(15, .bold)) + .foregroundStyle(TXAccent.text) + .frame(width: 30, height: 30) + .background(p.surface, in: RoundedRectangle(cornerRadius: 9)) + .overlay(RoundedRectangle(cornerRadius: 9).strokeBorder(p.border, lineWidth: 1)) + Text("terminalX") + .font(TX.Font.mono(14, .medium)) + .foregroundStyle(p.text) + Spacer() + // Pin 只在展开态出现;钉住时高亮。 + TXIconButton(icon: "pin", size: 28, isSelected: router.sidebar == .pinned, + label: "钉在左侧") { router.togglePin() } + // 展开态的开合键:方向朝左(收起)。 + TXIconButton(icon: "chevron.left.2", size: 28, label: "收起") { router.toggleSidebar() } + } + .padding(.horizontal, TX.Space.m) + .padding(.top, 11) + .padding(.bottom, 6) + } + + private var homeRow: some View { + Button { router.goHome() } label: { + HStack(spacing: TX.Space.s) { + Image(systemName: "house") + .font(.system(size: 13)) + .foregroundStyle(p.text2) + .frame(width: 26) + Text("首页") + .font(TX.Font.mono(13, .medium)) + .foregroundStyle(p.text) + Spacer() + Text("⌘⇧H") + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.textFaint) + } + .padding(.horizontal, TX.Space.m) + .frame(height: 36) + } + .buttonStyle(.plain) + } + + private var sessionsSection: some View { + VStack(alignment: .leading, spacing: 2) { + TXSectionLabel(text: "活动会话", trailing: "\(manager.sessions.count)") + .padding(.horizontal, TX.Space.m) + .padding(.top, TX.Space.s) + .padding(.bottom, 2) + if manager.activeSessions.isEmpty { + Text("没有在跑的会话") + .font(TX.Font.mono(11.5)) + .foregroundStyle(p.textWeak) + .padding(.leading, 53) + .frame(height: 32, alignment: .leading) + } + ForEach(manager.activeSessions) { s in + SidebarSessionRow(session: s, isSelected: router.route.sessionID == s.id) { + manager.focus(s) + router.enter(s.id) + } + } + Button(action: onNewHost) { + Text("连接新主机…") + .font(TX.Font.mono(12)) + .foregroundStyle(p.text4) + .padding(.leading, 53) // 与会话行标题左缘对齐 + .frame(height: 32, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .leading) + } + .buttonStyle(.plain) + } + } + + private var tmuxSection: some View { + VStack(alignment: .leading, spacing: 2) { + TXSectionLabel(text: tmuxHeader) + .padding(.horizontal, TX.Space.m) + .padding(.bottom, 2) + if let tmux = session.tmuxController { + ForEach(tmux.windows) { win in + SidebarWindowRow(window: win, + isSelected: win.id == tmux.activeWindowID) { + tmux.selectWindow(win.id) + } + } + newWindowRow { tmux.newWindow() } + } else if let summary = session.tmuxSummary { + // 已知是 tmux 会话但 controller 还没 attach:按概览占位,避免与分组标题矛盾。 + ForEach(0 ..< max(1, summary.windows), id: \.self) { i in + placeholderWindowRow( + index: i, + title: i == 0 && !summary.currentTitle.isEmpty ? summary.currentTitle : "window \(i)", + detail: i == 0 && summary.panes > 0 + ? "\(summary.panes) pane\(summary.panes == 1 ? "" : "s")" : "", + isSelected: i == 0) + } + newWindowRow {} + } else { + Text("无 tmux · 客户端窗口") + .font(TX.Font.mono(11.5)) + .foregroundStyle(p.textWeak) + .padding(.horizontal, TX.Space.m) + .padding(.vertical, TX.Space.xs) + } + } + } + + /// 「新建 window ⌘T」行(左缘与 window 行标题对齐 x=53)。 + private func newWindowRow(_ action: @escaping () -> Void) -> some View { + Button(action: action) { + HStack { + Text("新建 window") + .font(TX.Font.mono(12)) + .foregroundStyle(p.text4) + Spacer() + Text("⌘T") + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.textFaint) + } + .padding(.leading, 53) + .padding(.trailing, TX.Space.m) + .frame(height: 32) + } + .buttonStyle(.plain) + } + + /// controller 未就绪时的 window 行占位(视觉与 SidebarWindowRow 一致)。 + private func placeholderWindowRow(index: Int, title: String, detail: String, + isSelected: Bool) -> some View { + HStack(spacing: TX.Space.s) { + Text("\(index)") + .font(TX.Font.mono(11, .semibold)) + .foregroundStyle(isSelected ? TXAccent.text : p.text4) + .frame(width: 26, height: 22) + .background(isSelected ? TXAccent.selectedBg : .clear, + in: RoundedRectangle(cornerRadius: 5)) + VStack(alignment: .leading, spacing: 1) { + Text(title) + .font(TX.Font.mono(13, isSelected ? .medium : .regular)) + .foregroundStyle(isSelected ? p.text : p.text2) + .lineLimit(1) + if !detail.isEmpty { + Text(detail) + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.textWeak) + } + } + Spacer(minLength: 0) + if isSelected { TXStatusDot(color: p.online, size: 7) } + } + .padding(.horizontal, TX.Space.s) + .frame(height: 44) + .background(isSelected ? TXAccent.selectedBg.opacity(0.75) : .clear, + in: RoundedRectangle(cornerRadius: TX.Radius.control)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.control) + .strokeBorder(isSelected ? TXAccent.selectedStroke : .clear, lineWidth: 1) + ) + .padding(.horizontal, TX.Space.s) + } + + private var tmuxHeader: String { + let host = session.displayName + if let t = session.tmuxSummary, !t.name.isEmpty { return "TMUX · \(host) · \(t.name)" } + return session.tmuxSummary == nil ? "窗口 · \(host)" : "TMUX · \(host)" + } + + /// 面板脚:tailnet 状态(绿点 + tailnet + 设备 IP)。 + /// + /// **与舞台底部提示条同高**(都是 `TX.Layout.footHeight`),故两侧的分割线落在同一条 y 上; + /// 分割线用 1pt divider(设计稿如此),不加底色块、不加阴影。 + private var tailscaleFoot: some View { + VStack(spacing: 0) { + Rectangle().fill(p.divider).frame(height: 1) + HStack(spacing: TX.Space.s) { + TXStatusDot(color: tailscaleStore.connections.isEmpty ? p.offline : p.online) + VStack(alignment: .leading, spacing: 1) { + Text(tailscaleStore.connections.isEmpty ? "Tailscale 未配置" : "Tailscale 已连接") + .font(TX.Font.mono(11.5, .medium)) + .foregroundStyle(p.text2) + if let c = tailscaleStore.connections.first { + // 设计:tailnet 内主机名 + 本机 tailnet IP(Up 后缓存的 SelfIP)。 + Text([c.hostname, c.lastSelfIP].compactMap { $0 }.joined(separator: " · ")) + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.textWeak) + .lineLimit(1) + } + } + Spacer(minLength: 0) + } + .padding(.horizontal, TX.Space.m) + .frame(height: TX.Layout.footHeight) + } + } +} + +/// 侧边栏会话行:26pt 头像 + 名称·window title + 右侧链路/重连标。 +struct SidebarSessionRow: View { + @ObservedObject var session: TerminalSession + let isSelected: Bool + let onTap: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + Button(action: onTap) { + HStack(spacing: TX.Space.s) { + TXInitialsAvatar(initials: session.initials, size: 26, + isSelected: isSelected, + status: RailSidebarStyle.statusColor(session, p), + punchOut: isSelected ? TXAccent.selectedBg : p.surfaceDark) + VStack(alignment: .leading, spacing: 1) { + Text(title) + .font(TX.Font.mono(13, .medium)) + .foregroundStyle(p.text) + .lineLimit(1) + Text(subtitle) + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.textWeak) + .lineLimit(1) + } + Spacer(minLength: TX.Space.xs) + trailingTag + } + .padding(.horizontal, TX.Space.s) + .frame(height: 48) + .background(isSelected ? TXAccent.selectedBg : .clear, + in: RoundedRectangle(cornerRadius: TX.Radius.control)) + // inset 描边而非 border:选中时内容不位移 1pt。 + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.control) + .strokeBorder(isSelected ? TXAccent.selectedStroke : .clear, lineWidth: 1) + ) + .padding(.horizontal, TX.Space.s) + } + .buttonStyle(.plain) + } + + private var title: String { + if let t = session.tmuxSummary, !t.name.isEmpty { + return "\(session.displayName) · \(t.name)" + } + return session.displayName + } + + private var subtitle: String { + if session.isMinimized { return "最小化中" } + guard let t = session.tmuxSummary else { return "原生窗口" } + let title = t.currentTitle.isEmpty ? session.phaseText : t.currentTitle + return "\(title) · \(t.windows) window\(t.windows == 1 ? "" : "s")" + } + + @ViewBuilder private var trailingTag: some View { + switch session.link { + case .reconnecting: + Text("重连中").font(TX.Font.mono(10.5, .medium)).foregroundStyle(p.reconnecting) + case .offline: + Text("离线").font(TX.Font.mono(10.5)).foregroundStyle(p.offline) + default: + EmptyView() + } + } +} + +/// 侧边栏 window 行:26×22pt 编号徽标 + title + cwd/panes。 +struct SidebarWindowRow: View { + @ObservedObject var window: TmuxWindow + let isSelected: Bool + let onTap: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + private var subtitle: String { + var parts: [String] = [] + if !window.cwd.isEmpty { parts.append(TerminalTitleCapsule.abbreviate(window.cwd)) } + let n = window.panes.count + if n > 1 { parts.append("\(n) panes") } + return parts.isEmpty ? "1 pane" : parts.joined(separator: " · ") + } + + var body: some View { + Button(action: onTap) { + HStack(spacing: TX.Space.s) { + Text("\(window.index)") + .font(TX.Font.mono(11, .semibold)) + .foregroundStyle(isSelected ? TXAccent.text : p.text4) + .frame(width: 26, height: 22) + .background(isSelected ? TXAccent.selectedBg : .clear, + in: RoundedRectangle(cornerRadius: 5)) + VStack(alignment: .leading, spacing: 1) { + Text(window.title) + .font(TX.Font.mono(13, isSelected ? .medium : .regular)) + .foregroundStyle(isSelected ? p.text : p.text2) + .lineLimit(1) + // 设计:cwd · n panes(cwd 来自 #{pane_current_path}) + Text(subtitle) + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.textWeak) + .lineLimit(1) + } + Spacer(minLength: 0) + if isSelected { + TXStatusDot(color: p.online, size: 7) + } + } + .padding(.horizontal, TX.Space.s) + .frame(height: 44) + .background(isSelected ? TXAccent.selectedBg.opacity(0.75) : .clear, + in: RoundedRectangle(cornerRadius: TX.Radius.control)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.control) + .strokeBorder(isSelected ? TXAccent.selectedStroke : .clear, lineWidth: 1) + ) + .padding(.horizontal, TX.Space.s) + } + .buttonStyle(.plain) + } +} diff --git a/apps/TerminalX/iOS/SSHTerminalModel.swift b/apps/TerminalX/iOS/SSHTerminalModel.swift deleted file mode 100644 index 4377b91..0000000 --- a/apps/TerminalX/iOS/SSHTerminalModel.swift +++ /dev/null @@ -1,655 +0,0 @@ -import Foundation -import GhosttyTerminal -import TXCore -import TXTransport -import TsnetBridge -#if canImport(UIKit) -import UIKit -#endif - -enum TsnetError: Error { case newNodeFailed, notUp } - -/// 主 tsnet 节点管理器(线程安全,供 SSHTerminalModel 在后台线程 up/dial)。 -/// up 后节点常驻,重连时复用(只重新 dial fd,不重新加入 tailnet)。 -final class TsnetManager: @unchecked Sendable { - private let lock = NSLock() - private var node: TsnetbridgeNode? - private let stateDir: String - - init() { - let base = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] - stateDir = base + "/tsnet-main" - try? FileManager.default.createDirectory(atPath: stateDir, withIntermediateDirectories: true) - } - - /// 确保节点已 up(幂等)。阻塞,需后台线程调用。 - func ensureUp(authKey: String, timeoutMs: Int) throws { - lock.lock(); let existing = node; lock.unlock() - if existing != nil { return } - guard let n = TsnetbridgeNewNode(stateDir, "terminalx-ipad") else { throw TsnetError.newNodeFailed } - try n.up(withAuthKey: authKey, timeoutMs: timeoutMs) - lock.lock(); node = n; lock.unlock() - } - - /// 经 tsnet dial tailnet 内 host:port,返回已连接 fd。阻塞,需后台线程调用。 - /// 带重试:tsnet 刚 Up 后到 peer 的路径(DERP/直连打洞)预热期间首次 dial 可能 i/o timeout, - /// 短退避重试若干次直到路径就绪。 - func dialFD(host: String, port: Int, timeoutMs: Int) throws -> Int32 { - lock.lock(); let n = node; lock.unlock() - guard let n else { throw TsnetError.notUp } - var lastError: Error? - for attempt in 0 ..< 6 { - do { - var fd: Int64 = -1 - try n.dialTCPFD(host, port: port, timeoutMs: 8000, ret0_: &fd) - return Int32(fd) - } catch { - lastError = error - if attempt < 5 { Thread.sleep(forTimeInterval: 2.0) } // 后台线程,路径预热 - } - } - throw lastError ?? TsnetError.notUp - } - - /// 为 mosh 建立 UDP relay:loopback ↔ tsnet(host:moshPort)。返回句柄(含 localPort)。 - /// 阻塞(tsnet UDP Dial),需后台线程调用。 - func startMoshRelay(host: String, moshPort: Int, timeoutMs: Int) throws -> TsnetbridgeMoshRelay { - lock.lock(); let n = node; lock.unlock() - guard let n else { throw TsnetError.notUp } - return try n.startMoshRelay(host, moshPort: moshPort, timeoutMs: timeoutMs) - } - - /// M3:前台恢复时唤醒 tsnet 网络层(注入链路事件 + Rebind/重新 STUN)。可能阻塞,后台线程调用。 - func wakeUp() { - lock.lock(); let n = node; lock.unlock() - n?.wakeUp() - } -} - -/// 线程安全地持有 transport 引用,供 libghostty 的输入/resize 回调(非主线程)安全取用。 -final class TransportHolder: @unchecked Sendable { - private let lock = NSLock() - private var value: Transport? - var transport: Transport? { - get { lock.lock(); defer { lock.unlock() }; return value } - set { lock.lock(); value = newValue; lock.unlock() } - } -} - -/// 线程安全地持有 mosh 会话引用;非 nil 时终端输入/resize 路由到 mosh 而非 SSH transport。 -final class MoshHolder: @unchecked Sendable { - private let lock = NSLock() - private var value: MoshSession? - var session: MoshSession? { - get { lock.lock(); defer { lock.unlock() }; return value } - set { lock.lock(); value = newValue; lock.unlock() } - } -} - -/// 输出闸门:surface 未 attach 时 receive 会丢弃写入,故未就绪先缓冲、就绪后按序 flush(修白屏)。 -final class OutputGate: @unchecked Sendable { - private let lock = NSLock() - private let session: InMemoryTerminalSession - private var ready = false - private var pending = Data() - - init(session: InMemoryTerminalSession) { self.session = session } - - func deliver(_ data: Data) { - lock.lock() - if ready { lock.unlock(); session.receive(data) } - else { pending.append(data); lock.unlock() } - } - - func markReady() { - lock.lock() - if ready { lock.unlock(); return } - ready = true - let buffered = pending; pending = Data() - lock.unlock() - if !buffered.isEmpty { session.receive(buffered) } - } -} - -/// tmux 网关字节的有序事件(enter 携初始字节 / bytes 后续)。经单一 AsyncStream 传到主线程, -/// 杜绝"每 chunk 起独立 Task"导致的乱序(多 pane 高吞吐会打碎 % 协议解析)。 -enum TmuxByteEvent: Sendable { case enter([UInt8]); case bytes([UInt8]) } - -/// 线程安全承载 tmux 字节通道的 continuation(传输后台线程 yield,主线程单消费者取用)。 -final class TmuxByteChannel: @unchecked Sendable { - let stream: AsyncStream - private let cont: AsyncStream.Continuation - init() { (stream, cont) = AsyncStream.makeStream() } - func enter(_ b: [UInt8]) { cont.yield(.enter(b)) } - func bytes(_ b: [UInt8]) { cont.yield(.bytes(b)) } -} - -/// 线程安全记录全屏终端格子数 + cell 像素尺寸(tmux attach/resize 时据此换算 `refresh-client -C`)。 -final class GridBox: @unchecked Sendable { - private let lock = NSLock() - private var cols = 80, rows = 24, cellW = 0, cellH = 0 - var value: (cols: Int, rows: Int, cellW: Int, cellH: Int) { - lock.lock(); defer { lock.unlock() }; return (cols, rows, cellW, cellH) - } - func set(cols: Int, rows: Int, cellW: Int, cellH: Int) { - lock.lock() - self.cols = max(1, cols); self.rows = max(1, rows) - if cellW > 0 { self.cellW = cellW } // cell 尺寸稳定,保留最后有效值 - if cellH > 0 { self.cellH = cellH } - lock.unlock() - } -} - -/// M4 host key 不符详情(供 SwiftUI alert 并排显示旧/新指纹 + 用户抉择)。 -struct HostKeyMismatchInfo: Identifiable { - let id = UUID() - let egress: String, host: String, port: Int - let keyType: Int32 - let presentedBlob: Data - let presentedFp: String - let storedFp: String -} - -/// M0 SSH 终端会话模型:SSHSession(libssh2) ⇄ GhosttyKit in-memory 终端, -/// 由 TXCore.SessionMachine 编排生命周期(断线指数退避自动重连、后台冻结/前台恢复)。 -@MainActor -final class SSHTerminalModel: ObservableObject { - let state: TerminalViewState - let session: InMemoryTerminalSession - private let holder: TransportHolder - private let gate: OutputGate - - /// 是否展示终端页(连接后保持,重连/后台期间不退回表单)。 - @Published var showsTerminal = false - @Published var banner: String = "" - @Published var phaseText: String = "空闲" - /// 非 nil 时进入 tmux control-mode(原生 tab)。 - @Published var tmuxController: TmuxController? - /// M4:host key 与 pin 不符 → 非 nil 触发 SwiftUI alert(信任新密钥 / 取消)。 - @Published var hostKeyMismatch: HostKeyMismatchInfo? - /// 终端顶部状态行:当前连接标题(主机名/地址)。 - @Published var connectionTitle: String = "" - /// mosh 是否已接管(传输徽章 SSH/mosh)。 - @Published var moshEngaged: Bool = false - - private let knownHosts = KeychainKnownHostsStore() - - private var machine = SessionMachine() - private var config: SSHConfig? - private var reconnectTask: Task? - private var autoCommand: String? - - /// tsnet 模式:非 nil 时经 tsnet fd 桥连接(而非系统直连),重连复用同一节点。 - private var tsnetAuthKey: String? - private let tsnetMgr = TsnetManager() - - /// M2 mosh 模式:SSH 登录后跑 mosh-server,解析 MOSH CONNECT,改由 mosh(UDP over tsnet relay) 承载会话。 - private var moshMode = false - private var moshServerCmd = "mosh-server new -s -c 256 -l LANG=en_US.UTF-8 -l LC_ALL=en_US.UTF-8" - private let moshHolder: MoshHolder - private var moshSession: MoshSession? - private var moshRelay: TsnetbridgeMoshRelay? - private var moshScanner = MoshConnectScanner() - private var moshBootstrapSent = false - // M3 前台恢复:基线时刻 + healthy 轮询 + 恢复看门狗 + 后台任务。 - private var moshResumeBaseMs: UInt64 = 0 - private var moshHealthyPoll: Task? - private var resumeWatchdog: Task? - #if canImport(UIKit) - private var bgTask: UIBackgroundTaskIdentifier = .invalid - #endif - - private let tmuxChannel = TmuxByteChannel() - /// 全屏终端格子数(raw 模式 surface resize 时记录;tmux attach 用于 refresh-client -C)。 - let screenGrid = GridBox() - - init() { - let holder = TransportHolder() - let moshHolder = MoshHolder() - let grid = screenGrid - let session = InMemoryTerminalSession( - // 输入/resize:mosh 激活时路由到 mosh,否则走 SSH transport。 - write: { data in - if let m = moshHolder.session { m.send([UInt8](data)) } - else { holder.transport?.send(data) } - }, - resize: { vp in - // 记录全屏格子 + cell 像素(tmux resize 换算用)。 - grid.set(cols: Int(vp.columns), rows: Int(vp.rows), - cellW: Int(vp.cellWidthPixels), cellH: Int(vp.cellHeightPixels)) - if let m = moshHolder.session { m.resize(cols: Int(vp.columns), rows: Int(vp.rows)) } - else { holder.transport?.resize(cols: vp.columns, rows: vp.rows) } - } - ) - let state = TerminalViewState() - state.configuration = TerminalSurfaceOptions(backend: .inMemory(session)) - _ = state.controller.setTheme(.txDefault) // Catppuccin Mocha 终端配色 - self.holder = holder - self.moshHolder = moshHolder - self.session = session - self.state = state - self.gate = OutputGate(session: session) - - // tmux 网关字节:单消费者按序处理(enter 建 controller+喂初始,bytes 续喂)。 - let ch = tmuxChannel - Task { @MainActor [weak self] in - for await ev in ch.stream { - guard let self else { continue } - switch ev { - case .enter(let initial): self.enterTmux(initialBytes: initial) - case .bytes(let b): self.tmuxController?.feed(Data(b)) - } - } - } - } - - // MARK: - 公开入口 - - /// M4b:非 nil 时用 publickey(SE/软件签名器) 认证而非密码。 - private var pubkeySigner: SigningKeyProvider? - - /// 从主机列表连接(设置 mosh 模式后走标准连接)。 - func connect(to h: SavedHost) { - moshMode = h.useMosh - connect(host: h.host, port: h.port, username: h.username, password: h.password) - connectionTitle = h.name - } - - func connect(host: String, port: Int, username: String, password: String) { - connectionTitle = host - moshEngaged = false - let egress = tsnetAuthKey != nil ? "tsnet" : "direct" - let auth: SSHConfig.Authentication = pubkeySigner.map { .publicKeyCallback($0) } ?? .password(password) - config = SSHConfig(host: host, port: port, username: username, - authentication: auth, - hostKeyVerifier: makeHostKeyVerifier(egress: egress, host: host, port: port)) - run(machine.reduce(.connectRequested)) - } - - /// M4:构造 host key 校验器(TOFU)。在 ssh 串行队列同步调用;firstUse 自动 pin + 横幅, - /// mismatch 冒泡到主线程触发 alert 并返回 false(SSHSession 会断开重试)。 - private func makeHostKeyVerifier(egress: String, host: String, port: Int) -> @Sendable (Data, Int32) -> Bool { - let store = knownHosts - let triple = HostTriple(egress: egress, host: host, port: port) - return { [weak self] blob, keyType in - switch HostKey.evaluate(stored: store.lookup(triple)?.blob, presented: blob) { - case .firstUse: - store.pin(triple, record: HostKeyRecord(blob: blob, keyType: keyType)) - let fp = HostKey.opensshFingerprint(blob) - Task { @MainActor in self?.banner = "已记住 host key \(fp)" } - return true - case .trusted: - return true - case .mismatch(let stored): - let info = HostKeyMismatchInfo( - egress: egress, host: host, port: port, keyType: keyType, - presentedBlob: blob, presentedFp: HostKey.opensshFingerprint(blob), - storedFp: HostKey.opensshFingerprint(stored)) - Task { @MainActor in self?.hostKeyMismatch = info } - return false - } - } - } - - /// 用户确认信任新 host key → 覆盖 pin 并重连。 - func trustNewHostKey() { - guard let m = hostKeyMismatch else { return } - knownHosts.pin(HostTriple(egress: m.egress, host: m.host, port: m.port), - record: HostKeyRecord(blob: m.presentedBlob, keyType: m.keyType)) - hostKeyMismatch = nil - run(machine.reduce(.retryRequested)) // failed → connecting(verifier 此时将判定 trusted) - } - - /// 用户取消 → 保持断开。 - func dismissHostKeyMismatch() { hostKeyMismatch = nil } - - /// 切换主题时推送新终端配色到当前 raw surface 与所有 tmux pane(新建面板另经 TerminalTheme.txDefault 读取)。 - func applyTerminalTheme(_ t: TerminalTheme) { - _ = state.controller.setTheme(t) - tmuxController?.applyTerminalTheme(t) - } - - func close() { run(machine.reduce(.closeRequested)) } - func enterBackground() { - if moshMode { beginBgTask() } // 争取 ~30s 后台执行,mosh 每 3s ack 保活 NAT/WG/DERP - run(machine.reduce(.enteredBackground)) - } - func enterForeground() { - endBgTask() - run(machine.reduce(.enteredForeground)) - } - - /// TerminalScreen 在 surface 首次布局后调用:放行缓冲输出(+ 无头自动命令)。 - func markSurfaceReady() { - gate.markReady() - if let cmd = autoCommand { - autoCommand = nil - let holder = self.holder - Task { - try? await Task.sleep(nanoseconds: 2_500_000_000) - holder.transport?.send(Data("\n".utf8)) - try? await Task.sleep(nanoseconds: 900_000_000) - holder.transport?.send(Data((cmd + "\n").utf8)) - } - } - } - - /// 无头验证:从启动参数自动连接(simctl launch --args -txHost … -txAutoCommand …)。 - func autoConnectIfConfigured() { - let d = UserDefaults.standard - guard let host = d.string(forKey: "txHost"), !host.isEmpty, - let user = d.string(forKey: "txUser") else { return } - let port = d.integer(forKey: "txPort") - autoCommand = d.string(forKey: "txAutoCommand") - if let key = d.string(forKey: "txTsnetKey"), !key.isEmpty { tsnetAuthKey = key } - if d.bool(forKey: "txMosh") { - moshMode = true - if let cmd = d.string(forKey: "txMoshServerCmd"), !cmd.isEmpty { moshServerCmd = cmd } - autoCommand = nil // mosh 模式用自己的引导流程,不用 txAutoCommand - } - let realPort = port == 0 ? 22 : port - // M4b:publickey 认证(SE/软件签名器)。日志打出 authorized_keys 行供加入服务器。 - if d.bool(forKey: "txPubkeyAuth") { - pubkeySigner = SigningKeyProvider.loadOrCreate() - if let s = pubkeySigner { - NSLog("M4DBG authorized_keys hw=\(s.isHardwareBacked): \(s.authorizedKeysLine)") - } - } - // 无头验证:播种一个假 pin → 真实 host key 将 mismatch,触发 alert 路径。 - if let b64 = d.string(forKey: "txHostKeyPinOverrideBase64"), let fake = Data(base64Encoded: b64) { - let egress = tsnetAuthKey != nil ? "tsnet" : "direct" - knownHosts.pin(HostTriple(egress: egress, host: host, port: realPort), - record: HostKeyRecord(blob: fake, keyType: 3)) - } - connect(host: host, port: realPort, - username: user, password: d.string(forKey: "txPass") ?? "") - } - - // MARK: - 状态机副作用执行 - - private func run(_ effects: [SessionMachine.Effect]) { - for effect in effects { - switch effect { - case .startConnect: startSession() - case .scheduleReconnect(_, let delayMS): scheduleReconnect(delayMS) - case .cancelReconnectTimer: reconnectTask?.cancel(); reconnectTask = nil - case .teardownTransport: holder.transport?.stop() - case .notify(let message): banner = message - case .nudgeResume: performWakePulse(); startHealthyPollIfNeeded() - case .scheduleResumeWatchdog(let ms): scheduleResumeWatchdog(ms) - case .cancelResumeWatchdog: cancelResumeWatchdog() - case .teardownMosh: teardownMosh() - } - } - syncUI() - } - - private func startSession() { - guard let config else { return } - if let key = tsnetAuthKey { - // tsnet 模式:后台 up 节点 + dial fd,再用外部 fd 建 SSHSession。 - let mgr = tsnetMgr - Task.detached { - do { - try mgr.ensureUp(authKey: key, timeoutMs: 45000) - let fd = try mgr.dialFD(host: config.host, port: config.port, timeoutMs: 15000) - await MainActor.run { self.wireAndStart(SSHSession(config: config, preconnectedFD: fd)) } - } catch { - await MainActor.run { - self.handleTransportState(.failed("tsnet: \(error.localizedDescription)")) - } - } - } - } else { - wireAndStart(SSHSession(config: config)) - } - } - - private func wireAndStart(_ ssh: SSHSession) { - exitTmux() // 新会话回原始模式(tmux 会话在服务端,重连后需重新 -CC attach) - let router = tmuxRouter - if moshMode { - // mosh 模式:SSH 字节喂给引导流程(既显示进度、又扫描 MOSH CONNECT)。 - ssh.onBytes = { [weak self] data in - Task { @MainActor in self?.moshBootstrapFeed(data) } - } - } else { - ssh.onBytes = { data in router.feed(data) } // 经路由器分流 raw / tmux 网关 - } - ssh.onState = { [weak self] newState in - Task { @MainActor in self?.handleTransportState(newState) } - } - holder.transport = ssh - ssh.start() - } - - private lazy var tmuxRouter = TmuxRouter( - rawGate: gate, - // 经有序通道传主线程(enter 先于后续 bytes,单消费者保序)。 - enterGateway: { [tmuxChannel] after in tmuxChannel.enter(after) }, - gatewayBytes: { [tmuxChannel] bytes in tmuxChannel.bytes(bytes) } - ) - - private func enterTmux(initialBytes: [UInt8]) { - let holder = self.holder - let controller = TmuxController(sendRaw: { data in holder.transport?.send(data) }) - controller.onExit = { [weak self] in Task { @MainActor in self?.exitTmux() } } - // 告知全屏格子数 + cell 像素 → attach 时 refresh-client -C、动态 resize 换算。 - let g = screenGrid.value - controller.setClientSize(cols: g.cols, rows: g.rows) - controller.setCellPixels(w: g.cellW, h: g.cellH) - tmuxController = controller - if !initialBytes.isEmpty { controller.feed(Data(initialBytes)) } - } - - private func exitTmux() { - if tmuxController != nil { tmuxController = nil } - tmuxRouter.reset() - } - - // MARK: - M2 mosh 编排 - - /// SSH 已连接后引导 mosh-server(延迟等 shell 提示符,只发一次)。 - private func scheduleMoshBootstrap() { - guard moshMode, !moshBootstrapSent, moshSession == nil else { return } - moshBootstrapSent = true - let holder = self.holder - let cmd = moshServerCmd - banner = "启动 mosh-server…" - Task { - try? await Task.sleep(nanoseconds: 2_500_000_000) // 等 shell 提示符就绪 - holder.transport?.send(Data("\n".utf8)) - try? await Task.sleep(nanoseconds: 800_000_000) - NSLog("MOSHDBG sent mosh-server cmd: \(cmd)") - holder.transport?.send(Data((cmd + "\n").utf8)) - } - } - - /// mosh 引导期消费 SSH 字节:显示进度 + 扫描 MOSH CONNECT,命中即切换到 mosh。 - /// mosh 接管后忽略 SSH 残余字节,避免污染 mosh 的屏幕状态。 - private func moshBootstrapFeed(_ data: Data) { - guard moshSession == nil else { return } - gate.deliver(data) // 让用户看到 mosh-server 引导输出 - let text = String(decoding: data, as: UTF8.self) - if let info = moshScanner.feed(text) { - NSLog("MOSHDBG parsed MOSH CONNECT port=\(info.port) keyLen=\(info.key.count)") - launchMosh(info) - } - } - - /// 拿到 MOSH CONNECT 后:tsnet 起 UDP relay(loopback:port) → 建 MoshSession → 接管会话。 - private func launchMosh(_ info: MoshConnectInfo) { - guard moshSession == nil, let config else { return } - banner = "mosh 连接中(server port \(info.port))…" - if tsnetAuthKey != nil { - let mgr = tsnetMgr - let host = config.host - Task.detached { - do { - NSLog("MOSHDBG startMoshRelay host=\(host) moshPort=\(info.port)") - let relay = try mgr.startMoshRelay(host: host, moshPort: info.port, timeoutMs: 15000) - let localPort = relay.localPort() - NSLog("MOSHDBG relay up localPort=\(localPort)") - await MainActor.run { self.activateMosh(ip: "127.0.0.1", port: localPort, key: info.key, relay: relay) } - } catch { - NSLog("MOSHDBG relay FAILED: \(error.localizedDescription)") - await MainActor.run { self.banner = "mosh relay 失败:\(error.localizedDescription)" } - } - } - } else { - // 直连模式:iOS 允许出站 UDP,直接连 host:port。 - activateMosh(ip: config.host, port: info.port, key: info.key, relay: nil) - } - } - - private func activateMosh(ip: String, port: Int, key: String, relay: TsnetbridgeMoshRelay?) { - guard moshSession == nil else { return } - let mosh = MoshSession(ip: ip, port: port, key: key, cols: 80, rows: 24) - mosh.onBytes = { [weak self] bytes in - guard let self else { return } - self.gate.deliver(Data(bytes)) - } - mosh.onClosed = { [weak self] rc in - Task { @MainActor in - guard let self else { return } - self.run(self.machine.reduce(.moshExited(rc: Int(rc)))) - } - } - moshRelay = relay - moshSession = mosh - moshHolder.session = mosh // 输入/resize 从此路由到 mosh - mosh.start() - moshEngaged = true - NSLog("MOSHDBG activateMosh ip=\(ip) port=\(port) started") - // mosh 接管成功 → 状态机拆掉底层 idle SSH(moshActive 之后忽略其 transportClosed)。 - run(machine.reduce(.moshEstablished)) - } - - // MARK: - M3 前台快速恢复 - - /// 唤醒脉冲:tsnet WakeUp(后台线程,可能阻塞)+ relay 同端口 Rebind + mosh SIGCONT。 - private func performWakePulse() { - guard let mosh = moshSession else { return } - if moshResumeBaseMs == 0 { moshResumeBaseMs = mosh.lastHeardMs() } - if tsnetAuthKey != nil { - let mgr = tsnetMgr - Task.detached { mgr.wakeUp() } - } - do { try moshRelay?.rebind() } - catch { NSLog("MOSHDBG relay rebind failed: \(error.localizedDescription)") } - mosh.nudge() - NSLog("MOSHDBG wake pulse base=\(moshResumeBaseMs)") - } - - /// 轮询 mosh 探针:收到基线之后的新服务器状态即判定 SSP 已续(healthy)。 - private func startHealthyPollIfNeeded() { - guard moshHealthyPoll == nil, let mosh = moshSession else { return } - let base = moshResumeBaseMs - moshHealthyPoll = Task { [weak self] in - for _ in 0 ..< 100 { // ~10s 安全上限(看门狗更早兜底) - try? await Task.sleep(nanoseconds: 100_000_000) - if Task.isCancelled { return } - if mosh.lastHeardMs() > base { - guard let self else { return } - NSLog("TXM3 recovered base=\(base) now=\(mosh.lastHeardMs())") - self.moshHealthyPoll = nil - self.run(self.machine.reduce(.moshHealthy)) - return - } - } - } - } - - private func scheduleResumeWatchdog(_ ms: Int) { - resumeWatchdog?.cancel() - resumeWatchdog = Task { [weak self] in - try? await Task.sleep(nanoseconds: UInt64(ms) * 1_000_000) - guard !Task.isCancelled, let self else { return } - self.run(self.machine.reduce(.resumeWatchdogFired)) - } - } - - private func cancelResumeWatchdog() { - resumeWatchdog?.cancel(); resumeWatchdog = nil - moshHealthyPoll?.cancel(); moshHealthyPoll = nil - moshResumeBaseMs = 0 - } - - /// 拆除 mosh 会话(兜底全量重建 / 关闭时)。重置引导状态,允许重新引导 mosh-server。 - private func teardownMosh() { - cancelResumeWatchdog() - moshEngaged = false - moshHolder.session = nil - moshSession?.close(); moshSession = nil - try? moshRelay?.close(); moshRelay = nil - moshBootstrapSent = false - moshScanner = MoshConnectScanner() - } - - private func beginBgTask() { - #if canImport(UIKit) - guard bgTask == .invalid else { return } - bgTask = UIApplication.shared.beginBackgroundTask(withName: "mosh-keepalive") { [weak self] in - self?.endBgTask() - } - #endif - } - - private func endBgTask() { - #if canImport(UIKit) - if bgTask != .invalid { UIApplication.shared.endBackgroundTask(bgTask); bgTask = .invalid } - #endif - } - - private func handleTransportState(_ st: TransportState) { - switch st { - case .authenticating: run(machine.reduce(.authenticating)) - case .connected: - run(machine.reduce(.established)) - if moshMode { scheduleMoshBootstrap() } - case .disconnected(let reason): run(machine.reduce(.transportClosed(reason: reason))) - case .failed(let message): run(machine.reduce(.authFailed(reason: message))) - case .idle, .connecting: break - } - } - - private func scheduleReconnect(_ delayMS: Int) { - reconnectTask?.cancel() - reconnectTask = Task { [weak self] in - try? await Task.sleep(nanoseconds: UInt64(delayMS) * 1_000_000) - guard !Task.isCancelled else { return } - await MainActor.run { - guard let self else { return } - self.run(self.machine.reduce(.reconnectTimerFired)) - } - } - } - - private func syncUI() { - switch machine.phase { - case .connected, .reconnecting, .waitingToReconnect, .backgroundParked, - .moshActive, .moshParked, .moshResuming: - showsTerminal = true - case .idle, .connecting, .authenticating, .closed, .failed: - showsTerminal = false - } - phaseText = Self.describe(machine.phase) - } - - private static func describe(_ phase: SessionMachine.Phase) -> String { - switch phase { - case .idle: "空闲" - case .connecting: "连接中…" - case .authenticating: "认证中…" - case .connected: "已连接" - case .backgroundParked: "已挂起(后台)" - case .waitingToReconnect(let n): "等待重连(第 \(n) 次)" - case .reconnecting(let n): "重连中(第 \(n) 次)" - case .failed(let r): "失败:\(r)" - case .closed: "已关闭" - case .moshActive: "mosh 已连接" - case .moshParked: "已挂起(后台)" - case .moshResuming(let n): "mosh 恢复中(第 \(n) 次)" - } - } -} diff --git a/apps/TerminalX/iOS/SessionManager.swift b/apps/TerminalX/iOS/SessionManager.swift new file mode 100644 index 0000000..7555e9a --- /dev/null +++ b/apps/TerminalX/iOS/SessionManager.swift @@ -0,0 +1,193 @@ +import Combine +import Foundation +import GhosttyTerminal + +/// 多会话持有者:一台主机可以有多条会话,会话在后台继续跑(最小化 / 回首页都不断)。 +/// +/// 设计要点: +/// - **会话不因导航而销毁**:只有显式关闭(红点 / ⌘W)才 teardown。 +/// - 会话 surface 由 `SessionCanvas` 常驻挂载,故 `%output` 持续消费、内容不丢。 +/// - 生命周期广播(前后台)由本类扇出给每条会话——`TerminalSession` 自己不监听 scenePhase。 +@MainActor +final class SessionManager: ObservableObject { + @Published private(set) var sessions: [TerminalSession] = [] + /// 当前焦点会话(进终端时显示哪条)。 + @Published var focusedID: UUID? + + /// 「已就绪、可以进终端」的会话(视图层消费后清空)。 + /// + /// 为什么要这个信号:点主机后**先留在首页**探测 tmux、弹会话选择器,选完(或探测到没有 + /// tmux)才进终端页 —— 连接过程属于"准备连接",不该先把用户丢进空终端再弹框。 + @Published var enterRequest: UUID? + + /// 点了主机但还没进终端的会话 id(等探测/选择结果)。 + private var awaitingEnter: Set = [] + /// 转发每条会话的变化(嵌套 ObservableObject 不冒泡,见 CLAUDE.md)。 + private var sessionObservers: [UUID: AnyCancellable] = [:] + + private weak var credentialStore: (any CredentialStore)? + private weak var tailscaleStore: TailscaleStore? + + var focused: TerminalSession? { + guard let id = focusedID else { return nil } + return sessions.first { $0.id == id } + } + + func session(_ id: UUID) -> TerminalSession? { + sessions.first { $0.id == id } + } + + /// 正在建立中(还没进终端)的会话 —— 首页据此显示连接进度。 + var connectingSessions: [TerminalSession] { + sessions.filter { awaitingEnter.contains($0.id) } + } + + /// 等用户选 tmux 会话的那条(选择器数据源)。 + var sessionAwaitingChoice: TerminalSession? { + sessions.first { $0.pendingSessionChoice != nil } + } + + /// 首页「活动会话」区的排序:最小化的置顶(设计要求,否则用户不知道东西去哪了),其余按最近活跃。 + var activeSessions: [TerminalSession] { + sessions.sorted { + if $0.isMinimized != $1.isMinimized { return $0.isMinimized } + return $0.lastActiveAt > $1.lastActiveAt + } + } + + /// ⌘1…9 的会话顺序(与首页卡片顺序一致)。 + func session(atShortcutIndex i: Int) -> TerminalSession? { + let list = activeSessions + return i >= 0 && i < list.count ? list[i] : nil + } + + /// 注入解析凭据/出口所需的 Store(新建会话时转交)。 + func bind(credentials: any CredentialStore, tailscale: TailscaleStore) { + credentialStore = credentials + tailscaleStore = tailscale + for s in sessions { s.bind(credentials: credentials, tailscale: tailscale) } + } + + // MARK: - 开 / 关 + + /// 打开一台主机的会话:**已有该主机的活会话则复用**(对标设计「回到刚才那个会话」),否则新建并连接。 + /// 打开一台主机:**已有该主机的活会话则直接复用并进终端**;否则新建会话开始连接, + /// 但**先不进终端** —— 等 tmux 探测/用户选择完成后由 `enterRequest` 通知视图层。 + @discardableResult + func open(host: SavedHost) -> TerminalSession { + if let existing = sessions.first(where: { $0.host?.id == host.id }) { + focus(existing) + enterRequest = existing.id // 复用的会话没有连接过程,立刻进 + return existing + } + let s = makeSession() + sessions.append(s) + focus(s) + awaitingEnter.insert(s.id) + s.connect(to: host) + return s + } + + /// 会话状态变化后判断是否该进终端了。 + /// + /// 就绪 = 不需要用户选择,且(已建立 / 探测确认无 tmux / 用户选了原生 / 连接失败)。 + /// 失败也进终端:那里有「重试 / 返回首页」,比留在首页只显示一行错误更有出路。 + private func evaluateEnter(_ s: TerminalSession) { + guard awaitingEnter.contains(s.id) else { return } + if s.pendingSessionChoice != nil { return } // 等用户选会话 + if s.isProbingTmux { return } // 还在探测 → 留在首页,别先把人送进终端 + let failed = s.phaseText.hasPrefix("失败") + guard s.isLive || s.tmuxUnavailable || s.tmuxSkipped || failed else { return } + awaitingEnter.remove(s.id) + enterRequest = s.id + } + + /// 焦点切换:记录活跃时刻并解除最小化标记。 + func focus(_ s: TerminalSession) { + focusedID = s.id + s.isMinimized = false + s.lastActiveAt = Date() + } + + /// 最小化:**不确认**、不动传输,只标记 + 记时刻(回首页由调用方改路由)。 + func minimize(_ id: UUID) { + guard let s = session(id) else { return } + s.isMinimized = true + s.lastActiveAt = Date() + } + + /// 关闭并移除会话。tmux 的「仅断开 / 结束会话」分支在 UI-4 的确认对话框里决定, + /// 这里只负责最终 teardown + 摘除(会话对象随之释放,surface 一并回收)。 + func close(_ id: UUID) { + guard let s = session(id) else { return } + s.close() + sessions.removeAll { $0.id == id } + sessionObservers[id] = nil + awaitingEnter.remove(id) + if focusedID == id { focusedID = activeSessions.first?.id } + } + + // MARK: - 广播 + + // 假会话不参与:它没有传输层,广播会经 syncUI 把注入状态覆盖回 idle。 + func enterBackground() { for s in sessions where !s.isSyntheticFixture { s.enterBackground() } } + func enterForeground() { for s in sessions where !s.isSyntheticFixture { s.enterForeground() } } + + /// 主题切换:推给所有会话的 raw surface 与 tmux pane。 + func applyTerminalTheme(_ t: TerminalTheme) { + for s in sessions { s.applyTerminalTheme(t) } + } + + /// 任一会话的 host key 告警(多会话时取第一条,逐条处理)。 + var pendingHostKeyMismatch: TerminalSession? { + sessions.first { $0.hostKeyMismatch != nil } + } + + // MARK: - 无头验证 + + /// 启动参数自动连接(simctl launch --args -txHost …)。返回新建的会话供调用方切路由。 + @discardableResult + func autoConnectIfConfigured() -> TerminalSession? { + let d = UserDefaults.standard + guard let host = d.string(forKey: "txHost"), !host.isEmpty else { return nil } + let s = makeSession() + sessions.append(s) + focus(s) + s.autoConnectIfConfigured() + return s + } + + #if DEBUG + /// 无头 UI 验证:批量造假会话(`-txUIFixture 1`)。返回第一条供调用方进终端。 + @discardableResult + func spawnFixtureSessions() -> TerminalSession? { + for (i, spec) in UIPreviewFixture.specs.enumerated() { + // 复用 UIPreviewFixture.hosts 里的同名主机(同一 id),主机卡才能亮「已有会话」。 + guard let host = UIPreviewFixture.hosts.first(where: { $0.name == spec.name }) else { continue } + let s = makeSession() + s.injectFixture(host: host, lines: spec.lines, link: spec.link, + mosh: host.useMosh, tmux: spec.tmux, + tmuxSessions: spec.tmux == nil ? [] : UIPreviewFixture.tmuxSessions(current: spec.tmux!.name)) + // 活跃时刻错开,让首页卡片排序稳定可对拍。 + s.lastActiveAt = Date(timeIntervalSinceNow: Double(-i) * 60) + sessions.append(s) + } + focusedID = sessions.first?.id + return sessions.first + } + #endif + + private func makeSession() -> TerminalSession { + let s = TerminalSession() + if let c = credentialStore, let t = tailscaleStore { + s.bind(credentials: c, tailscale: t) + } + // 转发会话变化:① 让订阅 manager 的视图(首页/选择器)跟着刷新;② 驱动 enter 判定。 + sessionObservers[s.id] = s.objectWillChange.sink { [weak self, weak s] _ in + guard let self else { return } + self.objectWillChange.send() + if let s { Task { @MainActor in self.evaluateEnter(s) } } + } + return s + } +} diff --git a/apps/TerminalX/iOS/SessionPickerSheet.swift b/apps/TerminalX/iOS/SessionPickerSheet.swift new file mode 100644 index 0000000..d485931 --- /dev/null +++ b/apps/TerminalX/iOS/SessionPickerSheet.swift @@ -0,0 +1,206 @@ +import SwiftUI +import TXCore + +/// 连接前的会话选择器(连接流程的一部分):点主机 → 探测服务器 → 问「接回哪个会话」,选完才进终端。 +/// +/// 三种连接方式:**接回已有 tmux 会话** · **新建 tmux 会话** · **原生终端**(不经 tmux)。 +/// 服务器**没装 tmux 时不显示本 sheet** —— 直接连原生终端,不拿一个只有一个选项的框去打扰用户。 +/// +/// 措辞注意:右上不是「跳过」(那读起来像"跳过某个步骤",含义模糊)——它是一种**并列的连接方式**, +/// 所以直接叫「原生终端」。分段控件里 `Herdr` / `最近` 暂时置灰(后续支持),位置先占住避免重排。 +struct SessionPickerSheet: View { + /// 正在连接的主机名(标题里点明"连到哪台",避免多主机时看不出上下文)。 + let host: String + let choice: TmuxSessionChoice + let onAttach: (String) -> Void + let onCreate: (String) -> Void + let onNative: () -> Void + @EnvironmentObject var theme: TXThemeManager + @State private var newName = "" + @State private var creating = false + @FocusState private var nameFocused: Bool + private var p: TXPalette { theme.palette } + + var body: some View { + VStack(alignment: .leading, spacing: TX.Space.m) { + grabber + title + header + if choice.sessions.isEmpty { + emptyState + } else { + sessionList + } + newSessionRow + Spacer(minLength: 0) + } + .padding(.horizontal, TX.Space.l) + .padding(.bottom, TX.Space.l) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .background(p.bg.ignoresSafeArea()) + } + + private var title: some View { + VStack(alignment: .leading, spacing: 3) { + Text("连接到 \(host)") + .font(TX.Font.mono(17, .medium)) + .foregroundStyle(p.text) + Text("接回一个 tmux 会话,或直接用原生终端") + .font(TX.Font.mono(12)) + .foregroundStyle(p.textWeak) + } + } + + private var grabber: some View { + RoundedRectangle(cornerRadius: 2.5) + .fill(p.borderStrong) + .frame(width: 44, height: 5) + .frame(maxWidth: .infinity) + .padding(.top, TX.Space.s) + } + + // MARK: - 分段控件 + 跳过 + + private var header: some View { + HStack(spacing: TX.Space.s) { + HStack(spacing: 2) { + segment(icon: "chevron.left.forwardslash.chevron.right", title: "Tmux", enabled: true) + segment(icon: "shippingbox", title: "Herdr", enabled: false) + segment(icon: "clock.arrow.circlepath", title: "最近", enabled: false) + } + .padding(4) + .background(p.surface, in: Capsule()) + Spacer(minLength: TX.Space.s) + Button(action: onNative) { + HStack(spacing: TX.Space.xs) { + Image(systemName: "terminal") + .font(.system(size: 13, weight: .medium)) + Text("原生终端") + .font(TX.Font.mono(15, .medium)) + } + .foregroundStyle(p.text) + .padding(.horizontal, TX.Space.m) + .frame(height: 44) + .background(p.surface, in: Capsule()) + .overlay(Capsule().strokeBorder(p.border, lineWidth: 1)) + } + .buttonStyle(.plain) + .accessibilityLabel("直接连接原生终端,不经过 tmux") + } + } + + private func segment(icon: String, title: String, enabled: Bool) -> some View { + HStack(spacing: TX.Space.xs) { + Image(systemName: icon) + .font(.system(size: 13, weight: .medium)) + Text(title) + .font(TX.Font.mono(15, .medium)) + } + .foregroundStyle(enabled ? p.text : p.textWeak.opacity(0.7)) + .padding(.horizontal, TX.Space.m) + .frame(height: 36) + .background(enabled ? p.bgDeep : .clear, in: Capsule()) + .overlay { + if enabled { + Capsule().strokeBorder(p.border, lineWidth: 1) + } + } + } + + // MARK: - 会话列表 + + private var sessionList: some View { + VStack(spacing: 0) { + ForEach(Array(choice.sessions.enumerated()), id: \.element.name) { index, s in + Button { onAttach(s.name) } label: { + sessionRow(s) + } + .buttonStyle(.plain) + if index < choice.sessions.count - 1 { + Rectangle().fill(p.divider).frame(height: 1) + .padding(.horizontal, TX.Space.m) + } + } + } + .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + } + + private func sessionRow(_ s: TmuxSessionProbe.Session) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: TX.Space.s) { + Text(s.name) + .font(TX.Font.mono(22, .medium)) + .foregroundStyle(p.text) + // tmux 会话本身就是活着的服务端进程 → 一律「活跃」;被别的客户端占用时另标。 + TXBadge(text: "活跃", emphasized: true) + if s.isAttached { + TXBadge(text: "已被接入") + } + Spacer(minLength: 0) + } + Text(subtitle(s)) + .font(TX.Font.mono(13)) + .foregroundStyle(p.textWeak) + } + .padding(.horizontal, TX.Space.m) + .padding(.vertical, 18) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + + private func subtitle(_ s: TmuxSessionProbe.Session) -> String { + var parts = ["\(s.windows) 个窗口"] + if let t = s.lastActivity { parts.append(SessionCard.relative(t)) } + return parts.joined(separator: " · ") + } + + private var emptyState: some View { + VStack(alignment: .leading, spacing: 6) { + Text("这台主机上还没有 tmux 会话") + .font(TX.Font.mono(15, .medium)) + .foregroundStyle(p.text) + Text("新建一个就能让会话留在服务端:关掉 App、换网络都能原样接回。也可以直接用原生终端。") + .font(TX.Font.mono(12)) + .foregroundStyle(p.textWeak) + .lineSpacing(3) + } + .padding(TX.Space.m) + .frame(maxWidth: .infinity, alignment: .leading) + .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + } + + // MARK: - 新建 + + @ViewBuilder private var newSessionRow: some View { + if creating { + HStack(spacing: TX.Space.s) { + TXSearchField(text: $newName, placeholder: choice.defaultNewName) + .focused($nameFocused) + TXButton(title: "创建", emphasized: true) { + let name = newName.trimmingCharacters(in: .whitespaces) + onCreate(name.isEmpty ? choice.defaultNewName : name) + } + TXButton(title: "取消") { creating = false } + } + } else { + Button { + creating = true + nameFocused = true + } label: { + HStack(spacing: TX.Space.s) { + Image(systemName: "plus") + .font(.system(size: 13, weight: .medium)) + Text("新建 tmux 会话…") + .font(TX.Font.mono(15, .medium)) + Spacer(minLength: 0) + } + .foregroundStyle(TXAccent.textBright) + .padding(.horizontal, TX.Space.m) + .frame(height: 56) + .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } +} diff --git a/apps/TerminalX/iOS/SettingsView.swift b/apps/TerminalX/iOS/SettingsView.swift new file mode 100644 index 0000000..82a10c5 --- /dev/null +++ b/apps/TerminalX/iOS/SettingsView.swift @@ -0,0 +1,173 @@ +import SwiftUI + +/// 设置容器(首页齿轮 → sheet)。过渡形态:`NavigationStack` + 分类跳转。 +/// UI-5 将按设计稿 `1g 设置-外观` 重写为「左 280pt 类别栏 + 右内容区」。 +struct SettingsHost: View { + @ObservedObject var credStore: FileCredentialStore + @ObservedObject var tailscaleStore: TailscaleStore + @Environment(\.dismiss) private var dismiss + @EnvironmentObject var theme: TXThemeManager + /// known hosts 是无状态的 keychain/文件访问层,容器自持一个即可。 + private let knownHosts = KeychainKnownHostsStore() + private var p: TXPalette { theme.palette } + + var body: some View { + NavigationStack { + ZStack { + p.bg.ignoresSafeArea() + List { + Section("外观") { + NavigationLink { SettingsView() } label: { row("paintbrush", "外观与主题") } + } + .listRowBackground(p.surface) + Section("保险库") { + NavigationLink { KeychainListView(store: credStore) } label: { + row("key.fill", "Keychain 凭据") + } + NavigationLink { KnownHostsView(store: knownHosts) } label: { + row("checkmark.shield", "Known Hosts") + } + } + .listRowBackground(p.surface) + Section("网络") { + NavigationLink { TailscaleListView(store: tailscaleStore) } label: { + row("network", "Tailscale") + } + } + .listRowBackground(p.surface) + } + .scrollContentBackground(.hidden) + .foregroundStyle(p.text) + } + .navigationTitle("设置") + .toolbarBackground(p.bg, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("完成") { dismiss() }.tint(TXAccent.base) + } + } + } + .tint(TXAccent.base) + } + + private func row(_ icon: String, _ title: String) -> some View { + Label(title, systemImage: icon) + .font(TX.Font.mono(13)) + .foregroundStyle(p.text) + } +} + +/// 设置页:主题(app 与终端统一)+ 终端 + 关于。 +struct SettingsView: View { + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + ZStack { + p.bg.ignoresSafeArea() + List { + Section("主题(app 与终端统一)") { + ForEach(TXPalette.all) { pal in + Button { theme.apply(pal) } label: { themeRow(pal) } + .buttonStyle(.plain) + } + } + .listRowBackground(p.surface) + Section("终端") { + row("textformat", "字体与大小", value: "系统等宽") + } + .listRowBackground(p.surface) + Section("关于") { + row("info.circle", "terminalX", value: "0.0.1") + } + .listRowBackground(p.surface) + } + .scrollContentBackground(.hidden) + .foregroundStyle(p.textPrimary) + } + .navigationTitle("设置") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(p.bg, for: .navigationBar) + } + + /// 主题行:色板预览(底/面/强调)+ 名称 + 选中勾。 + private func themeRow(_ pal: TXPalette) -> some View { + HStack(spacing: TX.Space.m) { + ZStack { + RoundedRectangle(cornerRadius: 7).fill(pal.bg) + HStack(spacing: 3) { + Circle().fill(pal.accent).frame(width: 7, height: 7) + Circle().fill(pal.mosh).frame(width: 7, height: 7) + } + } + .frame(width: 40, height: 28) + .overlay(RoundedRectangle(cornerRadius: 7).stroke(p.border, lineWidth: 1)) + + Text(pal.name).foregroundStyle(p.textPrimary) + Spacer() + if pal.id == theme.palette.id { + Image(systemName: "checkmark").foregroundStyle(p.accent).font(.system(size: 14, weight: .semibold)) + } + } + } + + private func row(_ icon: String, _ title: String, value: String) -> some View { + HStack { + Image(systemName: icon).foregroundStyle(p.textSecondary).frame(width: 26) + Text(title).foregroundStyle(p.textPrimary) + Spacer() + Text(value).foregroundStyle(p.textSecondary).font(.system(size: 14)) + } + } +} + +/// 已信任 host key(TOFU pin)查看/删除页。 +struct KnownHostsView: View { + let store: KeychainKnownHostsStore + @EnvironmentObject var theme: TXThemeManager + @State private var entries: [KnownHostEntry] = [] + private var p: TXPalette { theme.palette } + + var body: some View { + ZStack { + p.bg.ignoresSafeArea() + ScrollView { + VStack(alignment: .leading, spacing: TX.Space.s) { + if entries.isEmpty { + TXEmptyState(icon: "checkmark.shield", title: "还没有已信任的主机密钥", + subtitle: "首次连接主机时会自动记住其 host key") + } else { + ForEach(entries) { e in + row(e) + .contextMenu { + Button(role: .destructive) { + store.removeKey(e.id); reload() + } label: { Label("移除信任", systemImage: "trash") } + } + } + } + } + .padding(TX.Space.m) + } + } + .navigationTitle("Known Hosts") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(p.bg, for: .navigationBar) + .onAppear(perform: reload) + } + + private func row(_ e: KnownHostEntry) -> some View { + VStack(alignment: .leading, spacing: 3) { + Text("\(e.host):\(e.port)").font(.system(size: 15, weight: .semibold)).foregroundStyle(p.textPrimary) + Text("出口:\(e.egress)").font(TX.Font.mono(11)).foregroundStyle(p.textSecondary) + Text(e.fingerprint).font(TX.Font.mono(11)).foregroundStyle(p.textTertiary) + .textSelection(.enabled) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(TX.Space.m) + .background(p.surface, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + .overlay(RoundedRectangle(cornerRadius: TX.Radius.card).stroke(p.border, lineWidth: 1)) + } + + private func reload() { entries = store.entries() } +} diff --git a/apps/TerminalX/iOS/SigningKeyProvider.swift b/apps/TerminalX/iOS/SigningKeyProvider.swift index e867885..6c39aac 100644 --- a/apps/TerminalX/iOS/SigningKeyProvider.swift +++ b/apps/TerminalX/iOS/SigningKeyProvider.swift @@ -43,6 +43,23 @@ final class SigningKeyProvider: SSHSigner, @unchecked Sendable { return URL(fileURLWithPath: base).appendingPathComponent("ssh-ecdsa-p256.key") } + /// 从 SecKey 外部表示(x9.63 私钥,生成时存入凭据机密)重建签名器(路径 A)。 + static func from(x963: Data) -> SigningKeyProvider? { + guard let key = SecKeyCreateWithData(x963 as CFData, [ + kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + kSecAttrKeySizeInBits: 256, + ] as CFDictionary, nil) else { return nil } + return SigningKeyProvider(privateKey: key, hardware: false) + } + + /// 生成一把软件 P-256 私钥,返回签名器 + 其外部表示(供凭据机密持久化)。 + static func generateSoftwareP256() -> (provider: SigningKeyProvider, x963: Data)? { + guard let sw = createSoftwareKey(), + let ext = SecKeyCopyExternalRepresentation(sw, nil) as Data? else { return nil } + return (SigningKeyProvider(privateKey: sw, hardware: false), ext) + } + static func loadOrCreate() -> SigningKeyProvider? { // 1) 软件密钥文件(模拟器持久化,跨启动稳定)。 if let data = try? Data(contentsOf: keyFileURL), @@ -65,6 +82,23 @@ final class SigningKeyProvider: SSHSigner, @unchecked Sendable { return SigningKeyProvider(privateKey: sw, hardware: false) } + /// 运行时探测 Secure Enclave 是否可用(建一把临时非持久 SE key)。 + /// 未签名/模拟器返回 false(驱动 UI 灰化 Face ID 选项);签名真机自动点亮,无需改码。 + static func secureEnclaveAvailable() -> Bool { + guard let access = SecAccessControlCreateWithFlags( + nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .privateKeyUsage, nil) else { return false } + let attrs: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecAttrKeySizeInBits as String: 256, + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, + kSecPrivateKeyAttrs as String: [ + kSecAttrIsPermanent as String: false, + kSecAttrAccessControl as String: access, + ], + ] + return SecKeyCreateRandomKey(attrs as CFDictionary, nil) != nil + } + private static func createSecureEnclaveKey() -> SecKey? { guard let access = SecAccessControlCreateWithFlags( nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .privateKeyUsage, nil) else { return nil } diff --git a/apps/TerminalX/iOS/TXParts.swift b/apps/TerminalX/iOS/TXParts.swift new file mode 100644 index 0000000..f769be0 --- /dev/null +++ b/apps/TerminalX/iOS/TXParts.swift @@ -0,0 +1,301 @@ +import SwiftUI + +// 设计稿反复出现的小件(首页 / 轨道 / 侧边栏共用)。数值取自 docs/design/UI-DESIGN-HANDOFF.md。 + +/// 状态点:8pt 实心 + 2pt 挖底描边(`box-shadow: 0 0 0 2px <底色>`,让点浮在头像/卡片边缘上)。 +struct TXStatusDot: View { + let color: Color + var size: CGFloat = 8 + /// 挖底色(点所压的那层的底色);nil = 不挖底。 + var punchOut: Color? + + var body: some View { + Circle() + .fill(color) + .frame(width: size, height: size) + .overlay { + if let punchOut { + Circle().stroke(punchOut, lineWidth: 2) + } + } + } +} + +/// 两字缩写头像(轨道 38pt / 侧边栏 26pt / 首页卡 26pt)。选中态用 inset 描边,避免内容位移 1pt。 +struct TXInitialsAvatar: View { + let initials: String + var size: CGFloat = 26 + var isSelected: Bool = false + /// 右上状态点颜色(nil = 不显示)。 + var status: Color? + /// 状态点的挖底色(所在容器底色)。 + var punchOut: Color = .clear + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + private var radius: CGFloat { size >= 34 ? 11 : TX.Radius.badge } + + var body: some View { + Text(initials) + .font(TX.Font.mono(size >= 34 ? 13 : 11, .semibold)) + .foregroundStyle(isSelected ? TXAccent.text : p.text3) + .frame(width: size, height: size) + .background(isSelected ? TXAccent.selectedBg : p.surface, + in: RoundedRectangle(cornerRadius: radius)) + .overlay( + RoundedRectangle(cornerRadius: radius) + .strokeBorder(isSelected ? TXAccent.dim : p.border, lineWidth: 1) + ) + .overlay(alignment: .topTrailing) { + if let status { + TXStatusDot(color: status, punchOut: punchOut) + .offset(x: 3, y: -3) + } + } + } +} + +/// 分组小标题(600 10px, ls .09em, uppercase)。 +struct TXSectionLabel: View { + let text: String + /// 右侧计数(如「活动会话 3」的 3 单独右对齐)。 + var trailing: String? + var color: Color? + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + HStack { + Text(text.uppercased()) + .font(TX.Font.mono(10, .semibold)) + .tracking(0.9) + .foregroundStyle(color ?? p.textWeak) + if let trailing { + Spacer() + Text(trailing) + .font(TX.Font.mono(10, .semibold)) + .foregroundStyle(color ?? p.textWeak) + } + } + } +} + +/// 徽标胶囊(主机卡的 mosh / tsnet / SSH / SE key)。 +struct TXBadge: View { + let text: String + var emphasized: Bool = false + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + Text(text) + .font(TX.Font.mono(11, .medium)) + .foregroundStyle(emphasized ? TXAccent.text : p.text4) + .padding(.horizontal, 7) + .padding(.vertical, 3) + .background(emphasized ? TXAccent.selectedBg : p.surface, + in: RoundedRectangle(cornerRadius: 5)) + .overlay( + RoundedRectangle(cornerRadius: 5) + .strokeBorder(emphasized ? TXAccent.selectedStroke : .clear, lineWidth: 1) + ) + } +} + +/// 筛选 chip(首页「全部 / 生产 / 家里 / 云」)。 +struct TXChip: View { + let text: String + let isSelected: Bool + let action: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + Button(action: action) { + Text(text) + .font(TX.Font.mono(11.5, .medium)) + .foregroundStyle(isSelected ? TXAccent.textBright : p.text4) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background(isSelected ? TXAccent.selectedBg : p.surface, + in: RoundedRectangle(cornerRadius: TX.Radius.badge)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.badge) + .strokeBorder(isSelected ? TXAccent.selectedStroke : p.divider, lineWidth: 1) + ) + } + .buttonStyle(.plain) + } +} + +/// 搜索框(32pt 高、圆角 8、控件底)。右侧可挂快捷键提示。 +struct TXSearchField: View { + @Binding var text: String + var placeholder: String + var shortcut: String? + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + HStack(spacing: TX.Space.xs) { + Image(systemName: "magnifyingglass") + .font(.system(size: 12)) + .foregroundStyle(p.textWeak) + TextField(placeholder, text: $text) + .font(TX.Font.mono(12)) + .foregroundStyle(p.text) + .textFieldStyle(.plain) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + if let shortcut, text.isEmpty { + Text(shortcut) + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.textFaint) + } + } + .padding(.horizontal, TX.Space.s) + .frame(height: 32) + .background(p.surface, in: RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8).strokeBorder(p.divider, lineWidth: 1) + ) + } +} + +/// 次要按钮(描边款,如「新建连接」「安装并启用 tmux」)。 +struct TXButton: View { + let title: String + var icon: String? + /// 主动作用强调描边 + 强调文字(设计:强调色只做线和文字,不铺底)。 + var emphasized: Bool = false + let action: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + Button(action: action) { + HStack(spacing: TX.Space.xs) { + if let icon { + Image(systemName: icon).font(.system(size: 12, weight: .medium)) + } + Text(title).font(TX.Font.mono(12, .medium)) + } + .foregroundStyle(emphasized ? TXAccent.textBright : p.text2) + .padding(.horizontal, 14) + .frame(height: 32) + .background(p.surface, in: RoundedRectangle(cornerRadius: TX.Radius.control)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.control) + .strokeBorder(emphasized ? TXAccent.border : p.border, lineWidth: 1) + ) + } + .buttonStyle(.plain) + } +} + +/// 图标按钮。两种形态(按设计稿区分,别混): +/// - **有框**(`bare: false`):控件底 + 常规描边,用于展开键、页头齿轮、Pin 等"控件"。 +/// - **裸图标**(`bare: true`):无底无框,用于轨道里的搜索 / 首页 / 设置——设计稿这三个只有图标。 +struct TXIconButton: View { + let icon: String + var size: CGFloat = 34 + var isSelected: Bool = false + var bare: Bool = false + /// 命中区(默认补到 44pt 触控下限;轨道里按设计坐标排布时传入实际间距)。 + var hitSize: CGFloat? + var label: String? + let action: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + Button(action: action) { + Image(systemName: icon) + .font(.system(size: bare ? 17 : 14, weight: .regular)) + .foregroundStyle(isSelected ? TXAccent.text : p.text3) + .frame(width: size, height: size) + .background { + if !bare { + RoundedRectangle(cornerRadius: TX.Radius.control) + .fill(isSelected ? TXAccent.selectedBg : p.surface) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.control) + .strokeBorder(isSelected ? TXAccent.selectedStroke : p.border, + lineWidth: 1) + ) + } + } + .frame(width: hitSize ?? max(size, TX.Layout.minTouch), + height: hitSize ?? max(size, TX.Layout.minTouch)) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel(label ?? icon) + } +} + +/// 终端内容的**文本镜像**(首页会话缩略卡 / 侧边栏预览)。 +/// +/// 为什么不渲染真 surface:一个 ghostty surface 只有一个 IOSurfaceLayer,不能同处两地; +/// 离屏 Metal 渲染是已修过的泄漏雷区。故用 `readViewportText()` 抓文本,按终端底色排版, +/// **内容底对齐**(新输出贴着卡片下缘,和真终端的视觉重心一致)。 +struct TXTerminalMirror: View { + let lines: [String] + var fontSize: CGFloat = 11 + /// 终端底色(Catppuccin base,跟随所选 flavor 的终端配色而非 chrome)。 + var background: Color + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + /// 无内容时的占位文案(真实会话刚建立、surface 还没 attach 或还没有输出)。 + var placeholder: String = "等待输出…" + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Spacer(minLength: 0) + if lines.isEmpty { + Text(placeholder) + .font(TX.Font.mono(fontSize)) + .foregroundStyle(p.text4) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } + ForEach(Array(lines.enumerated()), id: \.offset) { _, line in + Text(line.isEmpty ? " " : line) + .font(TX.Font.mono(fontSize)) + // 设计:终端次要文本 11px / 行高 1.5 → 行距 ≈ 字号 × 0.6。 + .lineSpacing(fontSize * 0.6) + .foregroundStyle(color(for: line)) + .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(.horizontal, 14) + .padding(.vertical, TX.Space.xs) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading) + .background(background) + .clipped() + } + + /// 启发式着色:`readViewportText()` 只给纯文本(ANSI 属性已丢),故按行首符号/常见片段上色, + /// 逼近真实终端的信息密度。不追求语法正确——错判只影响一行的颜色,不影响可读性。 + private func color(for line: String) -> Color { + let t = line.trimmingCharacters(in: .whitespaces) + if t.isEmpty { return p.text4 } + if t.hasPrefix("✓") || t.hasPrefix("✔") || t.hasPrefix("✅") || t.hasPrefix("**") { + return p.online + } + if t.hasPrefix("✗") || t.hasPrefix("❌") { return p.danger } + let lower = t.lowercased() + if lower.hasPrefix("error") || lower.contains(" error:") { return p.danger } + if lower.hasPrefix("warning") || lower.contains(" warning:") { return p.warning } + if t.hasPrefix("◇") || t.hasPrefix("▸") || t.hasPrefix("↳") || t.hasPrefix("*") { + return p.mosh + } + if t.hasPrefix("●") { return p.online } + // 提示符行(→ / ➜ / $ 结尾的命令行)用主文字色,命令输出用次文字色 —— 与真实终端观感一致。 + if t.hasPrefix("→") || t.hasPrefix("➜") || t.contains("$ ") { return p.text } + if t.hasPrefix("[") { return p.offline } + return p.text3 + } +} diff --git a/apps/TerminalX/iOS/TailscaleStore.swift b/apps/TerminalX/iOS/TailscaleStore.swift new file mode 100644 index 0000000..60c3c83 --- /dev/null +++ b/apps/TerminalX/iOS/TailscaleStore.swift @@ -0,0 +1,80 @@ +import Foundation + +/// 一个 Tailscale 连接(= 一个常驻 tsnet 节点)。可添加多个;host 可绑定其一作为出口。 +/// 每连接独立 stateDir(`tsnet-`,创建即固化、终身不变——tsnet 无文件锁,同 Dir 双 Up 会损坏状态)。 +struct TailscaleConnection: Identifiable, Codable, Equatable { + let id: UUID + var name: String + /// tailnet 内展示的主机名。 + var hostname: String + /// tsnet 状态目录名(Application Support 下),创建即固化。 + let stateDirName: String + /// true = 从未注册 或 被 tailnet 清理 → 需重填 authKey 才能 Up。 + var needsAuthKey: Bool + /// 展示用缓存:上次 Up 后的本节点 tailnet IP。 + var lastSelfIP: String? + + init(id: UUID = UUID(), name: String, hostname: String = "terminalx-ipad", + needsAuthKey: Bool = true, lastSelfIP: String? = nil) { + self.id = id + self.name = name + self.hostname = hostname + self.stateDirName = "tsnet-\(id.uuidString)" + self.needsAuthKey = needsAuthKey + self.lastSelfIP = lastSelfIP + } +} + +/// Tailscale 连接列表持久化(tailnets.json)。本体无机密(authKey 默认注册后即丢弃)。 +@MainActor +final class TailscaleStore: ObservableObject { + @Published private(set) var connections: [TailscaleConnection] = [] + + private let url: URL + + init() { + let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + try? FileManager.default.createDirectory(at: base, withIntermediateDirectories: true) + url = base.appendingPathComponent("tailnets.json") + load() + } + + func add(_ conn: TailscaleConnection) { connections.append(conn); save() } + + func update(_ conn: TailscaleConnection) { + guard let i = connections.firstIndex(where: { $0.id == conn.id }) else { return } + connections[i] = conn + save() + } + + func remove(_ conn: TailscaleConnection) { + connections.removeAll { $0.id == conn.id } + save() + // 删连接 → 删其 tsnet 状态目录,避免残留身份。 + let base = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] + try? FileManager.default.removeItem(atPath: base + "/" + conn.stateDirName) + } + + func connection(id: UUID?) -> TailscaleConnection? { + guard let id else { return nil } + return connections.first { $0.id == id } + } + + #if DEBUG + /// 无头 UI 验证:注入示意 tailnet(**不写盘**,不污染真实 tailnets.json)。 + func injectFixtureConnections(_ list: [TailscaleConnection]) { + connections = list + } + #endif + + private func load() { + guard let data = try? Data(contentsOf: url), + let decoded = try? JSONDecoder().decode([TailscaleConnection].self, from: data) else { return } + connections = decoded + } + + private func save() { + guard let data = try? JSONEncoder().encode(connections) else { return } + try? data.write(to: url, options: .atomic) + } +} diff --git a/apps/TerminalX/iOS/TailscaleView.swift b/apps/TerminalX/iOS/TailscaleView.swift new file mode 100644 index 0000000..21485e7 --- /dev/null +++ b/apps/TerminalX/iOS/TailscaleView.swift @@ -0,0 +1,206 @@ +import SwiftUI + +/// Tailscale 连接列表页。可添加多个;每个是一个常驻 tsnet 节点。 +struct TailscaleListView: View { + @ObservedObject var store: TailscaleStore + @EnvironmentObject var theme: TXThemeManager + @State private var editing: TailscaleConnection? + @State private var showEditor = false + private var p: TXPalette { theme.palette } + + var body: some View { + ZStack { + p.bg.ignoresSafeArea() + ScrollView { + VStack(alignment: .leading, spacing: TX.Space.s) { + if store.connections.isEmpty { + TXEmptyState(icon: "network", title: "还没有 Tailscale 连接", + subtitle: "添加一个 tailnet,主机可选择经它连接") + } else { + ForEach(store.connections) { conn in + connRow(conn) + .contextMenu { + Button { editing = conn; showEditor = true } label: { + Label("编辑", systemImage: "pencil") + } + Button(role: .destructive) { store.remove(conn) } label: { + Label("删除", systemImage: "trash") + } + } + } + } + } + .padding(TX.Space.m) + } + } + .navigationTitle("Tailscale") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(p.bg, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { editing = nil; showEditor = true } label: { Image(systemName: "plus") } + } + } + .sheet(isPresented: $showEditor) { + TailscaleEditorView(existing: editing, store: store) + } + } + + private func connRow(_ conn: TailscaleConnection) -> some View { + HStack(spacing: TX.Space.m) { + Image(systemName: "network") + .font(.system(size: 18)) + .foregroundStyle(conn.needsAuthKey ? p.textTertiary : p.mosh) + .frame(width: 40, height: 40) + .background(p.surfaceElevated, in: RoundedRectangle(cornerRadius: TX.Radius.chip)) + VStack(alignment: .leading, spacing: 2) { + Text(conn.name).font(.system(size: 15, weight: .semibold)).foregroundStyle(p.textPrimary) + Text(conn.needsAuthKey ? "未认证" : (conn.lastSelfIP.map { "已注册 · \($0)" } ?? "已注册")) + .font(TX.Font.mono(12)) + .foregroundStyle(conn.needsAuthKey ? p.warning : p.textSecondary) + } + Spacer() + Circle().fill(conn.needsAuthKey ? p.offline : p.mosh).frame(width: 8, height: 8) + } + .padding(TX.Space.m) + .background(p.surface, in: RoundedRectangle(cornerRadius: TX.Radius.card)) + .overlay(RoundedRectangle(cornerRadius: TX.Radius.card).stroke(p.border, lineWidth: 1)) + } +} + +/// Tailscale 连接编辑页:填 name/hostname/authKey → 注册(Up tsnet 节点)→ 显示 SelfIP。 +struct TailscaleEditorView: View { + let existing: TailscaleConnection? + @ObservedObject var store: TailscaleStore + + @EnvironmentObject var theme: TXThemeManager + @Environment(\.dismiss) private var dismiss + + @State private var name = "" + @State private var hostname = "terminalx-ipad" + @State private var authKey = "" + @State private var registering = false + @State private var statusText = "" + @State private var registered = false + @State private var draft: TailscaleConnection? + + private var p: TXPalette { theme.palette } + private var canRegister: Bool { !name.isEmpty && (!authKey.isEmpty || registered) } + + var body: some View { + NavigationStack { + ZStack { + p.bg.ignoresSafeArea() + Form { + Section("连接") { + TXTextField(title: "名称", text: $name, placeholder: "公司 tailnet") + TXTextField(title: "本节点主机名", text: $hostname) + } + Section { + TXTextField(title: "Auth Key(tskey-…)", text: $authKey, secure: true) + Button { + register() + } label: { + HStack { + if registering { ProgressView().controlSize(.small) } + Text(registering ? "注册中…" : "注册 / 连接测试") + } + } + .disabled(!canRegister || registering) + .txRow(p) + } header: { + Text("认证") + } footer: { + Text("Auth key 仅用于首次注册,注册成功后不保存(节点身份持久化在设备本地)。") + } + if !statusText.isEmpty { + Section { + Text(statusText) + .font(.system(size: 13)) + .foregroundStyle(registered ? p.accent : p.warning) + .txRow(p) + } + } + } + .scrollContentBackground(.hidden) + } + .navigationTitle(existing == nil ? "添加 Tailscale" : "编辑 Tailscale") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("取消") { dismiss() }.tint(p.textSecondary) + } + ToolbarItem(placement: .confirmationAction) { + Button("完成") { finish() }.tint(p.accent) + } + } + .toolbarBackground(p.bg, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .onAppear(perform: populate) + } + .tint(p.accent) + } + + private func populate() { + guard let c = existing else { return } + name = c.name; hostname = c.hostname + registered = !c.needsAuthKey + draft = c + if registered { statusText = c.lastSelfIP.map { "已注册 · \($0)" } ?? "已注册" } + } + + /// 注册:经 TsnetRegistry Up 该连接的节点(后台阻塞),成功后 SelfIP 落库、needsAuthKey=false。 + private func register() { + // 确定连接身份(新建则造一个,固化 stateDir)。 + let conn = draft ?? existing ?? { + let c = TailscaleConnection(name: name, hostname: hostname) + return c + }() + draft = conn + registering = true; statusText = "" + let connID = conn.id, dir = conn.stateDirName, host = hostname, key = authKey + Task.detached { + do { + let node = try TsnetRegistry.shared.node( + connID: connID, stateDirName: dir, hostname: host, authKey: key, timeoutMs: 45000) + let ip = node.selfIP() + await MainActor.run { + registering = false; registered = true + statusText = ip.isEmpty ? "已注册(IP 获取中)" : "已注册 · \(ip)" + // 落库(保留同一 id/stateDir)。 + let updated = TailscaleConnection(id: connID, name: name.isEmpty ? "tailnet" : name, + hostname: host, needsAuthKey: false, + lastSelfIP: ip.isEmpty ? nil : ip) + persist(updated) + } + } catch { + await MainActor.run { + registering = false + statusText = "注册失败:\(error.localizedDescription)" + } + } + } + } + + /// 完成:若已注册则确保落库;未注册也保存 meta(needsAuthKey=true,供之后再认证)。 + private func finish() { + let base = draft ?? existing + let conn: TailscaleConnection + if let base { + conn = TailscaleConnection(id: base.id, name: name.isEmpty ? "tailnet" : name, + hostname: hostname, needsAuthKey: !registered, + lastSelfIP: registered ? base.lastSelfIP : nil) + } else { + conn = TailscaleConnection(name: name.isEmpty ? "tailnet" : name, + hostname: hostname, needsAuthKey: !registered) + } + persist(conn) + dismiss() + } + + private func persist(_ conn: TailscaleConnection) { + if store.connections.contains(where: { $0.id == conn.id }) { store.update(conn) } + else { store.add(conn) } + draft = conn + } +} diff --git a/apps/TerminalX/iOS/TerminalChrome.swift b/apps/TerminalX/iOS/TerminalChrome.swift new file mode 100644 index 0000000..fde2c63 --- /dev/null +++ b/apps/TerminalX/iOS/TerminalChrome.swift @@ -0,0 +1,406 @@ +import SwiftUI + +// 设计稿 `1b 终端-沉浸轨道` 的浮起 chrome:左上标题胶囊 + 右上状态胶囊 + 底部提示条。 +// +// 浮起(absolute + blur)而非占一行布局:终端面积最大化是这一屏的唯一目标。 +// 终端内容容器顶部留 86pt 预留带给胶囊,预留带用 padding 实现(容器 clipped 防向上溢出)。 + +/// 浮起胶囊的共用外观:blur 材质 + 半透底 + 常规描边 + 圆角 11。 +private struct FloatingCapsule: View { + @EnvironmentObject var theme: TXThemeManager + @ViewBuilder let content: Content + private var p: TXPalette { theme.palette } + + var body: some View { + content + .padding(.horizontal, TX.Space.s) + .frame(height: 34) + .background { + RoundedRectangle(cornerRadius: TX.Radius.capsule) + .fill(p.surfaceDark.opacity(0.88)) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: TX.Radius.capsule)) + } + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.capsule) + .strokeBorder(p.border, lineWidth: 1) + ) + .shadow(color: .black.opacity(0.55), radius: 15, y: 10) + } +} + +/// 左上标题胶囊:红/黄圆点 → 竖分隔 → 状态点 → 主机名 → 「window title · cwd」。 +struct TerminalTitleCapsule: View { + @ObservedObject var session: TerminalSession + let onClose: () -> Void + let onMinimize: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + FloatingCapsule { + HStack(spacing: TX.Space.xs) { + windowDot(TXWindowDot.close, "关闭会话", action: onClose) + windowDot(TXWindowDot.minimize, "最小化", action: onMinimize) + Rectangle().fill(p.border).frame(width: 1, height: 14) + .padding(.horizontal, 2) + TXStatusDot(color: RailSidebarStyle.statusColor(session, p), size: 6) + Text(session.displayName) + .font(TX.Font.mono(12.5, .medium)) + .foregroundStyle(p.text) + .lineLimit(1) + if !contextLine.isEmpty { + Text(contextLine) + .font(TX.Font.mono(11.5)) + .foregroundStyle(p.textWeak) + .lineLimit(1) + } + // 终端格子数(设计稿 08 的「· 168×44」)。首次布局后稳定,故不必 @Published。 + Text(gridLabel) + .font(TX.Font.mono(11.5)) + .foregroundStyle(p.textFaint) + } + } + } + + /// 「当前 window title · cwd」(设计稿 3a)。无 tmux 时退回连接状态文案。 + private var contextLine: String { + guard let t = session.tmuxSummary else { + return session.isLive ? "" : session.phaseText + } + var parts: [String] = [] + if !t.currentTitle.isEmpty { parts.append(t.currentTitle) } + if !t.cwd.isEmpty { parts.append(Self.abbreviate(t.cwd)) } + return parts.joined(separator: " · ") + } + + /// `/Users/yz/src/x` → `~/src/x`(终端习惯写法,设计稿即此形态)。 + static func abbreviate(_ path: String) -> String { + let home = NSHomeDirectory() + if !home.isEmpty, path.hasPrefix(home) { return "~" + path.dropFirst(home.count) } + // 远端 home 与本机不同,按常见前缀兜底。 + for prefix in ["/home/", "/Users/"] where path.hasPrefix(prefix) { + let rest = path.dropFirst(prefix.count) + if let slash = rest.firstIndex(of: "/") { return "~" + rest[slash...] } + return "~" + } + return path + } + + private var gridLabel: String { + let g = session.screenGrid.value + return "\(g.cols)×\(g.rows)" + } + + private func windowDot(_ color: Color, _ label: String, action: @escaping () -> Void) -> some View { + Button(action: action) { + Circle() + .fill(color) + .frame(width: 13, height: 13) + .overlay(Circle().strokeBorder(Color.black.opacity(0.25), lineWidth: 0.5)) + .frame(width: 22, height: 30) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel(label) + } +} + +/// 右上状态胶囊:三格分层徽标 `tsnet | mosh | tmux`,格间 1×16pt 竖线。 +/// +/// **只染出问题的那层**:网络层不可达 → 第一格转琥珀;重连中 → 第二格转琥珀;tmux 缺失 → 第三格 +/// 写「无 tmux · 客户端窗口」而非报错。首版第二格不显示 RTT 数字(见 HANDOFF §12.4)。 +struct TerminalStatusCapsule: View { + @ObservedObject var session: TerminalSession + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + FloatingCapsule { + HStack(spacing: 0) { + cell(egressText, color: egressColor, dot: true) + separator + cell(transportText, color: transportColor, dot: false) + separator + cell(tmuxText, color: p.text2, dot: false) + } + } + } + + private var separator: some View { + Rectangle().fill(p.border).frame(width: 1, height: 16) + .padding(.horizontal, TX.Space.s) + } + + private func cell(_ text: String, color: Color, dot: Bool) -> some View { + HStack(spacing: 5) { + if dot { TXStatusDot(color: color, size: 6) } + Text(text) + .font(TX.Font.mono(12, .medium)) + .foregroundStyle(color) + .lineLimit(1) + } + } + + // 第一格:网络可达性(tsnet 出口 / 直连) + private var egressText: String { + switch session.link { + case .relay: "tsnet" + case .direct: "direct" + case .connecting: "连接中" + case .reconnecting: "tsnet" + case .offline: "离线" + } + } + + private var egressColor: Color { + switch session.link { + case .direct, .relay: p.online + case .connecting: p.text4 + case .reconnecting: p.reconnecting + case .offline: p.offline + } + } + + // 第二格:传输层(mosh / SSH) + private var transportText: String { + if case .reconnecting(let n) = session.link { + return "\(session.moshEngaged ? "mosh" : "ssh") 重连 \(n)" + } + return session.moshEngaged ? "mosh" : "ssh" + } + + private var transportColor: Color { + if case .reconnecting = session.link { return p.reconnecting } + // 设计:这一格的文字是中性色,只有 RTT 数字才染绿/琥珀(首版无 RTT → 全中性)。 + return p.text2 + } + + // 第三格:会话复用层 + private var tmuxText: String { + if let t = session.tmuxSummary { return t.name.isEmpty ? "tmux" : "tmux \(t.name)" } + // 还在探测 / 等用户选会话时别谎报「无 tmux」——那是探测确定不可用后才成立的结论。 + if session.pendingSessionChoice != nil { return "选择 tmux 会话…" } + if session.tmuxSkipped { return "原生窗口 · 不经 tmux" } + if session.tmuxUnavailable { return "无 tmux · 客户端窗口" } + return session.isLive ? "检测 tmux…" : "—" + } +} + +/// 终端区的**非内容态**覆盖层:连接中 / 失败 / 已关闭。 +/// +/// 真实连接不是瞬间的:解析凭据 → tsnet Up → dial → 认证 → (mosh 引导)→ tmux attach, +/// 期间终端一片空白。这层给出「现在在干什么、失败了能怎么办」,避免用户对着黑屏猜。 +/// 已连接(含重连中,屏幕内容还在)时**不显示**,不遮挡终端。 +struct TerminalStateOverlay: View { + @ObservedObject var session: TerminalSession + let onRetry: () -> Void + let onBack: () -> Void + /// 取消正在进行的连接(用户不想再等)。 + let onCancel: () -> Void + @EnvironmentObject var theme: TXThemeManager + /// 连接已等待秒数 —— 真实连接可能要走 tsnet Up / mosh 引导,等待要可见、可取消。 + @State private var waited = 0 + private var p: TXPalette { theme.palette } + + /// 失败态:phaseText 由 SessionMachine 给出(`失败:…`)。 + private var failure: String? { + session.phaseText.hasPrefix("失败") ? String(session.phaseText.dropFirst(3)) : nil + } + + private var isConnecting: Bool { + !session.isLive && failure == nil && session.phaseText != "已关闭" + } + + var body: some View { + if let failure { + state(icon: "exclamationmark.triangle", tint: p.danger, title: "连接失败", + detail: failure) { + HStack(spacing: TX.Space.s) { + TXButton(title: "返回首页", action: onBack) + TXButton(title: "重试", emphasized: true, action: onRetry) + } + } + } else if session.phaseText == "已关闭" { + state(icon: "power", tint: p.textWeak, title: "会话已关闭", + detail: "服务端连接已断开。") { + HStack(spacing: TX.Space.s) { + TXButton(title: "返回首页", action: onBack) + TXButton(title: "重新连接", emphasized: true, action: onRetry) + } + } + } else if isConnecting { + state(icon: nil, tint: p.text4, title: session.phaseText, + detail: session.banner.isEmpty ? connectHint : session.banner) { + VStack(spacing: TX.Space.xs) { + if waited >= 3 { + Text("已等待 \(waited)s") + .font(TX.Font.mono(11)) + .foregroundStyle(p.textFaint) + } + if waited >= 5 { + // 等久了才给取消:避免正常的 1–2s 连接闪现一个按钮。 + TXButton(title: "取消连接", action: onCancel) + } + } + } + .task(id: session.phaseText) { + waited = 0 + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 1_000_000_000) + waited += 1 + } + } + } + } + + /// 分阶段提示:让等待有解释(尤其 tsnet 首次 Up 与 mosh 引导都要数秒)。 + private var connectHint: String { + switch session.link { + case .relay: "正在经 Tailscale 出口建立连接…" + case .connecting, .direct: session.moshEngaged ? "正在引导 mosh…" : "正在建立 SSH 连接…" + case .reconnecting(let n): "第 \(n) 次重连中…" + case .offline: "等待连接…" + } + } + + private func state(icon: String?, tint: Color, title: String, detail: String, + @ViewBuilder actions: () -> Actions) -> some View { + VStack(spacing: TX.Space.s) { + if let icon { + Image(systemName: icon) + .font(.system(size: 26, weight: .light)) + .foregroundStyle(tint) + } else { + ProgressView().controlSize(.regular).tint(TXAccent.base) + } + Text(title) + .font(TX.Font.mono(13, .medium)) + .foregroundStyle(p.text) + Text(detail) + .font(TX.Font.mono(11.5)) + .foregroundStyle(p.textWeak) + .multilineTextAlignment(.center) + .lineLimit(3) + .frame(maxWidth: 420) + actions() + .padding(.top, TX.Space.xs) + } + .padding(TX.Space.l) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +/// 无 tmux 主机的行内横幅(设计稿 `2c`):说明窗口由 App 维持、mosh 兜底,但**关掉 App 不会在 +/// 服务端继续跑**;右侧给「安装并启用 tmux」次要按钮。**不弹窗、不强推**,可关闭。 +struct NoTmuxBanner: View { + let onInstall: () -> Void + let onDismiss: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + HStack(spacing: TX.Space.s) { + Image(systemName: "info.circle") + .font(.system(size: 12)) + .foregroundStyle(p.warning) + Text("未检测到 tmux · 当前是客户端窗口:mosh 能兜断线,但**关掉 App 后服务端不会继续跑**") + .font(TX.Font.mono(11.5)) + .foregroundStyle(p.text3) + .lineLimit(1) + Spacer(minLength: TX.Space.s) + TXButton(title: "安装并启用 tmux", action: onInstall) + Button(action: onDismiss) { + Image(systemName: "xmark") + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(p.textWeak) + .frame(width: 28, height: 28) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + .padding(.horizontal, TX.Space.s) + .frame(height: 40) + .background(p.surfaceDark.opacity(0.92)) + .overlay(alignment: .bottom) { Rectangle().fill(p.divider).frame(height: 1) } + } +} + +/// 「安装并启用 tmux」的引导(点横幅按钮后):给出各发行版命令,用户可一键塞进终端执行。 +struct TmuxInstallSheet: View { + let onRun: (String) -> Void + let onCancel: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + /// 常见包管理器。跨发行版探测一条命令搞定:有哪个用哪个。 + private let combined = "command -v apt-get >/dev/null && sudo apt-get install -y tmux || " + + "command -v dnf >/dev/null && sudo dnf install -y tmux || " + + "command -v pacman >/dev/null && sudo pacman -S --noconfirm tmux || " + + "command -v apk >/dev/null && sudo apk add tmux || " + + "command -v brew >/dev/null && brew install tmux" + + var body: some View { + TXModalScrim(onDismiss: onCancel) { + VStack(alignment: .leading, spacing: TX.Space.s) { + Text("安装 tmux") + .font(TX.Font.mono(15, .medium)) + .foregroundStyle(p.text) + Text("tmux 让会话留在服务端:关掉 App、换网络、睡一觉回来都能原样接回。\n下面这条会按服务器的包管理器自动选择(需要 sudo 权限)。") + .font(TX.Font.mono(12)) + .foregroundStyle(p.text3) + .lineSpacing(4) + Text(combined) + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.text4) + .padding(TX.Space.s) + .frame(maxWidth: .infinity, alignment: .leading) + .background(p.bgDeep, in: RoundedRectangle(cornerRadius: 8)) + HStack(spacing: TX.Space.s) { + Spacer() + TXButton(title: "取消", action: onCancel) + TXButton(title: "在终端执行", emphasized: true) { onRun(combined) } + } + .padding(.top, TX.Space.xs) + } + .padding(TX.Space.l) + .frame(width: 520) + .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.dialog)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.dialog) + .strokeBorder(p.borderStrong, lineWidth: 1) + ) + .shadow(color: .black.opacity(0.7), radius: 40, y: 30) + } + } +} + +/// 底部提示条(34pt):接硬件键盘时只留这条快捷键清单;触控时由键盘附加行替换(UI-5)。 +struct TerminalHintBar: View { + /// 左侧上下文说明(如「Pin 态 · 终端变窄,仅在钉住/解除时 resize 一次」)。 + let context: String + let hints: [(key: String, label: String)] + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + HStack(spacing: TX.Space.s) { + Text(context) + .font(TX.Font.mono(11.5)) + .foregroundStyle(p.textWeak) + .lineLimit(1) + Spacer(minLength: TX.Space.m) + ForEach(hints, id: \.key) { hint in + HStack(spacing: 5) { + Text(hint.key) + .font(TX.Font.mono(11.5, .medium)) + .foregroundStyle(p.text4) + Text(hint.label) + .font(TX.Font.mono(11.5)) + .foregroundStyle(p.textWeak) + } + } + } + .padding(.horizontal, TX.Space.m) + .frame(height: TX.Layout.footHeight) + } +} diff --git a/apps/TerminalX/iOS/TerminalDialogs.swift b/apps/TerminalX/iOS/TerminalDialogs.swift new file mode 100644 index 0000000..b948a81 --- /dev/null +++ b/apps/TerminalX/iOS/TerminalDialogs.swift @@ -0,0 +1,279 @@ +import SwiftUI + +// 设计稿 `3c 关闭确认` 与 `3b 新建分屏菜单`。 +// +// 语义分工(务必别混): +// - **黄点 / ⌘M / 下滑 = 最小化**:不确认,直接回首页,会话继续跑。 +// - **红点 / ⌘W = 关闭**:弹一次确认,按主机能力两套文案;`esc` 取消、`⏎` 选推荐项。 + +/// 浮层容器:遮罩 + 居中卡片(对话框 / 菜单共用)。 +struct TXModalScrim: View { + let onDismiss: () -> Void + @ViewBuilder let content: Content + + var body: some View { + ZStack { + Color(hex: 0x0A0B12).opacity(0.42) + .ignoresSafeArea() + .contentShape(Rectangle()) + .onTapGesture(perform: onDismiss) + content + } + .transition(.opacity) + } +} + +// MARK: - 关闭确认(3c) + +/// 关闭确认。两套文案由会话能力决定: +/// - **tmux 会话**:推荐「仅断开」(`detach-client`,服务端继续跑);左侧次要文字按钮「结束会话」 +/// (`kill-session`,**需再确认一次**)。 +/// - **原生窗口**:琥珀警告「没有 tmux 兜底,前台进程会随之结束」,推荐「取消」,危险按钮「关闭窗口」。 +struct CloseConfirmDialog: View { + @ObservedObject var session: TerminalSession + /// 仅断开(tmux)/ 直接关闭(原生窗口) + let onDetach: () -> Void + /// 结束 tmux 会话(kill-session) + let onKill: () -> Void + let onCancel: () -> Void + @EnvironmentObject var theme: TXThemeManager + @State private var confirmingKill = false + private var p: TXPalette { theme.palette } + + private var isTmux: Bool { session.tmuxSummary != nil } + + var body: some View { + TXModalScrim(onDismiss: onCancel) { + VStack(alignment: .leading, spacing: 0) { + Text(title) + .font(TX.Font.mono(15, .medium)) + .foregroundStyle(p.text) + .padding(.bottom, 8) + message + .padding(.bottom, TX.Space.l) + buttons + } + .padding(TX.Space.l) + .frame(width: isTmux ? 436 : 404) + .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.dialog)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.dialog) + .strokeBorder(p.borderStrong, lineWidth: 1) + ) + .shadow(color: .black.opacity(0.7), radius: 40, y: 30) + } + } + + private var title: String { + if let t = session.tmuxSummary, !t.name.isEmpty { + return "关闭 \(session.displayName) · \(t.name)?" + } + // 原生窗口带窗口编号(首版每主机单窗口,恒为 1;多窗口是二期能力)。 + return "关闭 \(session.displayName) · 窗口 1?" + } + + @ViewBuilder private var message: some View { + if let t = session.tmuxSummary { + Text("tmux 会话 (\(t.windows) windows / \(t.panes) panes)。仅断开后服务端继续跑,下次原样接回。") + .font(TX.Font.mono(12)) + .foregroundStyle(p.text3) + .lineSpacing(4) + } else { + Text("没有 tmux 兜底,") + .font(TX.Font.mono(12)) + .foregroundStyle(p.text3) + + Text("前台进程会随之结束") + .font(TX.Font.mono(12, .medium)) + .foregroundStyle(p.warning) + + Text("。想留着就点黄点最小化。") + .font(TX.Font.mono(12)) + .foregroundStyle(p.text3) + } + } + + @ViewBuilder private var buttons: some View { + HStack(spacing: TX.Space.s) { + if isTmux { + // 次要文字按钮(危险):kill-session 需再点一次确认。 + Button { + if confirmingKill { onKill() } else { confirmingKill = true } + } label: { + Text(confirmingKill ? "确认结束会话?" : "结束会话") + .font(TX.Font.mono(12, confirmingKill ? .semibold : .regular)) + .foregroundStyle(p.danger) + } + .buttonStyle(.plain) + } + Spacer() + TXButton(title: "取消", action: onCancel) + if isTmux { + TXButton(title: "仅断开 ⏎", emphasized: true, action: onDetach) + } else { + dangerButton + } + } + } + + /// 危险按钮(设计给的精确色:描边 #8d5560 / 底 rgba(92,58,63,.35) / 字 #f5a0ac)。 + private var dangerButton: some View { + Button(action: onDetach) { + Text("关闭窗口") + .font(TX.Font.mono(12, .medium)) + .foregroundStyle(Color(hex: 0xF5A0AC)) + .padding(.horizontal, 14) + .frame(height: 32) + .background(Color(hex: 0x5C3A3F).opacity(0.35), + in: RoundedRectangle(cornerRadius: TX.Radius.control)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.control) + .strokeBorder(Color(hex: 0x8D5560), lineWidth: 1) + ) + } + .buttonStyle(.plain) + } +} + +// MARK: - 新建分屏菜单(3b) + +/// 分屏菜单。**不问方向**(竖分 ⌘D / 横分 ⌘⇧D 本就是两个入口),只问「分到哪里」: +/// 当前会话 → 直接 `split-window`;其余 detached 会话 → `join-pane -s`;也可新建一个 tmux 会话。 +/// +/// 模型约束:分屏只发生在**同一台主机内**(跨主机分屏设计已取消)。 +struct SplitMenuView: View { + @ObservedObject var session: TerminalSession + /// true = 竖分(左右并排,⌘D);false = 横分(上下堆叠,⌘⇧D) + let vertical: Bool + let onPick: (SplitTarget) -> Void + let onCancel: () -> Void + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + enum SplitTarget { + case currentSession + case otherSession(String) + case newSession + /// 原生窗口(不经 tmux)。首版不可用:需要一台主机多条 SSH channel(见 HANDOFF §12.4)。 + case nativeWindow + } + + /// 其余会话 = 除了本客户端所在的那个。(不能按 `isAttached` 过滤——服务器上别的客户端 + /// attach 着其它会话时会把它们错误地排除掉。) + private var detached: [TmuxSessionInfo] { + let current = session.tmuxSummary?.name ?? "" + return session.tmuxSessionList.filter { $0.name != current } + } + + var body: some View { + TXModalScrim(onDismiss: onCancel) { + VStack(alignment: .leading, spacing: 0) { + header + Rectangle().fill(p.divider).frame(height: 1) + if session.tmuxSummary != nil { + TXSectionLabel(text: "tmux 会话") + .padding(.horizontal, TX.Space.m) + .padding(.top, TX.Space.s) + .padding(.bottom, 2) + row(title: currentName, detail: currentDetail, dot: p.online, + tag: "当前", selected: true) { onPick(.currentSession) } + ForEach(detached) { s in + // detached 会话的进程仍在服务端跑 → 状态点同样是绿的(设计稿如此)。 + row(title: s.name, + detail: "\(s.windows) window\(s.windows == 1 ? "" : "s") · detached", + dot: p.online, + tag: s.lastActivity.map { SessionCard.relative($0) }) { + onPick(.otherSession(s.name)) + } + } + row(title: "新建 tmux 会话…", detail: nil, dot: nil, tag: nil) { + onPick(.newSession) + } + Rectangle().fill(p.divider).frame(height: 1) + .padding(.vertical, TX.Space.xs) + } + // 原生窗口:设计里是并列选项;首版能力未就绪 → 灰显并注明,不装作能用。 + row(title: "原生窗口", detail: "不经 tmux · 关 App 即结束(二期)", + dot: nil, tag: nil, disabled: true) {} + } + .padding(.bottom, TX.Space.xs) + .frame(width: 360) + .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.dialog - 1)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.dialog - 1) + .strokeBorder(p.borderStrong, lineWidth: 1) + ) + .shadow(color: .black.opacity(0.7), radius: 40, y: 30) + } + } + + private var header: some View { + HStack { + Text(vertical ? "竖分屏到…" : "横分屏到…") + .font(TX.Font.mono(13, .medium)) + .foregroundStyle(p.text) + Spacer() + Text(session.displayName) + .font(TX.Font.mono(11)) + .foregroundStyle(TXAccent.mid) + } + .padding(.horizontal, TX.Space.m) + .frame(height: 44) + } + + private var currentName: String { + guard let t = session.tmuxSummary else { return session.displayName } + return t.name.isEmpty ? "当前会话" : t.name + } + + private var currentDetail: String { + guard let t = session.tmuxSummary else { return "" } + var parts: [String] = [] + if !t.currentTitle.isEmpty { parts.append(t.currentTitle) } + parts.append("\(t.windows) window\(t.windows == 1 ? "" : "s")") + if t.panes > 0 { parts.append("\(t.panes) panes") } + return parts.joined(separator: " · ") + } + + /// 菜单行:**前导列统一 20pt**(状态点也包在 20pt 居中列里),保证所有行文字左缘一致。 + private func row(title: String, detail: String?, dot: Color?, tag: String?, + selected: Bool = false, disabled: Bool = false, + action: @escaping () -> Void) -> some View { + Button(action: action) { + HStack(spacing: TX.Space.s) { + ZStack { + if let dot { TXStatusDot(color: dot, size: 7) } + } + .frame(width: 20) + VStack(alignment: .leading, spacing: 1) { + Text(title) + .font(TX.Font.mono(13, .medium)) + .foregroundStyle(disabled ? p.textWeak : p.text) + if let detail, !detail.isEmpty { + Text(detail) + .font(TX.Font.mono(10.5)) + .foregroundStyle(p.textWeak) + .lineLimit(1) + } + } + Spacer(minLength: TX.Space.xs) + if let tag { + Text(tag) + .font(TX.Font.mono(10.5, .medium)) + .foregroundStyle(selected ? TXAccent.text : TXAccent.mid) + } + } + .padding(.horizontal, TX.Space.s) + .frame(height: 40) + .frame(maxWidth: .infinity, alignment: .leading) + .background(selected ? TXAccent.selectedBg : .clear, + in: RoundedRectangle(cornerRadius: TX.Radius.control)) + .overlay( + RoundedRectangle(cornerRadius: TX.Radius.control) + .strokeBorder(selected ? TXAccent.selectedStroke : .clear, lineWidth: 1) + ) + .padding(.horizontal, TX.Space.xs) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .disabled(disabled) + } +} diff --git a/apps/TerminalX/iOS/TerminalSession.swift b/apps/TerminalX/iOS/TerminalSession.swift new file mode 100644 index 0000000..4018d99 --- /dev/null +++ b/apps/TerminalX/iOS/TerminalSession.swift @@ -0,0 +1,1097 @@ +import Combine +import Foundation +import GhosttyTerminal +import TXCore +import TXTransport +import TsnetBridge +#if canImport(UIKit) +import UIKit +#endif + +/// 出口计划(connect 时按 host 解析,贯穿 startSession/launchMosh/唤醒脉冲)。 +enum EgressPlan { + case direct + /// 经指定 Tailscale 连接的 tsnet 节点。authKey 仅首次注册需要(已存 state 用 nil)。 + case tsnet(connID: UUID, stateDirName: String, hostname: String, authKey: String?) + + /// host key pin 的命名空间(按出口隔离,架构决策 §2.6)。 + var egressKey: String { + switch self { + case .direct: "direct" + case .tsnet(let id, _, _, _): "tsnet-\(id.uuidString)" + } + } +} + +/// 经 tsnet 节点 dial,带退避重试(tsnet 刚 Up 后到 peer 的路径预热期间首次 dial 可能超时)。 +private func dialFDWithRetry(_ node: TsnetbridgeNode, host: String, port: Int) throws -> Int32 { + var lastError: Error? + for attempt in 0 ..< 6 { + do { + var fd: Int64 = -1 + try node.dialTCPFD(host, port: port, timeoutMs: 8000, ret0_: &fd) + return Int32(fd) + } catch { + lastError = error + if attempt < 5 { Thread.sleep(forTimeInterval: 2.0) } + } + } + throw lastError ?? TsnetError.notUp +} + +/// 线程安全地持有 transport 引用,供 libghostty 的输入/resize 回调(非主线程)安全取用。 +final class TransportHolder: @unchecked Sendable { + private let lock = NSLock() + private var value: Transport? + var transport: Transport? { + get { lock.lock(); defer { lock.unlock() }; return value } + set { lock.lock(); value = newValue; lock.unlock() } + } +} + +/// 线程安全地持有 mosh 会话引用;非 nil 时终端输入/resize 路由到 mosh 而非 SSH transport。 +final class MoshHolder: @unchecked Sendable { + private let lock = NSLock() + private var value: MoshSession? + var session: MoshSession? { + get { lock.lock(); defer { lock.unlock() }; return value } + set { lock.lock(); value = newValue; lock.unlock() } + } +} + +/// raw 输出的接收端(`OutputGate` 或它的旁路 `OutputTap`)。 +/// 抽这个协议只为让 `TmuxRouter` 能同时接受两者,别在这上面加职责。 +protocol RawOutputSink: Sendable { + func deliver(_ data: Data) +} + +/// 输出闸门:surface 未 attach 时 receive 会丢弃写入,故未就绪先缓冲、就绪后按序 flush(修白屏)。 +final class OutputGate: RawOutputSink, @unchecked Sendable { + private let lock = NSLock() + private let session: InMemoryTerminalSession + private var ready = false + private var pending = Data() + + init(session: InMemoryTerminalSession) { self.session = session } + + func deliver(_ data: Data) { + lock.lock() + if ready { lock.unlock(); session.receive(data) } + else { pending.append(data); lock.unlock() } + } + + func markReady() { + lock.lock() + if ready { lock.unlock(); return } + ready = true + let buffered = pending; pending = Data() + lock.unlock() + if !buffered.isEmpty { session.receive(buffered) } + } +} + +/// 输出旁路:把 raw 字节**同时**送给下游(终端渲染)和一个观察闭包(会话探测扫描)。 +/// +/// 为什么需要:连接后的 tmux 会话探测要读 shell 的输出,而那条路原本直通 `OutputGate`。 +/// 旁路而非改 gate:gate 的缓冲/flush 语义(修白屏的关键)不能动。 +final class OutputTap: RawOutputSink, @unchecked Sendable { + private let downstream: OutputGate + private let observe: @Sendable (Data) -> Void + + init(downstream: OutputGate, observe: @escaping @Sendable (Data) -> Void) { + self.downstream = downstream + self.observe = observe + } + + func deliver(_ data: Data) { + downstream.deliver(data) + observe(data) + } + + func markReady() { downstream.markReady() } +} + +/// tmux 网关字节的有序事件(enter 携初始字节 / bytes 后续)。经单一 AsyncStream 传到主线程, +/// 杜绝"每 chunk 起独立 Task"导致的乱序(多 pane 高吞吐会打碎 % 协议解析)。 +enum TmuxByteEvent: Sendable { case enter([UInt8]); case bytes([UInt8]) } + +/// 线程安全承载 tmux 字节通道的 continuation(传输后台线程 yield,主线程单消费者取用)。 +final class TmuxByteChannel: @unchecked Sendable { + let stream: AsyncStream + private let cont: AsyncStream.Continuation + init() { (stream, cont) = AsyncStream.makeStream() } + func enter(_ b: [UInt8]) { cont.yield(.enter(b)) } + func bytes(_ b: [UInt8]) { cont.yield(.bytes(b)) } +} + +/// 线程安全记录全屏终端格子数 + cell 像素尺寸(tmux attach/resize 时据此换算 `refresh-client -C`)。 +final class GridBox: @unchecked Sendable { + private let lock = NSLock() + private var cols = 80, rows = 24, cellW = 0, cellH = 0 + var value: (cols: Int, rows: Int, cellW: Int, cellH: Int) { + lock.lock(); defer { lock.unlock() }; return (cols, rows, cellW, cellH) + } + func set(cols: Int, rows: Int, cellW: Int, cellH: Int) { + lock.lock() + self.cols = max(1, cols); self.rows = max(1, rows) + if cellW > 0 { self.cellW = cellW } // cell 尺寸稳定,保留最后有效值 + if cellH > 0 { self.cellH = cellH } + lock.unlock() + } +} + +/// M4 host key 不符详情(供 SwiftUI alert 并排显示旧/新指纹 + 用户抉择)。 +struct HostKeyMismatchInfo: Identifiable { + let id = UUID() + let egress: String, host: String, port: Int + let keyType: Int32 + let presentedBlob: Data + let presentedFp: String + let storedFp: String +} + +/// 链路状态(设计稿右上状态胶囊「只染出问题的那层」用)。 +/// +/// 首版**不含 RTT 数字**:mosh 侧只有 `lastHeardMs()`(时刻,非 SRTT),要真实 RTT 需改 +/// vendor/mosh 的 iosclient 导出 SRTT 再桥到 Swift —— 属传输层改动,二期做。 +enum LinkState: Equatable { + case connecting + /// 直连(无 tsnet 出口) + case direct + /// 经 tsnet(首版不细分 DERP relay / 直连 WG) + case relay + case reconnecting(attempt: Int) + case offline +} + +/// 连接后待用户选择的 tmux 会话(选择器 sheet 的数据源)。 +struct TmuxSessionChoice: Equatable { + let version: String + let sessions: [TmuxSessionProbe.Session] + /// 「新建会话」的默认名(取主机配置里的 tmux 会话名,默认 main)。 + let defaultNewName: String +} + +/// tmux 概览(侧边栏分组标题、会话卡副行)。真实会话从 `TmuxController` 派生,假会话直接注入。 +struct TmuxSummary: Equatable { + /// tmux 会话名。首版拿不到(需 `list-sessions`,二期做)→ 空串时 UI 只显示「tmux」。 + var name: String = "" + var windows: Int = 0 + var panes: Int = 0 + /// 当前 window title。 + var currentTitle: String = "" + /// 当前 window 的工作目录(`#{pane_current_path}`)。标题胶囊显示「title · cwd」。 + var cwd: String = "" +} + +/// 一条会话 = 一个 TerminalSession 实例:SSHSession(libssh2) ⇄ GhosttyKit in-memory 终端, +/// 由 TXCore.SessionMachine 编排生命周期(断线指数退避自动重连、后台冻结/前台恢复)。 +/// +/// **多会话并存**由 `SessionManager` 持有多个实例实现;本类内无全局单例依赖 +/// (tsnet 经 `TsnetRegistry.shared` 按 connID 操作,天然多节点)。 +@MainActor +final class TerminalSession: ObservableObject, Identifiable { + /// 会话身份(路由 `.terminal(id)`、⌘1…9 索引、首页卡与轨道头像都用它)。 + let id = UUID() + + let state: TerminalViewState + let session: InMemoryTerminalSession + private let holder: TransportHolder + private let gate: OutputGate + + /// 归属主机(从主机列表连接时非 nil;无头直连为 nil,标题回退 `connectionTitle`)。 + @Published var host: SavedHost? + /// 会话已建立(重连/后台期间保持 true)。**不再驱动导航** —— 路由归 AppRouter。 + @Published var isLive = false + /// 最小化中:会话继续跑,只是不在前台。首页卡片据此置顶并标「最小化中」。 + @Published var isMinimized = false + /// 最后一次成为焦点的时刻(首页「上次活跃」+ 会话卡排序)。 + @Published var lastActiveAt = Date() + /// 链路状态(状态胶囊/状态点)。由 `syncUI()` 从 phase + 出口计划推导。 + @Published var link: LinkState = .offline + + /// 无头 UI 验证的假会话:不建连接、**不参与生命周期广播**(否则 scenePhase 广播会 + /// 经 syncUI 把注入的状态覆盖回 idle)。Release 构建恒为 false。 + private(set) var isSyntheticFixture = false + + /// 首页会话缩略卡的文本镜像(viewport 尾部若干行,剔掉尾部空行)。 + /// 不渲染第二个 surface(一 surface 一 IOSurfaceLayer,离屏渲染是已修过的泄漏雷区)。 + @Published private(set) var snapshotLines: [String] = [] + + /// 假会话注入的 tmux 概览(真实会话走 `tmuxSummary` 的派生分支)。 + private var injectedTmuxSummary: TmuxSummary? + /// 假会话注入的 tmux 会话列表(分屏菜单的 detached 行;真实会话走 controller.sessions)。 + private(set) var injectedTmuxSessions: [TmuxSessionInfo] = [] + + /// 分屏菜单的候选会话:真实会话用 controller 的 list-sessions 结果,假会话用注入值。 + var tmuxSessionList: [TmuxSessionInfo] { + if let c = tmuxController, !c.sessions.isEmpty { return c.sessions } + return injectedTmuxSessions + } + + /// tmux 概览:假会话用注入值,真实会话从 controller 现算。 + var tmuxSummary: TmuxSummary? { + if let injectedTmuxSummary { return injectedTmuxSummary } + guard let t = tmuxController else { return nil } + let active = t.windows.first { $0.id == t.activeWindowID } ?? t.windows.first + return TmuxSummary( + name: t.currentSessionName, + windows: t.windows.count, + panes: t.windows.reduce(0) { $0 + $1.panes.count }, + currentTitle: active?.title ?? "", + cwd: active?.cwd ?? "") + } + + /// 抓一次终端内容快照。tmux 模式取活动 pane 的 surface,否则取自己的。 + /// surface 未 attach 时 `readViewportText()` 返回 nil → 保留上一次快照(不清空,避免卡片闪空)。 + func captureSnapshot(maxLines: Int = 32) { + guard let text = snapshotSource?.readViewportText() else { return } + var lines = text.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) + while let last = lines.last, last.trimmingCharacters(in: .whitespaces).isEmpty { + lines.removeLast() + } + let tail = Array(lines.suffix(maxLines)) + if tail != snapshotLines { snapshotLines = tail } + } + + private var snapshotSource: InMemoryTerminalSession? { + if let t = tmuxController { + let win = t.windows.first { $0.id == t.activeWindowID } ?? t.windows.first + if let win, let paneID = win.activePane ?? win.panes.keys.first, + let surface = win.panes[paneID] { + return surface.session + } + } + return session + } + + @Published var banner: String = "" + @Published var phaseText: String = "空闲" + /// 非 nil 时进入 tmux control-mode(原生 tab)。 + @Published var tmuxController: TmuxController? + /// 把 controller 的变化**转发**成本对象的变化。 + /// + /// 必须有:`tmuxSummary` 等是读 controller 的 computed property,而轨道/胶囊/侧边栏只 + /// `@ObservedObject` 订阅了 session —— 嵌套 ObservableObject 不冒泡(见 CLAUDE.md 坑), + /// 没有这条转发,window 列表/会话名/cwd 解析对了 UI 也不刷新(真机上表现为轨道空、第三格只有「tmux」)。 + private var tmuxObserver: AnyCancellable? + /// M4:host key 与 pin 不符 → 非 nil 触发 SwiftUI alert(信任新密钥 / 取消)。 + @Published var hostKeyMismatch: HostKeyMismatchInfo? + /// 终端顶部状态行:当前连接标题(主机名/地址)。 + @Published var connectionTitle: String = "" + /// mosh 是否已接管(传输徽章 SSH/mosh)。 + @Published var moshEngaged: Bool = false + /// 连接后探测到的 tmux 会话(非 nil = 等用户选「attach 哪个 / 新建 / 跳过」)。 + /// 设计:点主机连接后先让用户挑,而不是替他决定 attach 哪个会话。 + @Published var pendingSessionChoice: TmuxSessionChoice? + + /// 用户在选择器里选了「原生终端」→ 本次会话不经 tmux,也不显示安装引导横幅(他是主动选的)。 + @Published private(set) var tmuxSkipped = false + + /// 正在探测服务器的 tmux 能力/会话。 + /// + /// **必须在 `.connected` 的那一刻就置位**(不能等 2.5s 后的 bootstrap):否则连接建立与探测开始 + /// 之间有个空窗,`SessionManager.evaluateEnter` 会看到 `isLive == true` 就把用户送进终端页, + /// 于是选择器弹在终端页上而不是首页 —— 那正是这次要改掉的错误时序。 + @Published private(set) var isProbingTmux = false + + /// 已发出 `tmux -CC` 但迟迟没进 control mode → 服务器很可能没装 tmux。 + /// 终端顶部据此显示可关闭的安装引导横幅(设计稿 2c),**不弹窗、不强推**。 + @Published var tmuxUnavailable = false + + let knownHosts = KeychainKnownHostsStore() + + private var machine = SessionMachine() + private var config: SSHConfig? + private var reconnectTask: Task? + private var autoCommand: String? + + /// 出口计划(direct / 经某 Tailscale 连接)。connect 时按 host 解析。 + private var egressPlan: EgressPlan = .direct + /// 当前会话的 Tailscale 连接 id(tsnet 出口时非 nil):mosh relay / 唤醒脉冲 / 会话计数按此 connID 操作。 + private var activeConnID: UUID? + + /// 解析 host 认证/出口所需的两个 Store(由 ContentView 注入)。 + private weak var credentialStore: (any CredentialStore)? + private weak var tailscaleStore: TailscaleStore? + + /// tmux 模式:登录后自动 `tmux -CC new -A -s ` 进 control mode(默认开)。 + private var tmuxMode = true + private var tmuxSessionName = "main" + private var tmuxBootstrapSent = false + private var tmuxProbeTask: Task? + /// 连接后的会话探测(普通 shell 阶段,进 control mode 之前)。 + private var sessionProbe: TmuxSessionProbe? + private var sessionProbeSent = false + + /// M2 mosh 模式:SSH 登录后跑 mosh-server,解析 MOSH CONNECT,改由 mosh(UDP over tsnet relay) 承载会话。 + private var moshMode = false + private var moshServerCmd = "mosh-server new -s -c 256 -l LANG=en_US.UTF-8 -l LC_ALL=en_US.UTF-8" + private let moshHolder: MoshHolder + private var moshSession: MoshSession? + private var moshRelay: TsnetbridgeMoshRelay? + private var moshScanner = MoshConnectScanner() + private var moshBootstrapSent = false + // M3 前台恢复:基线时刻 + healthy 轮询 + 恢复看门狗 + 后台任务。 + private var moshResumeBaseMs: UInt64 = 0 + private var moshHealthyPoll: Task? + private var resumeWatchdog: Task? + #if canImport(UIKit) + private var bgTask: UIBackgroundTaskIdentifier = .invalid + #endif + + private let tmuxChannel = TmuxByteChannel() + /// 全屏终端格子数(raw 模式 surface resize 时记录;tmux attach 用于 refresh-client -C)。 + let screenGrid = GridBox() + + init() { + let holder = TransportHolder() + let moshHolder = MoshHolder() + let grid = screenGrid + let session = InMemoryTerminalSession( + // 输入/resize:mosh 激活时路由到 mosh,否则走 SSH transport。 + write: { data in + if let m = moshHolder.session { m.send([UInt8](data)) } + else { holder.transport?.send(data) } + }, + resize: { vp in + // 记录全屏格子 + cell 像素(tmux resize 换算用)。 + grid.set(cols: Int(vp.columns), rows: Int(vp.rows), + cellW: Int(vp.cellWidthPixels), cellH: Int(vp.cellHeightPixels)) + if let m = moshHolder.session { m.resize(cols: Int(vp.columns), rows: Int(vp.rows)) } + else { holder.transport?.resize(cols: vp.columns, rows: vp.rows) } + } + ) + let state = TerminalViewState() + state.configuration = TerminalSurfaceOptions(backend: .inMemory(session)) + _ = state.controller.setTheme(.txDefault) // Catppuccin Mocha 终端配色 + self.holder = holder + self.moshHolder = moshHolder + self.session = session + self.state = state + self.gate = OutputGate(session: session) + + // tmux 网关字节:单消费者按序处理(enter 建 controller+喂初始,bytes 续喂)。 + let ch = tmuxChannel + Task { @MainActor [weak self] in + for await ev in ch.stream { + guard let self else { continue } + switch ev { + case .enter(let initial): self.enterTmux(initialBytes: initial) + case .bytes(let b): self.tmuxController?.feed(Data(b)) + } + } + } + } + + // MARK: - 公开入口 + + /// M4b:非 nil 时用 publickey(SE/软件签名器) 认证而非密码(launch-arg 无头验证路径)。 + private var pubkeySigner: SigningKeyProvider? + + /// 无头验证合成的临时 Tailscale 连接 id(固定,复用 "tsnet-main" 模拟器身份)。 + private static let headlessTsnetConnID = UUID(uuidString: "00000000-0000-0000-0000-0000000000FF")! + + /// 注入解析所需的 Store(ContentView.onAppear 调用)。 + func bind(credentials: any CredentialStore, tailscale: TailscaleStore) { + credentialStore = credentials + tailscaleStore = tailscale + } + + /// 会话显示名(首页卡、标题胶囊、关闭确认文案)。 + var displayName: String { + if let h = host, !h.name.isEmpty { return h.name } + return connectionTitle.isEmpty ? "终端" : connectionTitle + } + + /// 两字缩写(轨道 / 侧边栏 / 会话卡的主机头像)。 + /// 规则同设计稿:首段首字母 + 末段(末段以数字结尾取该数字,否则取首字母)—— + /// `mac-studio → ms`、`ubuntu-cn2 → u2`、`nas-home → nh`。 + var initials: String { + let name = displayName + // 纯 IP(只有数字和点):取首段前两位,"10.255.255.1" → "10"(按字母规则会得到无意义的 "11")。 + if name.allSatisfy({ $0.isNumber || $0 == "." }), let head = name.split(separator: ".").first { + return String(head.prefix(2)) + } + let parts = name.split(whereSeparator: { !$0.isLetter && !$0.isNumber }) + guard let first = parts.first, let last = parts.last, parts.count >= 2 else { + return String(name.prefix(2)).lowercased() + } + let tail = last.last.map { $0.isNumber ? String($0) : String(last.prefix(1)) } ?? "" + return (String(first.prefix(1)) + tail).lowercased() + } + + /// 是否 tmux 会话(决定关闭确认走「仅断开 / 结束会话」还是「进程随之结束」文案)。 + var isTmuxSession: Bool { tmuxController != nil } + + /// 从主机列表连接:解析凭据 + 出口,再驱动标准连接流程。 + func connect(to h: SavedHost) { + moshMode = h.useMosh + tmuxMode = h.useTmux + if let name = h.tmuxSession, !name.isEmpty { tmuxSessionName = name } + host = h + connectionTitle = h.name + + // 1) 解析认证 + let username: String + let auth: SSHConfig.Authentication + switch h.auth { + case .inlinePassword(let user, let pw): + username = user; auth = .password(pw) + case .credential(let id): + guard let store = credentialStore, + let cred = store.list().first(where: { $0.id == id }), + let secret = store.secret(for: id), + let resolved = CredentialAuth.authentication(for: cred, secret: secret) else { + phaseText = "失败:凭据不可用"; banner = "凭据已删除或需签名构建(Face ID 密钥)" + return + } + username = cred.username; auth = resolved + } + + // 2) 解析出口 + if let tid = h.tailscaleID { + guard let conn = tailscaleStore?.connection(id: tid) else { + phaseText = "失败:Tailscale 连接不存在"; return + } + if conn.needsAuthKey { + phaseText = "失败:Tailscale 未认证"; banner = "请先在 Tailscale 页完成认证"; return + } + egressPlan = .tsnet(connID: conn.id, stateDirName: conn.stateDirName, + hostname: conn.hostname, authKey: nil) + } else { + egressPlan = .direct + } + + startConnect(host: h.host, port: h.port, username: username, authentication: auth) + } + + /// 密码/签名器直连(无头验证 / 简单入口)。 + func connect(host: String, port: Int, username: String, password: String) { + let auth: SSHConfig.Authentication = pubkeySigner.map { .publicKeyCallback($0) } ?? .password(password) + startConnect(host: host, port: port, username: username, authentication: auth) + } + + /// 统一起连:构造 config(含按出口命名空间的 host key 校验)并驱动状态机。 + private func startConnect(host: String, port: Int, username: String, + authentication: SSHConfig.Authentication) { + if connectionTitle.isEmpty { connectionTitle = host } + moshEngaged = false + config = SSHConfig(host: host, port: port, username: username, + authentication: authentication, + hostKeyVerifier: makeHostKeyVerifier(egress: egressPlan.egressKey, host: host, port: port)) + run(machine.reduce(.connectRequested)) + } + + /// M4:构造 host key 校验器(TOFU)。在 ssh 串行队列同步调用;firstUse 自动 pin + 横幅, + /// mismatch 冒泡到主线程触发 alert 并返回 false(SSHSession 会断开重试)。 + private func makeHostKeyVerifier(egress: String, host: String, port: Int) -> @Sendable (Data, Int32) -> Bool { + let store = knownHosts + let triple = HostTriple(egress: egress, host: host, port: port) + return { [weak self] blob, keyType in + switch HostKey.evaluate(stored: store.lookup(triple)?.blob, presented: blob) { + case .firstUse: + store.pin(triple, record: HostKeyRecord(blob: blob, keyType: keyType)) + let fp = HostKey.opensshFingerprint(blob) + Task { @MainActor in self?.banner = "已记住 host key \(fp)" } + return true + case .trusted: + return true + case .mismatch(let stored): + let info = HostKeyMismatchInfo( + egress: egress, host: host, port: port, keyType: keyType, + presentedBlob: blob, presentedFp: HostKey.opensshFingerprint(blob), + storedFp: HostKey.opensshFingerprint(stored)) + Task { @MainActor in self?.hostKeyMismatch = info } + return false + } + } + } + + /// 用户确认信任新 host key → 覆盖 pin 并重连。 + func trustNewHostKey() { + guard let m = hostKeyMismatch else { return } + knownHosts.pin(HostTriple(egress: m.egress, host: m.host, port: m.port), + record: HostKeyRecord(blob: m.presentedBlob, keyType: m.keyType)) + hostKeyMismatch = nil + run(machine.reduce(.retryRequested)) // failed → connecting(verifier 此时将判定 trusted) + } + + /// 用户取消 → 保持断开。 + func dismissHostKeyMismatch() { hostKeyMismatch = nil } + + /// 失败 / 已关闭后重试:有 config 就走状态机重连;否则(凭据解析失败等)重跑一次 host 解析。 + func retry() { + if config != nil { + run(machine.reduce(.retryRequested)) + } else if let h = host { + connect(to: h) + } + } + + /// 切换主题时推送新终端配色到当前 raw surface 与所有 tmux pane(新建面板另经 TerminalTheme.txDefault 读取)。 + func applyTerminalTheme(_ t: TerminalTheme) { + _ = state.controller.setTheme(t) + tmuxController?.applyTerminalTheme(t) + } + + func close() { + if let id = activeConnID { TsnetRegistry.shared.markSessionActive(id, false) } + activeConnID = nil + run(machine.reduce(.closeRequested)) + } + func enterBackground() { + if moshMode { beginBgTask() } // 争取 ~30s 后台执行,mosh 每 3s ack 保活 NAT/WG/DERP + run(machine.reduce(.enteredBackground)) + } + func enterForeground() { + endBgTask() + run(machine.reduce(.enteredForeground)) + } + + /// TerminalScreen 在 surface 首次布局后调用:放行缓冲输出(+ 无头自动命令)。 + func markSurfaceReady() { + gate.markReady() + if let cmd = autoCommand { + autoCommand = nil + let holder = self.holder + Task { + try? await Task.sleep(nanoseconds: 2_500_000_000) + holder.transport?.send(Data("\n".utf8)) + try? await Task.sleep(nanoseconds: 900_000_000) + holder.transport?.send(Data((cmd + "\n").utf8)) + } + } + } + + #if DEBUG + /// 无头 UI 验证:把示意输出灌进 in-memory 终端。**不建传输连接、不驱动状态机**, + /// 只为让首页会话卡/轨道头像/状态徽标有内容可截图对拍(见 UIPreviewFixture)。 + /// 走 `gate` 而非 `session.receive`,故 surface 未 attach 时会先缓冲、就绪后 flush。 + func injectFixture(host: SavedHost, lines: [String], link: LinkState, mosh: Bool, + tmux: TmuxSummary? = nil, tmuxSessions: [TmuxSessionInfo] = []) { + isSyntheticFixture = true + injectedTmuxSummary = tmux + injectedTmuxSessions = tmuxSessions + self.host = host + connectionTitle = host.name + isLive = true + self.link = link + moshEngaged = mosh + phaseText = mosh ? "mosh 已连接" : "已连接" + gate.deliver(Data(lines.joined(separator: "\r\n").utf8)) + } + #endif + + /// 无头验证:从启动参数自动连接(simctl launch --args -txHost … -txAutoCommand …)。 + func autoConnectIfConfigured() { + let d = UserDefaults.standard + guard let host = d.string(forKey: "txHost"), !host.isEmpty, + let user = d.string(forKey: "txUser") else { return } + let port = d.integer(forKey: "txPort") + autoCommand = d.string(forKey: "txAutoCommand") + if let key = d.string(forKey: "txTsnetKey"), !key.isEmpty { + // 无头验证:合成临时 Tailscale 出口(stateDir 沿用 "tsnet-main" 复用既有模拟器身份)。 + egressPlan = .tsnet(connID: Self.headlessTsnetConnID, + stateDirName: "tsnet-main", hostname: "terminalx-ipad", authKey: key) + } + // tmux 自动进入:默认开;`-txTmux 0` 关(回归 raw 终端路径),`-txTmuxSession` 指定会话名。 + if d.object(forKey: "txTmux") != nil { tmuxMode = d.bool(forKey: "txTmux") } + if let n = d.string(forKey: "txTmuxSession"), !n.isEmpty { tmuxSessionName = n } + if d.bool(forKey: "txMosh") { + moshMode = true + if let cmd = d.string(forKey: "txMoshServerCmd"), !cmd.isEmpty { moshServerCmd = cmd } + autoCommand = nil // mosh 模式用自己的引导流程,不用 txAutoCommand + } + let realPort = port == 0 ? 22 : port + // M4b:publickey 认证(SE/软件签名器)。日志打出 authorized_keys 行供加入服务器。 + if d.bool(forKey: "txPubkeyAuth") { + pubkeySigner = SigningKeyProvider.loadOrCreate() + if let s = pubkeySigner { + NSLog("M4DBG authorized_keys hw=\(s.isHardwareBacked): \(s.authorizedKeysLine)") + } + } + // 无头验证:播种一个假 pin → 真实 host key 将 mismatch,触发 alert 路径。 + if let b64 = d.string(forKey: "txHostKeyPinOverrideBase64"), let fake = Data(base64Encoded: b64) { + knownHosts.pin(HostTriple(egress: egressPlan.egressKey, host: host, port: realPort), + record: HostKeyRecord(blob: fake, keyType: 3)) + } + connect(host: host, port: realPort, + username: user, password: d.string(forKey: "txPass") ?? "") + } + + // MARK: - 状态机副作用执行 + + private func run(_ effects: [SessionMachine.Effect]) { + for effect in effects { + switch effect { + case .startConnect: startSession() + case .scheduleReconnect(_, let delayMS): scheduleReconnect(delayMS) + case .cancelReconnectTimer: reconnectTask?.cancel(); reconnectTask = nil + case .teardownTransport: holder.transport?.stop() + case .notify(let message): banner = message + case .nudgeResume: performWakePulse(); startHealthyPollIfNeeded() + case .scheduleResumeWatchdog(let ms): scheduleResumeWatchdog(ms) + case .cancelResumeWatchdog: cancelResumeWatchdog() + case .teardownMosh: teardownMosh() + } + } + syncUI() + } + + private func startSession() { + guard let config else { return } + switch egressPlan { + case .direct: + wireAndStart(SSHSession(config: config)) + case .tsnet(let connID, let dir, let hostname, let key): + // tsnet 模式:后台经注册表 up 该连接的节点 + dial fd,再用外部 fd 建 SSHSession。 + Task.detached { + do { + let node = try TsnetRegistry.shared.node( + connID: connID, stateDirName: dir, hostname: hostname, + authKey: key, timeoutMs: 45000) + let fd = try dialFDWithRetry(node, host: config.host, port: config.port) + await MainActor.run { + self.activeConnID = connID + TsnetRegistry.shared.markSessionActive(connID, true) + self.wireAndStart(SSHSession(config: config, preconnectedFD: fd)) + } + } catch { + await MainActor.run { + self.handleTransportState(.failed("tsnet: \(error.localizedDescription)")) + } + } + } + } + } + + private func wireAndStart(_ ssh: SSHSession) { + exitTmux() // 新会话回原始模式(tmux 会话在服务端,重连后需重新 -CC attach) + let router = tmuxRouter + if moshMode { + // mosh 模式:SSH 字节喂给引导流程(既显示进度、又扫描 MOSH CONNECT)。 + ssh.onBytes = { [weak self] data in + Task { @MainActor in self?.moshBootstrapFeed(data) } + } + } else { + ssh.onBytes = { data in router.feed(data) } // 经路由器分流 raw / tmux 网关 + } + ssh.onState = { [weak self] newState in + Task { @MainActor in self?.handleTransportState(newState) } + } + holder.transport = ssh + ssh.start() + } + + /// raw 阶段的输出旁路:既喂终端渲染,也喂会话探测扫描器。 + private lazy var probeTapGate = OutputTap(downstream: gate) { [weak self] data in + Task { @MainActor in + guard let self, self.sessionProbe != nil else { return } + self.feedSessionProbe(String(decoding: data, as: UTF8.self)) + } + } + + private lazy var tmuxRouter = TmuxRouter( + rawGate: probeTapGate, + // 经有序通道传主线程(enter 先于后续 bytes,单消费者保序)。 + enterGateway: { [tmuxChannel] after in tmuxChannel.enter(after) }, + gatewayBytes: { [tmuxChannel] bytes in tmuxChannel.bytes(bytes) } + ) + + private func enterTmux(initialBytes: [UInt8]) { + tmuxProbeTask?.cancel(); tmuxProbeTask = nil + tmuxUnavailable = false + let holder = self.holder + let controller = TmuxController(sendRaw: { data in holder.transport?.send(data) }) + controller.onExit = { [weak self] in Task { @MainActor in self?.exitTmux() } } + // 告知全屏格子数 + cell 像素 → attach 时 refresh-client -C、动态 resize 换算。 + let g = screenGrid.value + controller.setClientSize(cols: g.cols, rows: g.rows) + controller.setCellPixels(w: g.cellW, h: g.cellH) + tmuxController = controller + tmuxObserver = controller.objectWillChange.sink { [weak self] _ in + self?.objectWillChange.send() + } + if !initialBytes.isEmpty { controller.feed(Data(initialBytes)) } + } + + private func exitTmux() { + tmuxObserver = nil + if tmuxController != nil { tmuxController = nil } + tmuxRouter.reset() + tmuxProbeTask?.cancel(); tmuxProbeTask = nil + tmuxBootstrapSent = false // 重连后需要重新 -CC attach + sessionProbeSent = false + sessionProbe = nil + pendingSessionChoice = nil + tmuxSkipped = false + isProbingTmux = false + } + + // MARK: - tmux 自动进入 + + /// 登录后自动进入 tmux control mode。 + /// + /// `tmux -CC new -A -s `:`-A` = 同名会话存在就 attach(断线原样接回的关键)、否则新建; + /// `-CC` = control mode(我们的 `TmuxRouter` 检测其 DCS 前导后接管字节流)。 + /// 服务器没装 tmux → shell 回 command not found,探测超时后降级为原生窗口 + 安装引导横幅。 + /// 连接建立后**先探测**服务器的 tmux 会话,等用户选择再 attach。 + /// + /// 为什么不直接 `tmux -CC new -A -s main`:那是替用户决定了 attach 哪个会话。真实运维里 + /// 服务器上常有多个会话(各跑不同任务),必须让用户挑;没装 tmux 则**不打扰**,直接原生终端。 + private func scheduleTmuxBootstrap() { + guard tmuxMode, !sessionProbeSent, tmuxController == nil else { return } + sessionProbeSent = true + banner = "检测 tmux 会话…" + Task { [weak self] in + // 等 shell 提示符就绪(登录横幅 / motd 可能还在刷)。2.5s 沿用 mosh 引导已验证的值—— + // 太短会把命令喂给还没起来的 shell,字符被丢。 + try? await Task.sleep(nanoseconds: 2_500_000_000) + guard let self else { return } + self.sessionProbe = TmuxSessionProbe() + NSLog("TMUXDBG probing tmux sessions") + self.sendToTerminal(TmuxSessionProbe.command) + self.startProbeTimeout() + } + } + + /// 探测输出的消费入口(raw 阶段的字节都会流经这里)。 + private func feedSessionProbe(_ text: String) { + guard var probe = sessionProbe else { return } + guard let result = probe.feed(text) else { + sessionProbe = probe // struct:写回累积状态 + return + } + sessionProbe = nil + tmuxProbeTask?.cancel(); tmuxProbeTask = nil + isProbingTmux = false + banner = "" + switch result { + case .unavailable: + // 没装 tmux → **不弹框**,直接用原生终端 + 行内安装引导横幅(用户明确要求)。 + NSLog("TMUXDBG probe: tmux 不可用 → 原生终端") + tmuxUnavailable = true + case .available(let version, let sessions): + NSLog("TMUXDBG probe: \(version) sessions=\(sessions.map(\.name))") + pendingSessionChoice = TmuxSessionChoice( + version: version, sessions: sessions, + defaultNewName: tmuxSessionName) + } + } + + /// 探测超时兜底(服务器不回、shell 卡住等):按无 tmux 处理,不把用户永远挂在等待里。 + private func startProbeTimeout() { + tmuxProbeTask?.cancel() + tmuxProbeTask = Task { [weak self] in + try? await Task.sleep(nanoseconds: 6_000_000_000) + guard !Task.isCancelled, let self, self.sessionProbe != nil else { return } + self.sessionProbe = nil + self.isProbingTmux = false + self.banner = "" + self.tmuxUnavailable = true + NSLog("TMUXDBG probe timeout → 按无 tmux 处理") + } + } + + // MARK: - 用户的会话选择 + + /// attach 已有会话。 + func attachTmuxSession(_ name: String) { + pendingSessionChoice = nil + isProbingTmux = false + guard tmuxController == nil else { return } + tmuxSessionName = name + tmuxBootstrapSent = true + banner = "接回 tmux 会话 \(name)…" + NSLog("TMUXDBG attach -t \(name)") + sendToTerminal("tmux -CC attach -t \(name)\n") + startTmuxProbe() + } + + /// 新建会话(`-A` 兜底:同名已存在就接回,避免报错)。 + func createTmuxSession(_ name: String) { + pendingSessionChoice = nil + isProbingTmux = false + guard tmuxController == nil else { return } + tmuxSessionName = name + tmuxBootstrapSent = true + banner = "新建 tmux 会话 \(name)…" + NSLog("TMUXDBG new -A -s \(name)") + sendToTerminal("tmux -CC new -A -s \(name)\n") + startTmuxProbe() + } + + /// 选择原生终端:不经 tmux(本次会话不再提示,也不显示安装横幅——用户是主动选的)。 + func useNativeTerminal() { + pendingSessionChoice = nil + isProbingTmux = false + tmuxUnavailable = false + tmuxSkipped = true + banner = "" + NSLog("TMUXDBG user chose native terminal") + } + + /// 探测:给足时间进 control mode,超时则判定不可用(服务器无 tmux / 命令失败)。 + private func startTmuxProbe() { + tmuxProbeTask?.cancel() + tmuxProbeTask = Task { [weak self] in + try? await Task.sleep(nanoseconds: 4_000_000_000) + guard !Task.isCancelled, let self, self.tmuxController == nil else { return } + self.tmuxUnavailable = true + self.banner = "" + NSLog("TMUXDBG probe timeout → 判定无 tmux(服务器未安装或命令失败)") + } + } + + /// 用户在横幅里点「安装并启用 tmux」后:装完再试一次。 + func retryTmux() { + guard tmuxController == nil else { return } + tmuxUnavailable = false + tmuxBootstrapSent = false + scheduleTmuxBootstrap() + } + + /// 用户关掉横幅(这台主机本次会话不再提示)。 + func dismissTmuxBanner() { tmuxUnavailable = false } + + /// 把一条命令塞进当前终端执行(安装引导用)。执行后给足时间装包,再自动重试进 tmux。 + func runInTerminal(_ command: String) { + sendToTerminal(command + "\n") + tmuxUnavailable = false + Task { [weak self] in + try? await Task.sleep(nanoseconds: 8_000_000_000) + guard let self, self.tmuxController == nil else { return } + self.retryTmux() + } + } + + /// 往当前承载通道写入(mosh 接管后走 mosh,否则走 SSH)——与终端输入同一条路径。 + private func sendToTerminal(_ text: String) { + let data = Data(text.utf8) + if let m = moshSession { m.send([UInt8](data)) } else { holder.transport?.send(data) } + } + + // MARK: - M2 mosh 编排 + + /// SSH 已连接后引导 mosh-server(延迟等 shell 提示符,只发一次)。 + private func scheduleMoshBootstrap() { + guard moshMode, !moshBootstrapSent, moshSession == nil else { return } + moshBootstrapSent = true + let holder = self.holder + let cmd = moshServerCmd + banner = "启动 mosh-server…" + Task { + try? await Task.sleep(nanoseconds: 2_500_000_000) // 等 shell 提示符就绪 + holder.transport?.send(Data("\n".utf8)) + try? await Task.sleep(nanoseconds: 800_000_000) + NSLog("MOSHDBG sent mosh-server cmd: \(cmd)") + holder.transport?.send(Data((cmd + "\n").utf8)) + } + } + + /// mosh 引导期消费 SSH 字节:显示进度 + 扫描 MOSH CONNECT,命中即切换到 mosh。 + /// mosh 接管后忽略 SSH 残余字节,避免污染 mosh 的屏幕状态。 + private func moshBootstrapFeed(_ data: Data) { + guard moshSession == nil else { return } + gate.deliver(data) // 让用户看到 mosh-server 引导输出 + let text = String(decoding: data, as: UTF8.self) + if let info = moshScanner.feed(text) { + NSLog("MOSHDBG parsed MOSH CONNECT port=\(info.port) keyLen=\(info.key.count)") + launchMosh(info) + } + } + + /// 拿到 MOSH CONNECT 后:tsnet 起 UDP relay(loopback:port) → 建 MoshSession → 接管会话。 + private func launchMosh(_ info: MoshConnectInfo) { + guard moshSession == nil, let config else { return } + banner = "mosh 连接中(server port \(info.port))…" + if let connID = activeConnID { + let host = config.host + Task.detached { + do { + NSLog("MOSHDBG startMoshRelay host=\(host) moshPort=\(info.port)") + let relay = try TsnetRegistry.shared.startMoshRelay( + connID: connID, host: host, moshPort: info.port, timeoutMs: 15000) + let localPort = relay.localPort() + NSLog("MOSHDBG relay up localPort=\(localPort)") + await MainActor.run { self.activateMosh(ip: "127.0.0.1", port: localPort, key: info.key, relay: relay) } + } catch { + NSLog("MOSHDBG relay FAILED: \(error.localizedDescription)") + await MainActor.run { self.banner = "mosh relay 失败:\(error.localizedDescription)" } + } + } + } else { + // 直连模式:iOS 允许出站 UDP,直接连 host:port。 + activateMosh(ip: config.host, port: info.port, key: info.key, relay: nil) + } + } + + private func activateMosh(ip: String, port: Int, key: String, relay: TsnetbridgeMoshRelay?) { + guard moshSession == nil else { return } + let mosh = MoshSession(ip: ip, port: port, key: key, cols: 80, rows: 24) + mosh.onBytes = { [weak self] bytes in + guard let self else { return } + self.gate.deliver(Data(bytes)) + } + mosh.onClosed = { [weak self] rc in + Task { @MainActor in + guard let self else { return } + self.run(self.machine.reduce(.moshExited(rc: Int(rc)))) + } + } + moshRelay = relay + moshSession = mosh + moshHolder.session = mosh // 输入/resize 从此路由到 mosh + mosh.start() + moshEngaged = true + NSLog("MOSHDBG activateMosh ip=\(ip) port=\(port) started") + scheduleTmuxBootstrap() // mosh 已承载会话 → 在 mosh 里进 tmux + // mosh 接管成功 → 状态机拆掉底层 idle SSH(moshActive 之后忽略其 transportClosed)。 + run(machine.reduce(.moshEstablished)) + } + + // MARK: - M3 前台快速恢复 + + /// 唤醒脉冲:tsnet WakeUp(后台线程,可能阻塞)+ relay 同端口 Rebind + mosh SIGCONT。 + private func performWakePulse() { + guard let mosh = moshSession else { return } + if moshResumeBaseMs == 0 { moshResumeBaseMs = mosh.lastHeardMs() } + if let connID = activeConnID { + Task.detached { TsnetRegistry.shared.wakeUp(connID: connID) } + } + do { try moshRelay?.rebind() } + catch { NSLog("MOSHDBG relay rebind failed: \(error.localizedDescription)") } + mosh.nudge() + NSLog("MOSHDBG wake pulse base=\(moshResumeBaseMs)") + } + + /// 轮询 mosh 探针:收到基线之后的新服务器状态即判定 SSP 已续(healthy)。 + private func startHealthyPollIfNeeded() { + guard moshHealthyPoll == nil, let mosh = moshSession else { return } + let base = moshResumeBaseMs + moshHealthyPoll = Task { [weak self] in + for _ in 0 ..< 100 { // ~10s 安全上限(看门狗更早兜底) + try? await Task.sleep(nanoseconds: 100_000_000) + if Task.isCancelled { return } + if mosh.lastHeardMs() > base { + guard let self else { return } + NSLog("TXM3 recovered base=\(base) now=\(mosh.lastHeardMs())") + self.moshHealthyPoll = nil + self.run(self.machine.reduce(.moshHealthy)) + return + } + } + } + } + + private func scheduleResumeWatchdog(_ ms: Int) { + resumeWatchdog?.cancel() + resumeWatchdog = Task { [weak self] in + try? await Task.sleep(nanoseconds: UInt64(ms) * 1_000_000) + guard !Task.isCancelled, let self else { return } + self.run(self.machine.reduce(.resumeWatchdogFired)) + } + } + + private func cancelResumeWatchdog() { + resumeWatchdog?.cancel(); resumeWatchdog = nil + moshHealthyPoll?.cancel(); moshHealthyPoll = nil + moshResumeBaseMs = 0 + } + + /// 拆除 mosh 会话(兜底全量重建 / 关闭时)。重置引导状态,允许重新引导 mosh-server。 + private func teardownMosh() { + cancelResumeWatchdog() + moshEngaged = false + moshHolder.session = nil + moshSession?.close(); moshSession = nil + try? moshRelay?.close(); moshRelay = nil + moshBootstrapSent = false + moshScanner = MoshConnectScanner() + } + + private func beginBgTask() { + #if canImport(UIKit) + guard bgTask == .invalid else { return } + bgTask = UIApplication.shared.beginBackgroundTask(withName: "mosh-keepalive") { [weak self] in + self?.endBgTask() + } + #endif + } + + private func endBgTask() { + #if canImport(UIKit) + if bgTask != .invalid { UIApplication.shared.endBackgroundTask(bgTask); bgTask = .invalid } + #endif + } + + private func handleTransportState(_ st: TransportState) { + switch st { + case .authenticating: run(machine.reduce(.authenticating)) + case .connected: + // 先置探测中,再驱动状态机 —— 顺序反了就会漏进上面注释说的空窗。 + if tmuxMode, tmuxController == nil, !sessionProbeSent { isProbingTmux = true } + run(machine.reduce(.established)) + if moshMode { scheduleMoshBootstrap() } + else { scheduleTmuxBootstrap() } // mosh 模式等 mosh 接管后再进 tmux + case .disconnected(let reason): run(machine.reduce(.transportClosed(reason: reason))) + case .failed(let message): run(machine.reduce(.authFailed(reason: message))) + case .idle, .connecting: break + } + } + + private func scheduleReconnect(_ delayMS: Int) { + reconnectTask?.cancel() + reconnectTask = Task { [weak self] in + try? await Task.sleep(nanoseconds: UInt64(delayMS) * 1_000_000) + guard !Task.isCancelled else { return } + await MainActor.run { + guard let self else { return } + self.run(self.machine.reduce(.reconnectTimerFired)) + } + } + } + + private func syncUI() { + switch machine.phase { + case .connected, .reconnecting, .waitingToReconnect, .backgroundParked, + .moshActive, .moshParked, .moshResuming: + isLive = true + case .idle, .connecting, .authenticating, .closed, .failed: + isLive = false + } + link = Self.linkState(machine.phase, egress: egressPlan) + phaseText = Self.describe(machine.phase) + } + + /// 链路状态推导:出口决定 direct/relay,重连/失败覆盖之。 + private static func linkState(_ phase: SessionMachine.Phase, egress: EgressPlan) -> LinkState { + let up: LinkState = { if case .tsnet = egress { return .relay } else { return .direct } }() + switch phase { + case .connecting, .authenticating: return .connecting + // 挂起(后台)时链路并未断,仍报上行状态;恢复中归入重连语义。 + case .connected, .moshActive, .backgroundParked, .moshParked: return up + case .waitingToReconnect(let n), .reconnecting(let n), .moshResuming(let n): + return .reconnecting(attempt: n) + case .idle, .closed, .failed: return .offline + } + } + + private static func describe(_ phase: SessionMachine.Phase) -> String { + switch phase { + case .idle: "空闲" + case .connecting: "连接中…" + case .authenticating: "认证中…" + case .connected: "已连接" + case .backgroundParked: "已挂起(后台)" + case .waitingToReconnect(let n): "等待重连(第 \(n) 次)" + case .reconnecting(let n): "重连中(第 \(n) 次)" + case .failed(let r): "失败:\(r)" + case .closed: "已关闭" + case .moshActive: "mosh 已连接" + case .moshParked: "已挂起(后台)" + case .moshResuming(let n): "mosh 恢复中(第 \(n) 次)" + } + } +} diff --git a/apps/TerminalX/iOS/Theme.swift b/apps/TerminalX/iOS/Theme.swift index d73f35d..f6433cb 100644 --- a/apps/TerminalX/iOS/Theme.swift +++ b/apps/TerminalX/iOS/Theme.swift @@ -2,86 +2,210 @@ import SwiftUI import GhosttyTerminal import GhosttyTheme -/// 统一主题:一套配色同时驱动 **app UI**(下面的颜色角色)和 **终端**(`ghosttyName` → GhosttyTheme)。 -/// 在设置里选主题后 app 与终端一起变色。新增主题 = 再写一个 `static let`,视图零改动。 -struct TXPalette: Equatable, Identifiable { +// MARK: - 设计令牌(对齐 docs/design/UI-DESIGN-HANDOFF.md) +// +// 三层分工,别混: +// TXAccent —— 唯一强调色(Nocturne blurple),**恒定不随主题变**。只做线、点、选中底和文字,绝不大面积铺色。 +// TXChrome —— app chrome 的中性阶。切 Catppuccin flavor 时这一层随之推导(4 张预生成表,见下)。 +// TXFlavor —— Catppuccin flavor 本体:驱动**终端内容区**(libghostty 渲染)+ 提供状态语义色。 +// TXPalette 是三者的门面,视图只吃 palette。 + +/// 唯一强调色:Nocturne blurple 阶。恒定 —— 换主题只换中性阶与终端配色,强调色不动。 +enum TXAccent { + /// `--color-accent`:主强调(线、点、选中描边的源色) + static let base = Color(hex: 0x9184D9) + /// accent-600:强调描边(如主题卡选中框) + static let border = Color(hex: 0x796CBF) + /// accent-900:选中底(列表行、轨道头像) + static let selectedBg = Color(hex: 0x2B2741) + /// accent-800:选中描边(配 inset stroke 用,避免选中时内容位移 1pt) + static let selectedStroke = Color(hex: 0x423A6A) + /// accent-400:强调文字 + static let text = Color(hex: 0xB5ABFC) + /// accent-300:强调文字(更亮,用于 hover/主按钮标题) + static let textBright = Color(hex: 0xD2CEFD) + /// accent-500:弱化强调文字(分组小标题) + static let mid = Color(hex: 0x968AE0) + /// accent-700:极弱强调(轨道头像 inset 描边) + static let dim = Color(hex: 0x5D5294) +} + +/// macOS 窗口按钮沿用系统色(关闭/最小化圆点),不进主题体系。 +enum TXWindowDot { + static let close = Color(hex: 0xFF5F57) + static let minimize = Color(hex: 0xFEBC2E) +} + +/// app chrome 的中性阶。**不含强调色**(那是 TXAccent 的事)。 +struct TXChrome: Equatable { + /// 舞台底 / pane 之间的缝(最深) + let bgDeep: UInt32 + /// 画布底 / 屏幕底 + let bg: UInt32 + /// 侧边栏 / 轨道 / 卡片底 + let surfaceDark: UInt32 + /// 控件底(输入框、次要按钮、徽标) + let surface: UInt32 + /// 分隔线(弱) + let divider: UInt32 + /// 描边(常规) + let border: UInt32 + /// 描边(强调 / 对话框) + let borderStrong: UInt32 + /// 描边(对话框内层 / 中等) + let borderDialog: UInt32 + /// 主文字 + let text: UInt32 + /// 次文字(neutral-300) + let text2: UInt32 + /// 次文字(neutral-400) + let text3: UInt32 + /// 三级文字(neutral-500) + let text4: UInt32 + /// 弱文字 / 占位(neutral-600) + let textWeak: UInt32 + /// 极弱(提示、注脚,neutral-700) + let textFaint: UInt32 +} + +extension TXChrome { + /// Mocha 表 = 设计交付的**精确锚点值**,其余 flavor 由此按 HSL 平移推导(见 docs/design/render-screens.py 同目录笔记)。 + static let mocha = TXChrome( + bgDeep: 0x11121C, bg: 0x161826, surfaceDark: 0x1B1D2B, surface: 0x232532, + divider: 0x2C2F3D, border: 0x3F424D, borderStrong: 0x595D6C, borderDialog: 0x4A4D5E, + text: 0xE9E9ED, text2: 0xCFD3E5, text3: 0xB2B6CA, text4: 0x9397AB, + textWeak: 0x75798C, textFaint: 0x4A4D5E) + + /// Macchiato:按 flavor base 相对 Mocha 的色相/明度差平移(文字阶抬升幅度收一半,保住对比度)。 + static let macchiato = TXChrome( + bgDeep: 0x171B28, bg: 0x1B2133, surfaceDark: 0x212737, surface: 0x292F3E, + divider: 0x323949, border: 0x464C58, borderStrong: 0x606877, borderDialog: 0x515869, + text: 0xEDEEF1, text2: 0xD3DAE9, text3: 0xB6BDCF, text4: 0x969EB0, + textWeak: 0x788091, textFaint: 0x515869) + + static let frappe = TXChrome( + bgDeep: 0x222735, bg: 0x272E3F, surfaceDark: 0x2D3443, surface: 0x363C49, + divider: 0x3F4754, border: 0x535963, borderStrong: 0x6D7582, borderDialog: 0x5E6574, + text: 0xF4F4F5, text2: 0xDCE1EB, text3: 0xBEC5D1, text4: 0x9FA6B2, + textWeak: 0x818793, textFaint: 0x5E6574) + + /// Latte 是唯一浅色 flavor:中性阶整体反转(底浅字深),不走平移。仍遵守"不用纯黑纯白"。 + static let latte = TXChrome( + bgDeep: 0xE4E6ED, bg: 0xF3F4F8, surfaceDark: 0xFAFBFD, surface: 0xEBEDF3, + divider: 0xDCDEE7, border: 0xC7CAD6, borderStrong: 0xA9ADBD, borderDialog: 0xB8BCCA, + text: 0x2A2C3A, text2: 0x43465A, text3: 0x5A5E73, text4: 0x757994, + textWeak: 0x8E92A6, textFaint: 0xB0B4C4) +} + +/// Catppuccin flavor:驱动终端内容区配色 + 提供状态语义色(徽标、状态点)。 +struct TXFlavor: Equatable { let id: String let name: String let isDark: Bool - /// 对应 GhosttyTheme 目录名,驱动终端配色(与 app 配色取自同一 Catppuccin flavor,视觉统一)。 + /// 对应 GhosttyTheme 目录名,驱动终端配色。 let ghosttyName: String - let bg: Color - let surface: Color - let surfaceElevated: Color - let accent: Color - let accentSoft: Color - let textPrimary: Color - let textSecondary: Color - let textTertiary: Color - let border: Color - let borderStrong: Color - let running: Color - let mosh: Color - let warning: Color - let offline: Color + // 语义色(Catppuccin 命名) + let green: UInt32 // 成功、提示符、在线 + let red: UInt32 // error + let yellow: UInt32 // warning、git 标记 + let blue: UInt32 // 路径、字段 + let teal: UInt32 // 勾选、mosh + let peach: UInt32 // 重连中 + let overlay: UInt32 // 次要输出、离线 + /// 终端内容区底色(Catppuccin base)。沉浸态胶囊下的预留带、首页缩略卡都铺这个色, + /// 与 libghostty 实际渲染的 pane 底色一致,视觉才连续。 + let terminalBg: UInt32 +} + +// MARK: - Palette(视图唯一入口) + +/// 统一主题门面:一套选择同时驱动 **app chrome**(中性阶)与 **终端**(`ghosttyName` → GhosttyTheme), +/// 强调色恒为 Nocturne blurple。新增主题 = 再写一个 `static let`,视图零改动。 +struct TXPalette: Equatable, Identifiable { + let flavor: TXFlavor + let chrome: TXChrome + + var id: String { flavor.id } + var name: String { flavor.name } + var isDark: Bool { flavor.isDark } + var ghosttyName: String { flavor.ghosttyName } /// 该主题的终端配色(GhosttyTheme → TerminalTheme)。找不到回退系统默认。 var terminalTheme: TerminalTheme { - GhosttyThemeCatalog.theme(named: ghosttyName)?.toTerminalTheme() ?? .default + GhosttyThemeCatalog.theme(named: flavor.ghosttyName)?.toTerminalTheme() ?? .default } + + // MARK: chrome 取色(设计 token 直通) + var bgDeep: Color { Color(hex: chrome.bgDeep) } + var bg: Color { Color(hex: chrome.bg) } + var surfaceDark: Color { Color(hex: chrome.surfaceDark) } + var surface: Color { Color(hex: chrome.surface) } + var divider: Color { Color(hex: chrome.divider) } + var border: Color { Color(hex: chrome.border) } + var borderStrong: Color { Color(hex: chrome.borderStrong) } + var borderDialog: Color { Color(hex: chrome.borderDialog) } + var text: Color { Color(hex: chrome.text) } + var text2: Color { Color(hex: chrome.text2) } + var text3: Color { Color(hex: chrome.text3) } + var text4: Color { Color(hex: chrome.text4) } + var textWeak: Color { Color(hex: chrome.textWeak) } + var textFaint: Color { Color(hex: chrome.textFaint) } + + // MARK: 状态语义色(随 flavor) + /// 在线 / 成功 / 运行中 + var online: Color { Color(hex: flavor.green) } + /// error + var danger: Color { Color(hex: flavor.red) } + /// 重连中 + var reconnecting: Color { Color(hex: flavor.peach) } + var linkGood: Color { Color(hex: flavor.green) } + var linkPath: Color { Color(hex: flavor.blue) } + /// 终端内容区底色(沉浸预留带 / 缩略卡背景)。 + var terminalBackground: Color { Color(hex: flavor.terminalBg) } + + // MARK: 兼容旧取色点 + // 各屏按设计稿重写时逐步改用上面的 chrome/TXAccent 直通名,重写完这一段就删。 + var surfaceElevated: Color { divider } + var accent: Color { TXAccent.base } + var accentSoft: Color { TXAccent.selectedBg } + var textPrimary: Color { text } + var textSecondary: Color { text3 } + var textTertiary: Color { textWeak } + var running: Color { online } + var mosh: Color { Color(hex: flavor.teal) } + var warning: Color { Color(hex: flavor.yellow) } + var offline: Color { Color(hex: flavor.overlay) } } extension TXPalette { - /// 用一套 Catppuccin flavor 的核心色构造统一 palette(app 与终端同源)。 - private static func catppuccin( - id: String, name: String, ghostty: String, isDark: Bool, - base: UInt32, surface0: UInt32, surface1: UInt32, - text: UInt32, subtext0: UInt32, overlay0: UInt32, - green: UInt32, teal: UInt32, yellow: UInt32 - ) -> TXPalette { - TXPalette( - id: id, name: name, isDark: isDark, ghosttyName: ghostty, - bg: Color(hex: base), - surface: Color(hex: surface0), - surfaceElevated: Color(hex: surface1), - accent: Color(hex: green), - accentSoft: Color(hex: green).opacity(0.16), - textPrimary: Color(hex: text), - textSecondary: Color(hex: subtext0), - textTertiary: Color(hex: overlay0), - border: (isDark ? Color.white : Color.black).opacity(0.08), - borderStrong: (isDark ? Color.white : Color.black).opacity(0.14), - running: Color(hex: green), - mosh: Color(hex: teal), - warning: Color(hex: yellow), - offline: Color(hex: overlay0) - ) - } + static let catppuccinMocha = TXPalette( + flavor: TXFlavor( + id: "catppuccin-mocha", name: "Mocha", isDark: true, ghosttyName: "Catppuccin Mocha", + green: 0xA6E3A1, red: 0xF38BA8, yellow: 0xF9E2AF, blue: 0x89B4FA, + teal: 0x94E2D5, peach: 0xFAB387, overlay: 0x6C7086, terminalBg: 0x1E1E2E), + chrome: .mocha) - static let catppuccinMocha = catppuccin( - id: "catppuccin-mocha", name: "Catppuccin Mocha", ghostty: "Catppuccin Mocha", isDark: true, - base: 0x1E1E2E, surface0: 0x313244, surface1: 0x45475A, - text: 0xCDD6F4, subtext0: 0xA6ADC8, overlay0: 0x6C7086, - green: 0xA6E3A1, teal: 0x94E2D5, yellow: 0xF9E2AF) + static let catppuccinMacchiato = TXPalette( + flavor: TXFlavor( + id: "catppuccin-macchiato", name: "Macchiato", isDark: true, ghosttyName: "Catppuccin Macchiato", + green: 0xA6DA95, red: 0xED8796, yellow: 0xEED49F, blue: 0x8AADF4, + teal: 0x8BD5CA, peach: 0xF5A97F, overlay: 0x6E738D, terminalBg: 0x24273A), + chrome: .macchiato) - static let catppuccinMacchiato = catppuccin( - id: "catppuccin-macchiato", name: "Catppuccin Macchiato", ghostty: "Catppuccin Macchiato", isDark: true, - base: 0x24273A, surface0: 0x363A4F, surface1: 0x494D64, - text: 0xCAD3F5, subtext0: 0xA5ADCB, overlay0: 0x6E738D, - green: 0xA6DA95, teal: 0x8BD5CA, yellow: 0xEED49F) + static let catppuccinFrappe = TXPalette( + flavor: TXFlavor( + id: "catppuccin-frappe", name: "Frappé", isDark: true, ghosttyName: "Catppuccin Frappe", + green: 0xA6D189, red: 0xE78284, yellow: 0xE5C890, blue: 0x8CAAEE, + teal: 0x81C8BE, peach: 0xEF9F76, overlay: 0x737994, terminalBg: 0x303446), + chrome: .frappe) - static let catppuccinFrappe = catppuccin( - id: "catppuccin-frappe", name: "Catppuccin Frappé", ghostty: "Catppuccin Frappe", isDark: true, - base: 0x303446, surface0: 0x414559, surface1: 0x51576D, - text: 0xC6D0F5, subtext0: 0xA5ADCE, overlay0: 0x737994, - green: 0xA6D189, teal: 0x81C8BE, yellow: 0xE5C890) - - static let catppuccinLatte = catppuccin( - id: "catppuccin-latte", name: "Catppuccin Latte", ghostty: "Catppuccin Latte", isDark: false, - base: 0xEFF1F5, surface0: 0xCCD0DA, surface1: 0xBCC0CC, - text: 0x4C4F69, subtext0: 0x6C6F85, overlay0: 0x9CA0B0, - green: 0x40A02B, teal: 0x179299, yellow: 0xDF8E1D) + static let catppuccinLatte = TXPalette( + flavor: TXFlavor( + id: "catppuccin-latte", name: "Latte", isDark: false, ghosttyName: "Catppuccin Latte", + green: 0x40A02B, red: 0xD20F39, yellow: 0xDF8E1D, blue: 0x1E66F5, + teal: 0x179299, peach: 0xFE640B, overlay: 0x9CA0B0, terminalBg: 0xEFF1F5), + chrome: .latte) /// 可选主题(设置里列出)。默认第一个。 static let all: [TXPalette] = [catppuccinMocha, catppuccinMacchiato, catppuccinFrappe, catppuccinLatte] @@ -118,24 +242,91 @@ extension TerminalTheme { } } -/// 结构性设计令牌(圆角/间距/字体)——不随主题变。 +// MARK: - 结构性令牌(不随主题变) + +/// 圆角 / 间距 / 字体 / 布局基准,数值取自设计交付文档。 enum TX { enum Radius { - static let card: CGFloat = 16 - static let chip: CGFloat = 10 - static let field: CGFloat = 12 + /// 徽标 / 胶囊小件 + static let badge: CGFloat = 6 + /// 控件(输入框、按钮) + static let control: CGFloat = 9 + /// 卡片 + static let card: CGFloat = 12 + /// 对话框 + static let dialog: CGFloat = 15 + /// 浮起 chrome(标题/状态胶囊) + static let capsule: CGFloat = 11 + + // 兼容旧名 + static let chip: CGFloat = 9 + static let field: CGFloat = 9 } + + /// 设计间距阶:2 / 6 / 8 / 10 / 12 / 14 / 16 / 20 / 26 / 34 / 40 enum Space { + static let hairline: CGFloat = 2 static let xs: CGFloat = 6 static let s: CGFloat = 10 static let m: CGFloat = 16 - static let l: CGFloat = 22 - static let xl: CGFloat = 32 + static let l: CGFloat = 20 + static let xl: CGFloat = 26 + static let xxl: CGFloat = 34 + /// 屏幕左右留白(首页 / 设置内容区) + static let screenGutter: CGFloat = 40 } + + /// 布局基准(pt)。竖屏值见 `.compact` 变体。 + enum Layout { + /// 折叠轨道宽(竖屏 48) + static let railWidth: CGFloat = 56 + static let railWidthCompact: CGFloat = 48 + /// 展开侧边栏宽 + static let sidebarWidth: CGFloat = 288 + /// pane 之间的缝 + static let paneGap: CGFloat = 2 + /// 拖拽热区 + static let paneDragHit: CGFloat = 12 + /// 触控命中区下限 + static let minTouch: CGFloat = 44 + /// 终端主 pane 顶部给浮起胶囊预留 + static let capsuleInset: CGFloat = 86 + /// 底部区高度:**舞台提示条与侧边栏脚共用**,故两侧的分割线落在同一条 y 上。 + static let footHeight: CGFloat = 34 + } + enum Font { + /// JetBrains Mono(chrome 与终端同源)。字体缺失时 SwiftUI 自动回退系统字体,中文走 PingFang SC。 static func mono(_ size: CGFloat, _ weight: SwiftUI.Font.Weight = .regular) -> SwiftUI.Font { - .system(size: size, weight: weight, design: .monospaced) + .custom(psName(weight), fixedSize: size) } + + /// 随动态字体缩放的变体(正文型文案用这个,终端内容不用)。 + static func monoScaled(_ size: CGFloat, _ weight: SwiftUI.Font.Weight = .regular) -> SwiftUI.Font { + .custom(psName(weight), size: size) + } + + private static func psName(_ weight: SwiftUI.Font.Weight) -> String { + switch weight { + case .bold, .heavy, .black: return "JetBrainsMono-Bold" + case .semibold: return "JetBrainsMono-SemiBold" + case .medium: return "JetBrainsMono-Medium" + default: return "JetBrainsMono-Regular" + } + } + + /// 分组小标题(600 10px, ls .09em, uppercase) + static let sectionLabel = mono(10, .semibold) + /// 屏幕主标题(500 34px) + static let screenTitle = mono(34, .medium) + /// 页面标题(500 24px) + static let pageTitle = mono(24, .medium) + /// 列表主行(500 13px) + static let listRow = mono(13, .medium) + /// 列表副行 / 元信息(400 11px) + static let listMeta = mono(11) + /// 徽标 / 胶囊(500 12px) + static let badge = mono(12, .medium) } } diff --git a/apps/TerminalX/iOS/TmuxController.swift b/apps/TerminalX/iOS/TmuxController.swift index 0d3173c..a1ec044 100644 --- a/apps/TerminalX/iOS/TmuxController.swift +++ b/apps/TerminalX/iOS/TmuxController.swift @@ -37,7 +37,11 @@ final class TmuxPaneSurface: ObservableObject, Identifiable { @MainActor final class TmuxWindow: ObservableObject, Identifiable { let id: TmuxWindowID + /// tmux `#{window_index}`(用户可见编号,轨道/侧边栏显示这个;`id` 是内部 `@N`)。 + @Published var index: Int @Published var title: String + /// 活动 pane 的工作目录(`#{pane_current_path}`)。title 是 shell 默认值时 UI 回退显示它。 + @Published var cwd: String = "" @Published var panes: [TmuxPaneID: TmuxPaneSurface] = [:] /// 渲染用:zoom 时只含被 zoom 的 pane。 @Published var visibleLayout: TmuxLayout? @@ -45,7 +49,19 @@ final class TmuxWindow: ObservableObject, Identifiable { var fullLayout: TmuxLayout? @Published var activePane: TmuxPaneID? - init(id: TmuxWindowID, title: String) { self.id = id; self.title = title } + init(id: TmuxWindowID, index: Int, title: String) { + self.id = id; self.index = index; self.title = title + } +} + +/// 一个 tmux 会话的概要(`list-sessions` 结果)。分屏菜单列「分到哪里」用。 +struct TmuxSessionInfo: Identifiable, Equatable { + var id: String { name } + let name: String + let windows: Int + /// 是否已被某客户端 attach(当前会话即 attached 的那个)。 + let isAttached: Bool + let lastActivity: Date? } /// tmux control-mode 网关:消费 `%`-协议流,把 window 映射为原生 tab、window 内多 pane 映射为分屏。 @@ -55,6 +71,8 @@ final class TmuxWindow: ObservableObject, Identifiable { final class TmuxController: ObservableObject { @Published private(set) var windows: [TmuxWindow] = [] @Published var activeWindowID: TmuxWindowID? + /// 本 tmux server 上的会话(`listSessions()` 填充,分屏菜单用)。 + @Published private(set) var sessions: [TmuxSessionInfo] = [] private let parser = TmuxControlParser() private var windowByID: [TmuxWindowID: TmuxWindow] = [:] @@ -70,7 +88,18 @@ final class TmuxController: ObservableObject { // control-mode 命令应答 FIFO 匹配。 private var attachAcked = false - private enum PendingKind { case ignore, listWindows, capturePane(TmuxPaneID) } + private enum PendingKind: CustomStringConvertible { + case ignore, listWindows, listSessions, currentSession, capturePane(TmuxPaneID) + var description: String { + switch self { + case .ignore: "ignore" + case .listWindows: "listWindows" + case .listSessions: "listSessions" + case .currentSession: "currentSession" + case .capturePane(let p): "capturePane(%\(p.raw))" + } + } + } private var pending: [PendingKind] = [] // attach kickoff(refresh-client + list-windows + 各 pane capture)必须等"容器真实几何已知"再发: @@ -80,6 +109,11 @@ final class TmuxController: ObservableObject { private var containerKnown = false private var kickoffDone = false + /// format 字段分隔符。**不要用 tab**:真机实测 tab 经 PTY 到 tmux 后变成下划线, + /// 导致 list-windows/list-sessions 整行分不开(title/cwd/会话名全丢,UI 显示兜底名)。 + /// 也不能用 `#`(tmux format 里 `#{}`/`##` 有特殊含义)。 + static let fieldSep = "|:|" + init(sendRaw: @escaping @Sendable (Data) -> Void) { self.sendRaw = sendRaw } @@ -91,8 +125,14 @@ final class TmuxController: ObservableObject { NSLog("TMUXDBG kickoff refresh-client -C \(clientCols)x\(clientRows)") sendCommand("refresh-client -C \(clientCols)x\(clientRows)") // 此刻已是容器真实格子 pending.append(.ignore) - sendCommand("list-windows -F \"#{window_id}\t#{window_name}\t#{window_active}\t#{window_layout}\t#{window_visible_layout}\"") + let sep = Self.fieldSep + sendCommand("list-windows -F \"#{window_id}\(sep)#{window_name}\(sep)#{window_active}\(sep)#{window_layout}\(sep)#{window_visible_layout}\(sep)#{pane_current_path}\(sep)#{window_index}\"") pending.append(.listWindows) + // 本客户端所在的会话名。**不能**用 list-sessions 的 `session_attached` 反推—— +// 服务器上别的客户端 attach 着其它会话时会猜错(真机实测把 `demo` 当成了当前会话)。 + sendCommand("display-message -p \"#{session_name}\"") + pending.append(.currentSession) + listSessions() // 分屏菜单要列全部会话 } /// 由 model 在 attach 前告知全屏格子(tmux 据此 refresh-client -C 布局)。 @@ -125,6 +165,10 @@ final class TmuxController: ObservableObject { } } + /// 本客户端所在的 tmux 会话名(`display-message -p "#{session_name}"` 的权威答案)。 + /// 拿不到时为空串 —— UI 只显示「tmux」而不编造名字。 + @Published private(set) var currentSessionName = "" + var activeWindow: TmuxWindow? { guard let id = activeWindowID else { return windows.first } return windowByID[id] @@ -231,18 +275,45 @@ final class TmuxController: ObservableObject { return } guard !pending.isEmpty else { return } - switch pending.removeFirst() { + let kind = pending.removeFirst() + switch kind { case .ignore: break case .listWindows: for line in r.lines { - let parts = line.split(separator: "\t", omittingEmptySubsequences: false) - guard parts.count >= 5, let w = TmuxWindowID.parse(parts[0]) else { continue } + let parts = line.components(separatedBy: Self.fieldSep) + guard parts.count >= 5, let w = TmuxWindowID.parse(parts[0]) else { + NSLog("TMUXDBG winparse FAILED count=\(parts.count) line=[\(line.prefix(160))]") + continue + } let win = ensureWindow(w) win.title = String(parts[1]) + if parts.count >= 6 { win.cwd = String(parts[5]) } + if parts.count >= 7, let idx = Int(parts[6]) { win.index = idx } if parts[2] == "1" { activeWindowID = w } applyLayout(window: w, full: String(parts[3]), visible: String(parts[4])) } + case .listSessions: + sessions = r.lines.compactMap { line in + let f = line.components(separatedBy: Self.fieldSep) + guard f.count >= 3 else { return nil } + let activity = f.count >= 4 ? Double(f[3]).map { Date(timeIntervalSince1970: $0) } : nil + return TmuxSessionInfo(name: f[0], + windows: Int(f[1]) ?? 1, + isAttached: f[2] == "1", + lastActivity: activity) + } + NSLog("TMUXDBG sessions=\(sessions.map { "\($0.name)/w\($0.windows)/a\($0.isAttached)" })") + case .currentSession: + if let name = r.lines.first?.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty { + currentSessionName = name + NSLog("TMUXDBG currentSession=\(name)") + } + case .currentSession: + if let name = r.lines.first?.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty { + currentSessionName = name + NSLog("TMUXDBG currentSession=\(name)") + } case .capturePane(let p): // 剔除尾部空行:capture-pane 常返回补满 pane 高度的尾随空行,若行数 > pane grid 行数, // 铺快照(ESC[H + N-1 个 \r\n)会溢出上滚、把顶部内容顶出屏幕(提示符被拆/内容钉底)。 @@ -263,7 +334,8 @@ final class TmuxController: ObservableObject { @discardableResult private func ensureWindow(_ w: TmuxWindowID) -> TmuxWindow { if let existing = windowByID[w] { return existing } - let win = TmuxWindow(id: w, title: "窗口 \(w.raw)") + // index 先用 id 占位,list-windows 应答到达后校正为真实 window_index。 + let win = TmuxWindow(id: w, index: w.raw, title: "窗口 \(w.raw)") windowByID[w] = win windows.append(win) windows.sort { $0.id.raw < $1.id.raw } @@ -307,6 +379,47 @@ final class TmuxController: ObservableObject { func newWindow() { sendCommand("new-window") } func killWindow(_ w: TmuxWindowID) { sendCommand("kill-window -t @\(w.raw)") } + // MARK: - 关闭 / 分屏 / resize(UI-4) + + /// 断开 tmux 客户端:**服务端会话继续跑**,下次原样接回。红点确认里的推荐动作。 + /// tmux 会回 `%exit` → 既有 `onExit` 链路收尾。 + func detach() { sendCommand("detach-client") } + + /// 结束当前 tmux 会话(服务端进程一并结束)。红点确认里的危险次要动作,需再确认一次。 + func killSession() { sendCommand("kill-session") } + + /// 分屏。tmux 语义:`-h` 左右并排(竖直分隔线)、`-v` 上下堆叠。 + /// 只发命令即可 —— tmux 回 `%layout-change`,既有 reconcile 会为新 pane 建 surface + /// 并走 capture-pane 快照链路(见 `applyLayout` / `ensurePane`)。 + func splitWindow(vertical: Bool, joinSession: String? = nil) { + if let s = joinSession { + // 把另一个会话的当前 window 拉进本 window 当 pane(tmux 原生支持,不嵌套 tmux)。 + sendCommand("join-pane \(vertical ? "-h" : "-v") -s \(s):") + } else { + sendCommand("split-window \(vertical ? "-h" : "-v")") + } + } + + /// 新建 tmux 会话并把它的 window 拉进当前 window 分屏(detached 建,避免抢走客户端)。 + func splitIntoNewSession(vertical: Bool, name: String) { + sendCommand("new-session -d -s \(name)") + sendCommand("join-pane \(vertical ? "-h" : "-v") -s \(name):") + } + + /// 拖 pane 分隔条**松手后**调整尺寸(拖动中只动视觉、不发命令,见设计约束)。 + /// 发完等 `%layout-change` 回来再更新真实 frame —— 与"路线 b"一致,客户端不自行重排。 + func resizePane(_ p: TmuxPaneID, cols: Int? = nil, rows: Int? = nil) { + if let cols, cols > 0 { sendCommand("resize-pane -t %\(p.raw) -x \(cols)") } + if let rows, rows > 0 { sendCommand("resize-pane -t %\(p.raw) -y \(rows)") } + } + + /// 枚举本 tmux server 上的会话(分屏菜单要列 detached 会话)。结果进 `sessions`。 + func listSessions() { + let sep = Self.fieldSep + sendCommand("list-sessions -F \"#{session_name}\(sep)#{session_windows}\(sep)#{session_attached}\(sep)#{session_activity}\"") + pending.append(.listSessions) + } + /// 某 pane 的 surface 就绪 → 放行其缓冲输出。 func markPaneReady(_ p: TmuxPaneID) { surfaceByPane[p]?.gate.markReady() } @@ -320,13 +433,13 @@ final class TmuxController: ObservableObject { /// 在传输后台线程调用;原始字节走线程安全的 rawGate,网关字节经闭包 hop 到主线程。 final class TmuxRouter: @unchecked Sendable { private let lock = NSLock() - private let rawGate: OutputGate + private let rawGate: any RawOutputSink private let enterGateway: @Sendable ([UInt8]) -> Void private let gatewayBytes: @Sendable ([UInt8]) -> Void private var carry: [UInt8] = [] private var inGateway = false - init(rawGate: OutputGate, + init(rawGate: any RawOutputSink, enterGateway: @escaping @Sendable ([UInt8]) -> Void, gatewayBytes: @escaping @Sendable ([UInt8]) -> Void) { self.rawGate = rawGate diff --git a/apps/TerminalX/iOS/TsnetRegistry.swift b/apps/TerminalX/iOS/TsnetRegistry.swift new file mode 100644 index 0000000..2114b3d --- /dev/null +++ b/apps/TerminalX/iOS/TsnetRegistry.swift @@ -0,0 +1,103 @@ +import Foundation +import TsnetBridge + +enum TsnetError: Error { case newNodeFailed, notUp, needsAuthKey } + +/// 多 tsnet 节点注册表:按 Tailscale 连接 id 复用节点。 +/// +/// tsnet v1.102.0 官方支持一进程多 `tsnet.Server`(各自独立 netstack;dial 走具体 node 句柄、 +/// 无全局路由,故重叠 100.x 不会误连——只要每个 host 始终经其绑定的那个 Node dial)。铁律不破: +/// 仍是一个 Go runtime、一次 gomobile bind(全在 tsnetbridge 包)。 +/// +/// 策略:**懒启动**(用到才 Up)·**Up 后常驻**(不空闲 Down,M3 mosh 恢复依赖 netstack 存活)· +/// **软上限 3**(超限 LRU 关闭无活跃会话的最久未用节点)·**in-flight 去重**(同连接并发 connect +/// 绝不 double-Up 同一 stateDir——tsnet 无文件锁,同 Dir 双 Up 会损坏状态)。 +/// +/// 线程安全(`@unchecked Sendable` + 锁):Up/dial 阻塞,由 `TerminalSession` 在后台线程调用。 +final class TsnetRegistry: @unchecked Sendable { + static let shared = TsnetRegistry() + + private let lock = NSLock() + private var nodes: [UUID: TsnetbridgeNode] = [:] + private var upLocks: [UUID: NSLock] = [:] + private var lastUsed: [UUID: Date] = [:] + private var activeSessions: Set = [] + private let maxLive = 3 + + private let stateBase: String + + init() { + stateBase = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] + } + + /// 取(必要时懒启动)某连接的 tsnet 节点。阻塞,需后台线程调用。 + /// `authKey` 仅在节点尚未注册(首次 Up)时需要;已持久化 state 的节点用 nil(走已存 node key)。 + func node(connID: UUID, stateDirName: String, hostname: String, + authKey: String?, timeoutMs: Int) throws -> TsnetbridgeNode { + lock.lock() + if let n = nodes[connID] { lastUsed[connID] = Date(); lock.unlock(); return n } + let upLock = upLocks[connID] ?? { let l = NSLock(); upLocks[connID] = l; return l }() + lock.unlock() + + // 同连接串行 Up(in-flight 去重);不同连接可并发。 + upLock.lock() + defer { upLock.unlock() } + + lock.lock() + if let n = nodes[connID] { lastUsed[connID] = Date(); lock.unlock(); return n } + lock.unlock() + + let dir = stateBase + "/" + stateDirName + try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) + guard let n = TsnetbridgeNewNode(dir, hostname) else { throw TsnetError.newNodeFailed } + try n.up(withAuthKey: authKey ?? "", timeoutMs: timeoutMs) + + lock.lock() + nodes[connID] = n + lastUsed[connID] = Date() + evictIfNeededLocked(keeping: connID) + lock.unlock() + return n + } + + /// 为 mosh 建立 UDP relay(阻塞,后台线程调用)。节点必须已 Up。 + func startMoshRelay(connID: UUID, host: String, moshPort: Int, timeoutMs: Int) throws -> TsnetbridgeMoshRelay { + lock.lock(); let n = nodes[connID]; lock.unlock() + guard let n else { throw TsnetError.notUp } + return try n.startMoshRelay(host, moshPort: moshPort, timeoutMs: timeoutMs) + } + + /// M3:唤醒某连接的 tsnet 网络层(前台恢复,可能阻塞)。 + func wakeUp(connID: UUID) { + lock.lock(); let n = nodes[connID]; lock.unlock() + n?.wakeUp() + } + + /// 标记某连接是否有活跃会话(供 LRU 不驱逐在用节点)。 + func markSessionActive(_ connID: UUID, _ active: Bool) { + lock.lock() + if active { activeSessions.insert(connID) } else { activeSessions.remove(connID) } + lock.unlock() + } + + /// 关闭并销毁某连接的节点(删连接 / LRU 驱逐)。 + func close(_ connID: UUID) { + lock.lock() + let n = nodes[connID] + nodes[connID] = nil; lastUsed[connID] = nil; activeSessions.remove(connID) + lock.unlock() + try? n?.close() + } + + /// 超出软上限时,关闭无活跃会话、最久未用的节点(保护刚取到的 keeping)。 + private func evictIfNeededLocked(keeping: UUID) { + while nodes.count > maxLive { + let candidates = nodes.keys + .filter { $0 != keeping && !activeSessions.contains($0) } + .sorted { (lastUsed[$0] ?? .distantPast) < (lastUsed[$1] ?? .distantPast) } + guard let victim = candidates.first, let n = nodes[victim] else { break } + nodes[victim] = nil; lastUsed[victim] = nil + try? n.close() + } + } +} diff --git a/apps/TerminalX/iOS/UIComponents.swift b/apps/TerminalX/iOS/UIComponents.swift new file mode 100644 index 0000000..ffd28c2 --- /dev/null +++ b/apps/TerminalX/iOS/UIComponents.swift @@ -0,0 +1,56 @@ +import SwiftUI + +/// 编辑页共享样式:深色 Form 行背景 + 主文字色。 +extension View { + func txRow(_ p: TXPalette) -> some View { + self.listRowBackground(p.surface).foregroundStyle(p.textPrimary) + } +} + +/// 深色主题的表单文本字段。 +struct TXTextField: View { + let title: String + @Binding var text: String + var placeholder: String = "" + var keyboard: UIKeyboardType = .default + var secure: Bool = false + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + Group { + if secure { + SecureField(placeholder.isEmpty ? title : placeholder, text: $text) + } else { + TextField(placeholder.isEmpty ? title : placeholder, text: $text) + .keyboardType(keyboard) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + } + } + .foregroundStyle(p.textPrimary) + .txRow(p) + } +} + +/// 空状态占位(列表为空时)。 +struct TXEmptyState: View { + let icon: String + let title: String + let subtitle: String + @EnvironmentObject var theme: TXThemeManager + private var p: TXPalette { theme.palette } + + var body: some View { + VStack(spacing: TX.Space.s) { + Image(systemName: icon) + .font(.system(size: 34, weight: .light)) + .foregroundStyle(p.textTertiary) + Text(title).font(.system(size: 15)).foregroundStyle(p.textSecondary) + Text(subtitle).font(.system(size: 13)).foregroundStyle(p.textTertiary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 48) + } +} diff --git a/apps/TerminalX/iOS/UIPreviewFixture.swift b/apps/TerminalX/iOS/UIPreviewFixture.swift new file mode 100644 index 0000000..909cbde --- /dev/null +++ b/apps/TerminalX/iOS/UIPreviewFixture.swift @@ -0,0 +1,267 @@ +#if DEBUG +import Foundation +import UIKit + +/// 无头 UI 验证脚手架(仅 DEBUG)。 +/// +/// 本项目的构建机没有 GUI,每屏都靠 `simctl io screenshot` 取证对拍设计稿;而设计稿里的 +/// 首页会话卡、轨道多主机头像、状态徽标都需要**多条已连接会话**才有内容。用真实主机验证既慢 +/// 又依赖即时凭据,故提供这条假会话通道:只灌示意输出到 in-memory 终端,**不建任何传输连接**。 +/// +/// 用法:`xcrun simctl launch 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 "terminalx://ui/<动作>"`。 + /// + /// 为什么不用坐标点击:本环境没有 GUI,idb companion 不可用;而坐标点击还会随布局改动失效。 + /// 走 URL 直接驱动 `AppRouter`/`SessionManager` 的公开语义,既稳定又正是要验的那条路径。 + /// + /// 支持:`home` · `minimize` · `close`(弹确认) · `close-now`(直接关) · `split/v|h` + /// · `split-now/v|h`(直接分屏) · `resize/`(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/`)。此时通常还在首页,故优先取待选择的会话。 + 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/`。 + 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": + if path.dropFirst().first == "pin" { router.togglePin() } else { 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 diff --git a/apps/TerminalX/project.yml b/apps/TerminalX/project.yml index dc430db..9256c91 100644 --- a/apps/TerminalX/project.yml +++ b/apps/TerminalX/project.yml @@ -21,6 +21,13 @@ targets: deploymentTarget: "17.0" sources: - path: iOS + # 打包字体(JetBrains Mono 四权重,UIAppFonts 在 TerminalX-Info.plist 声明)。 + # 必须平铺进 bundle 根(UIAppFonts 的值不带目录),故用 group 而非 folder reference。 + # OFL.txt 一并打包(OFL-1.1 要求许可随分发)。 + - path: Resources/Fonts + buildPhase: resources + excludes: + - "README.md" dependencies: - package: GhosttyKit product: GhosttyTerminal @@ -45,5 +52,7 @@ targets: TARGETED_DEVICE_FAMILY: "1,2" INFOPLIST_KEY_UILaunchScreen_Generation: YES INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: YES - # M0 渲染自检无需签名(仅模拟器);真机/上架在 M4/M5 配置。 - CODE_SIGNING_ALLOWED: NO + # 真机自动签名(公司团队 Nanjing ShenRuan)。tsnet 用户态无需 NE entitlement。 + # 签名后 Keychain(SecItem)/Secure Enclave 可用(未签名会 -34018 回退文件)。 + CODE_SIGN_STYLE: Automatic + DEVELOPMENT_TEAM: J75A6S5938 diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index c950c8e..dce1a31 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -115,4 +115,177 @@ ### 10.3 待续(UI) - 会话缩略卡区(Moshi 首屏实时终端预览横滑,需会话管理);设置页字体/工具栏配置;导航壳打磨;终端工具栏 accent tint(`inputAccessoryStyle` SwiftUI 未暴露需桥接)。 - 无头 UI 测试:`idb ui tap x y`(Python3.14 需事件循环 shim `/tmp/idbrun.py`;坐标=屏幕像素×0.5,注意设备朝向)。诊断埋点(GKDBG/txDbg*)已清理;MOSHDBG/TMUXDBG/M4DBG 仍在(旧 cleanup-todo)。 + +## 11. Hosts / Keychain / Tailscale 三分离管理(2026-07-24 本轮,对标 Termius) + +把连接拆成 **Hosts / Keychain(凭据) / Tailscale 连接** 三者分别管理,iPad 优先。设计阶段咨询 Fable 顾问(Q1–Q5 裁决)。**BUILD SUCCEEDED + 模拟器 idb 截图全流程验证 + TXCore 40 tests 绿。** + +### 11.1 数据模型(app 层新文件) +- `Credential.swift`:`Credential`(凭据 meta,无机密) + `CredentialSecret`(自描述机密)。Kind: password / privateKey(source: pasted/imported/generated, publicKeyBlob) / secureEnclave(占位)。 +- `CredentialStore.swift`:`@MainActor protocol CredentialStore`(list/secret(for:)/upsert/delete,meta 与机密分离) + `FileCredentialStore`(沙盒 `credentials.json`,版本化信封 `{"scheme":"plaintext-v1"}`,0600+`isExcludedFromBackup`+`completeFileProtection`)。**不自造加密**:无根信任下伪安全;签名后换 Keychain 后端即可(协议已抽象)。 +- `TailscaleStore.swift`:`TailscaleConnection`(每个=一个常驻 tsnet 节点;`stateDirName="tsnet-"` 创建即固化终身不变——tsnet 无文件锁,同 Dir 双 Up 损坏状态) + `TailscaleStore`(tailnets.json,无机密;authKey 注册后即弃)。 +- `HostStore.swift`:`SavedHost` 改为 `auth: AuthRef`(.inlinePassword / .credential(id)) + `tailscaleID: UUID?`;自定义 decoder 迁移旧 inline username/password 为 `.inlinePassword`(不造幽灵凭据)。 + +### 11.2 签名映射 `CredentialAuth.swift`(复用既有三认证路径,传输层零改动) +- password → `.password`;生成软件 P-256 → `SigningKeyProvider.from(x963:)` → `.publicKeyCallback`(路径A);粘贴/导入 PEM → `.privateKey`(libssh2 `publickey_frommemory` 路径B)。 +- **关键:`SSHSession.authenticate()` 三分支(.password/.privateKey/.publicKeyCallback)早已全部实现**,本次无需动 TXTransport。 +- `SigningKeyProvider` 加 `from(x963:)`/`generateSoftwareP256()`/`secureEnclaveAvailable()`(非持久探测,签名真机自动点亮 Face ID UI)。openssh-key-v1 mbedTLS 不认 → UI 警告提示 `ssh-keygen -p -m PEM` 转换。 + +### 11.3 多 tsnet 节点 `TsnetRegistry.swift`(替换旧单例 `TsnetManager`) +- 单例 `@unchecked Sendable`+锁,按 connID **懒启动/Up 后常驻/LRU 限 3/in-flight 去重**。Fable 经 tsnet v1.102.0 源码确认一进程多 `tsnet.Server` 官方支持(dial 走具体 node 句柄无全局路由→重叠 100.x 不误连;红线仅 stateDir 唯一)。 +- **坑:`TsnetbridgeNode`/`MoshRelay` 非 Sendable**,不能在 `Task.detached` 捕获或跨 actor 传 → mosh relay/wakeUp 收进 registry 按 connID 操作,model 只留 `activeConnID`(不再持 node)。 + +### 11.4 连接编排(`SSHTerminalModel`) +- `connect(to:)` 解析 AuthRef+tailscaleID → `EgressPlan`(.direct/.tsnet(connID,stateDir,hostname,authKey));`startConnect` 统一构造 config,host key pin 命名空间=`egressPlan.egressKey`(`direct`/`tsnet-`)。删实例级 `tsnetAuthKey`/`TsnetManager` 单例。 +- 无头验证:`-txTsnetKey` 合成临时连接(固定 connID + stateDir `tsnet-main` 复用模拟器身份)→ 启动参数矩阵不回归(已验证直连 SSH 到 127.0.0.1:22 走通 host key 校验+密码认证)。 +- `model.bind(credentials:tailscale:)` 由 ContentView 注入两 Store。 + +### 11.5 UI(`ContentView` + 4 个 View 文件 + `UIComponents.swift`) +- `RootSplitView`=`NavigationSplitView` 侧栏(§连接:主机 / §保险库:Keychain·Known Hosts / §网络:Tailscale / 设置)+详情;iPhone 紧凑自动坍缩。终端改 `.fullScreenCover($model.showsTerminal)` 全屏盖(退出回列表,状态保留)。 +- `HostsView.swift`(列表+HostCard+HostEditorView:认证 segmented[直接输入/从 Keychain 选]+出口 Picker[Direct/各 Tailscale])、`KeychainView.swift`(列表+CredentialEditorView:密码/SSH密钥[生成·粘贴·导入]/Face ID 占位)、`TailscaleView.swift`(列表+编辑器 name/hostname/authKey+注册按钮经 registry Up+SelfIP)、`SettingsView.swift`(主题+关于 + `KnownHostsView` pin 查看/删除)。 + +### 11.6 未验证(需用户实时资源) +真 tsnet 多节点 Up(需 authkey)、密钥/凭据成功登录(需服务器信任 key)、mosh over 新 egress plan。这些在有凭据时按 §8 流程补测。 + +--- + +## 12. Demo → 商业化 UI/UE 改造(2026-07-25 起,按 Claude Design 定稿) + +**设计定稿**(hifi,含精确 token):`docs/design/UI-DESIGN-HANDOFF.md` + `terminalX-iPadOS.dc.html`(13 屏画板)。 +渲染对拍:`python3 docs/design/render-screens.py` 拆屏 → headless Chrome 逐屏截 PNG。规划级裁决咨询 Fable 顾问。 + +**总判断**:差距不在样式而在结构。三处必须先拆的地基——① chrome 从 Catppuccin 派生改为 Nocturne 中性阶 + 恒定 blurple;② `NavigationSplitView`+`fullScreenCover` 改路由 + 会话常驻画布;③ 单会话模型改多会话并存。 + +### 12.1 路线图(6 阶段,UI-1/UI-2 已完成) +| 阶段 | 内容 | 状态 | +|---|---|---| +| UI-1 | 设计令牌地基(Nocturne chrome + blurple + JetBrains Mono) | ✅ 完成 | +| UI-2 | 导航与多会话状态模型(AppRouter/SessionManager/SessionCanvas) | ✅ 完成 | +| UI-3 | 终端主界面:56pt 轨道 + 浮起双胶囊 + 侧边栏三态 | ✅ 完成(pane 缝拖拽待做) | +| UI-4 | 首页会话卡 + 关闭确认 + 分屏菜单 + pane 拖拽 | ✅ 完成(pane 拖拽待真机 tmux 验证) | +| UI-5 | 设置·外观 + 键盘体系(GCKeyboard 检测 + 全套 ⌘ 快捷键) | 待做 | +| UI-6 | 竖屏参数化 + iPhone 首版 | 待做 | + +### 12.2 UI-1 设计令牌(`Theme.swift` 重写) +- 三层分工:`TXAccent`(Nocturne blurple 8 档,**恒定不随主题变**,只做线/点/选中底/文字)· `TXChrome`(chrome 中性阶 14 档,**4 张预生成表**)· `TXFlavor`(Catppuccin flavor,驱动终端 + 状态语义色)。`TXPalette` 是门面,旧取色名(`p.accent`/`p.textPrimary`…)保留为 computed,逐屏重写时再换直通名。 +- chrome 表推导:**Mocha 用设计交付的精确锚点值**;Macchiato/Frappé 按 flavor base 相对 Mocha 的 HSL 差平移(文字阶抬升幅度收一半,保对比度);Latte 是唯一浅色,整体反转手工写(仍不用纯黑纯白)。切 flavor → chrome 变阶、accent 不变。 +- JetBrains Mono 四权重 vendor 进 `apps/TerminalX/Resources/Fonts/`(附 OFL.txt,OFL-1.1 要求随分发)。**必须平铺 bundle 根**(`UIAppFonts` 值不带目录)→ project.yml 用 group 而非 folder reference。`TX.Font.mono` 按 weight 映射 PostScript 名,缺失时 SwiftUI 自动回退。 + +### 12.3 UI-2 导航与多会话(新 `AppRouter.swift`/`SessionManager.swift`,`SSHTerminalModel`→`TerminalSession`) +- `AppRoute = .home | .terminal(UUID)`;`SidebarState = .collapsed|.overlay|.pinned`(overlay 不占位、不 resize;pinned 钉住各 resize 一次)。 +- `TerminalSession` 加身份:`id`/`host`/`isLive`(原 showsTerminal,**不再驱动导航**)/`isMinimized`/`lastActiveAt`/`link`(LinkState 五态,由 phase+egress 推导)/`displayName`/`initials`/`isTmuxSession`。**TXCore、传输层零改动**(50 tests 保持绿)。 +- `SessionManager`:多实例持有、`open(host:)` 复用同主机会话、`focus`/`minimize`/`close`、前后台广播、⌘1…9 索引、首页排序(最小化置顶 + 最近活跃)。 +- `ContentView` → `RootView(ZStack{SessionCanvas, HomeShell})`。**SessionCanvas 常驻挂载所有会话**(详见 CLAUDE.md 坑:摘除即丢内容 + matchedGeometry 要求同层级)。host key alert 下移到 `TerminalScreen`(每会话一份,后台会话也能弹安全告警)。 +- 无头验证脚手架(DEBUG):`UIPreviewFixture` 假会话 + `terminalx://ui/*` 导航驱动 + `scripts/tx-shot.sh` 截图定向。**坑**:假会话必须 `isSyntheticFixture=true` 从生命周期广播里排除,否则 scenePhase→active 广播经 `syncUI()` 把注入状态覆盖回 idle。 +- **验收**(四步截图取证,全通过):A 终端会话1 → B `ui/minimize` 回首页(3 会话并存 + mac-studio 标「最小化中」置顶)→ C `ui/session/2` 切会话2(独立内容 + tsnet 徽标)→ D `ui/session/1` 回会话1 **内容原样**。 + +### 12.3b UI-3/UI-4 两屏还原(新文件 `TXParts` / `RailSidebar` / `TerminalChrome` / `HomeView`) +- **`TXParts.swift`** — 设计稿反复出现的小件:`TXStatusDot`(8pt + 2pt 挖底描边) · `TXInitialsAvatar`(26/38pt 两字缩写 + 右上状态点) · `TXSectionLabel`(600 10px ls .09em uppercase) · `TXBadge` · `TXChip` · `TXSearchField`(32pt) · `TXButton` · `TXIconButton`(视觉 34、命中 44) · **`TXTerminalMirror`**(终端文本镜像,底对齐)。 +- **`RailSidebar.swift`** — `RailView`(56pt/竖屏 48pt 轨道:logo 不可点 → 搜索 → 首页 → 主机头像+状态点 → 虚线+ → TMUX 段 window 数字 → 状态点 → »展开 → 齿轮) + `SidebarPanel`(288pt,遮罩/Pin 共用同一套内容) + `SidebarSessionRow`/`SidebarWindowRow`。**开合语义唯一**:logo 不可点,折叠态»展开在轨道底、展开态«收起在标题右,Pin 是独立按钮只在展开态出现。 +- **`TerminalChrome.swift`** — 浮起胶囊(`.ultraThinMaterial` + 半透底 + 描边 + 圆角 11):左上标题胶囊(红黄点/竖分隔/状态点/主机名/window title)+ 右上三格状态胶囊(`tsnet | mosh | tmux`,**只染出问题的那层**)+ 底部 34pt 提示条(文案随侧边栏三态切换)。 +- **`HomeView.swift`** — 页头(34px 标题 + 副行 + 220pt 搜索 + 新建连接 + 齿轮)→ 活动会话 3 列 `SessionCard`(卡头状态/主机名/tmux/链路 · 文本镜像 · 卡脚 windows·panes·上次活跃 + ⌘n + 回到会话)→ 全部主机 4 列 `HostTile` + 筛选 chips + 添加卡。重连/离线卡:镜像 `blur(1.5)` + `rgba(24,26,40,.72)` 遮罩 + 转圈 + 「屏幕内容已缓存,恢复后原样接回」。 +- 舞台:pane 区顶部留 `TX.Layout.capsuleInset`(86pt) 预留带 + `clipped()` 防溢出;pane 之间 2pt 缝(各让 1pt);`TmuxStage` 去掉 tab 条(window 切换移到轨道/侧边栏),保留「全 window 常驻挂载 + opacity」与容器几何单一来源。 +- **缩略卡的「真实回滚」**:`TerminalSession.captureSnapshot()` 用 `readViewportText()` 抓尾部 32 行(剔尾部空行),首页可见时每 1.5s 轮询 + 最小化前抓一次。**没有第二个 surface**。 +- fixture 扩展:`UIPreviewFixture.hosts`(7 台 + 分组/os)、`tailnets`、每条会话的 `TmuxSummary` 注入;`HostStore.injectFixtureHosts`/`TailscaleStore.injectFixtureConnections` 只进内存**不写盘**。 +- **验收**:四态截图对拍设计稿 —— 折叠轨道(08) · 遮罩态(00) · Pin 态(01,终端真的变窄) · 首页(09)。TXCore 50 tests 保持绿。 + +### 12.3c 像素级走查修正(用户走查 + 逐屏对拍,2026-07-25 二轮) +走查方法(**后续每屏都照这个来**):`scripts/tx-shot.sh` 截图 → `python3` 把设计稿 PNG 与截图统一缩放到 1366×1024 → 裁同一区域上下/左右并排 → 再对元素列做**垂直投影扫描**(逐行找与底色不同的段),把元素 y 坐标量化对比。目测靠不住(缩略图里 `#1E1E2E` 与 `#11121C` 分不出)。 + +修正项: +1. **沉浸不彻底(最严重)**:原来把 86pt 预留带做成"pane 容器整体下移",胶囊浮在舞台底色上。改为 **pane 容器从舞台顶部就开始**(终端底色 + 边框铺满),只把 surface 内容 `padding(.top, 86)` —— 预留带铺的是 `flavor.terminalBg`,故与终端内容视觉连续,胶囊真正"悬浮在终端之上"。像素采样验证:预留带与内容区同为 `#1E1E2E`。 +2. **终端页隐藏系统状态栏**(设计稿 `1b`/`3a` 顶部就是胶囊/logo,`1e`/`1g` 才有状态栏)+ `ignoresSafeArea(edges: .top)`。**坑**:`statusBarHidden` 不能挂在 `TerminalScreen` 上——它常驻挂载(SessionCanvas),会连首页一起隐藏;要挂在 `RootView` 上按 `router.route` 判断。 +3. **侧边栏动画**:三态切换统一走 `AppRouter.sidebarAnimation`(`.easeOut(0.24)`,设计要求 200–260ms),面板 `.move(edge: .leading)` 滑入 + 遮罩 `.opacity` 淡入。 +4. **底部两区同高**:侧边栏脚与舞台提示条共用 `TX.Layout.footHeight`(34pt),故两侧分割线落在同一 y;侧边栏脚原来的深色块 + 齿轮**去掉**,改 1pt `divider`(设计稿如此)。 +5. 轨道结构:**搜索/首页/设置是裸图标**(无底无框),只有 logo 与展开键有框(`TXIconButton(bare:)`);纵向节奏按扫描到的设计坐标写死(logo 28 · 头像 174/220/266 · TMUX 标签 388 · 数字 408 起);TMUX 段标签用 accent-500 而非中性灰。 +6. 侧边栏面板:logo 30pt、会话行 48pt、window 行 44pt(实测值);分组分隔线改**渐隐** LinearGradient;占位分支补回漏掉的「新建 window ⌘T」行。对齐后与设计稿偏差 ≤9pt(window 区完全重合)。 +7. 缩略卡:卡头/卡脚分隔线用 1pt `Rectangle`(`Divider().overlay()` 压不住默认色);行距 `fontSize×0.6`(设计 11px/1.5);**启发式着色** —— `readViewportText()` 只给纯文本,故按行首符号/关键字上色(`✓`绿 `error`红 `◇▸`青 `[..]`灰 提示符行亮),逼近真实终端密度。 + +### 12.3d UI-4 交互闭环(关闭确认 / 分屏菜单 / pane 拖拽) +- **`TmuxController` 新增命令**(纯追加,未动既有链路):`detach()`(`detach-client`) · `killSession()` · `splitWindow(vertical:joinSession:)`(`split-window` / 跨会话用 `join-pane -s`,**不嵌套 tmux**)· `splitIntoNewSession()`(`new-session -d` + `join-pane`)· `resizePane(cols:rows:)` · `listSessions()`(新 `PendingKind.listSessions`,结果进 `@Published sessions: [TmuxSessionInfo]`)。 +- **`AppModal` 提到 `AppRouter`**(不是视图 `@State`):⌘W/⌘D 快捷键与无头 URL 驱动都要能打开浮层,视图内部状态触达不到。 +- **关闭确认 `CloseConfirmDialog`**(3c,436/404pt):tmux → 推荐「仅断开 ⏎」(`detach-client`,服务端继续跑) + 次要红字「结束会话」(`kill-session`,**再点一次才执行**);原生窗口 → 琥珀警告 + 危险按钮(设计精确色 `#8d5560` / `rgba(92,58,63,.35)` / `#f5a0ac`)。`closeSession(kill:)` 先发 tmux 命令、延迟 250ms 再 teardown 传输层(让 `%exit` 走既有 `onExit`)。 +- **分屏菜单 `SplitMenuView`**(3b,360pt):**不问方向**(⌘D/⌘⇧D 本就是两个入口),只列「分到哪里」——当前会话(选中底 + 「当前」)· detached 会话(绿点,因为进程仍在服务端跑 + 上次活跃)· 新建 tmux 会话。前导列统一 20pt 使各行文字左缘一致。**原生窗口项灰显标「二期」**(需要一台主机多条 SSH channel,见 §12.4),不装作能用。 +- **pane 拖拽 `PaneSplitter`**:从布局树内部节点算出相邻子节点边界 → 热区 12pt + 把手 44×3pt。**拖动中只动视觉**(把手位移 + 转强调色),不改 pane frame、不发命令;**松手才发一次 `resize-pane`**,等 `%layout-change` 回来更新真实 frame(与"路线 b"一致:客户端不自行重排)。 +- **最小化过渡**:离开终端时 `opacity + scaleEffect(0.97)`(240ms easeOut)。用 `scaleEffect` 而非 frame 动画 —— 前者只是渲染变换**不触发 PTY resize**,后者会让 tmux 反复重排。完整 matchedGeometry 缩回卡片未做(fable 裁决:不对 `TerminalSurfaceView` 本体做 matchedGeometry)。 +- **验收**:三个浮层逐个对拍设计稿(3c 两套文案 + 3b 菜单);最小化往返内容原样、日志无 crash;TXCore 50 tests 绿。**pane 拖拽只做了静态编译与逻辑走查,没有真实 tmux 多 pane 会话验证过**(fixture 没有 controller)——有测试主机时必须补。 + +### 12.3e 接真实数据 + 空状态(UI-5 之前的必要一步) +之前几轮都靠 `-txUIFixture` 假会话对拍设计稿,真实路径下有数据缺口和裸露的空白屏。本轮补齐: + +**真实数据打通** +- **tmux 会话名**:kickoff 时顺带 `listSessions()`,`currentSessionName` = list-sessions 里 attached 的那个 → 状态胶囊/侧边栏标题显示真实 `tmux <会话名>`(拿不到就只显示 `tmux`,不编造)。 +- **cwd**:`list-windows` format 追加 `#{pane_current_path}` → `TmuxWindow.cwd`;标题胶囊显示「window title · ~/src/x」,侧边栏 window 行副行显示「cwd · n panes」。`abbreviate()` 把 `/home/u/x`、`/Users/u/x` 收成 `~/x`。 +- **主机的分组 / 系统**:`HostEditorView` 新增「归类」区(分组输入 + 已有分组一键选 chips + 系统描述)。此前 `SavedHost.group/os` 只有 fixture 在写,真实主机永远没有分组 → 首页 chips 恒只有「全部」。 +- IP 主机名的缩写:`10.255.255.1` 按字母规则会得到无意义的 `11` → 纯 IP 取首段前两位(`10`)。 + +**空状态(真实路径必经)** +- 首页首次运行(0 主机 0 会话):终端图标 + 「还没有主机」+ 能力说明 + 「新建连接 / 先配置 Tailscale」。 +- 有主机无会话:虚线提示条「还没有在跑的会话 —— 点下面任意主机即可连接」。 +- 搜索/筛选无命中:说明是分组还是搜索造成的 + 「清除筛选」。 +- 缩略卡无输出:「等待输出…」占位(真实会话刚连上时 `readViewportText()` 还是空的)。 +- 侧边栏无会话:「没有在跑的会话」。 +- **终端区 `TerminalStateOverlay`**:连接中(转圈 + 分阶段文案 + **已等待 Ns** + ≥5s 后出「取消连接」)/ 连接失败(错误详情 + 返回首页 + 重试)/ 已关闭(重新连接)。已连接与重连中**不渲染此层**,不遮挡终端内容。 +- 路由兜底:`route` 指向的会话已被关闭 → 自动回首页,不停在空画布上黑屏。 + +**传输层修一处真实可用性问题(`TXTransport`)** +- `SSHSession.connect()` 原来用裸 `Darwin.connect`,**阻塞到系统 TCP 超时(75s+)**,连不上的主机会让用户对着「连接中」干等。改为 `connectWithTimeout()`:置非阻塞 → connect → `poll(POLLOUT)` 等超时 → **查 `SO_ERROR`**(可写≠成功,拒绝连接也会变可写)→ 恢复阻塞(libssh2 用阻塞模式)。超时值走新增的 `SSHConfig.connectTimeoutSec`(默认 12s)。tsnet 路径(`preconnectedFD`)不经此处,不受影响。 +- 验证:不可达地址 12s 后报「失败:socket("连接失败(12s 超时)…")」,失败态覆盖层给出重试/返回。 + +### 12.3f tmux 自动进入(真机反馈修复:连上永远显示「无 tmux」) +**根因**:全代码库没有任何地方发起 `tmux -CC attach`。`TmuxRouter` 只被动检测传输字节流里的 DCS 前导,之前所有 tmux 端到端验证都靠 `-txAutoCommand "tmux -CC …"` 手动触发 → 真实点击连接只有 raw shell,状态胶囊第三格恒为「无 tmux · 客户端窗口」。不是检测坏了,是从来没人发起。 + +**修复** +- `SavedHost` 加 `useTmux: Bool = true`(旧数据迁移也默认 true)+ `tmuxSession: String?`(默认 `main`);主机编辑器新增「会话」区(tmux 开关 + 会话名 + mosh 开关,带说明)。 +- `TerminalSession.scheduleTmuxBootstrap()`:连接建立后(mosh 模式则等 mosh 接管后)延迟 **2.5s** 等 shell 提示符 → 发 `tmux -CC new -A -s <名>`。`-A` = 同名会话存在就接回(断线原样恢复的关键)。 +- **探测降级**:4s 内没进 control mode → `tmuxUnavailable = true` → 终端顶部行内横幅(设计 2c:说明「关掉 App 后服务端不会继续跑」+「安装并启用 tmux」+ 可关闭)。装完自动重试一次。 +- `exitTmux()` 重置引导标记 → 重连后重新 attach。 +- 无头参数:`-txTmux 0|1`、`-txTmuxSession <名>`。 + +**真机端到端验证通过**(192.168.99.83,macOS + fish + tmux 3.7b)。过程中查出 3 个真实 bug: + +1. **tab 分隔符被 PTY 吃掉**:`list-windows -F "…\t…"` 的 tab 经 PTY 到 tmux 后变成**下划线**,整行 `components(separatedBy:)` 分不开(`count=1`)→ title/cwd/会话名全丢、UI 显示兜底名「窗口 N」。改用 `TmuxController.fieldSep = "|:|"`。**排查坑**:`os_log` 把控制字符显示成 `_`,光看日志无法区分「真下划线」与「被显示替换的 tab」,我因此先误判为 os_log 显示问题——最后靠打印 `parts.count` 才确诊。 +2. **UI 不刷新**(正是 CLAUDE.md 里「嵌套 ObservableObject 不冒泡」那条坑的另一面):`tmuxSummary`/window 列表是读 controller 的 computed property,而轨道/胶囊/侧边栏只订阅 session → 数据解析对了 UI 也不动。修法:`TerminalSession` 用 Combine 订阅 `controller.objectWillChange` 并转发自己的 `objectWillChange`(一处解决所有读 controller 的视图)。 +3. **会话名猜错**:`currentSessionName` 原来取 `list-sessions` 里 `session_attached==1` 的那个,结果把用户自己终端 attach 的 `demo` 当成了当前会话。改用 `display-message -p "#{session_name}"`(权威:本客户端所在会话)。分屏菜单的「当前」判定也随之改为按会话名比较。 +4. 顺带修正:轨道/侧边栏的 window 编号用 `#{window_index}`(用户可见的 0/1/2…)而非内部 `window_id`(@2/@3…)。 + +**验证结果**(服务器侧交叉验证): +- tmux 自动进入 ✓ 状态胶囊 `direct | ssh | tmux main`、标题胶囊 `fish · ~ 163×50`、轨道 window `0/1/2/3/4`(与服务器 `idx=0..4 active=@3/idx=1` 完全吻合) +- **⌘D 分屏 ✓**:`split-window -h` 后服务器 `list-panes` 从 `%7 164x50` 变为 `%7 82x50 + %8 81x50`;app 端双 pane 左右并排渲染、2pt 缝、拖拽把手、活动 pane blurple 边框 +- **`resize-pane` ✓**:`ui/resize/110` 后服务器 pane 宽度变为 `110`(另一侧自动收缩到 53)——拖拽条的命令链路已验证(手势本身是标准 `DragGesture`) +- **会话服务端持久 ✓**:app 退出后 `tmux ls` 里测试会话仍在(验证完已 kill 清理) +- 新增无头验证命令:`ui/split-now/v|h`(跳过菜单直接分屏)、`ui/resize/` + +### 12.3g 连接后的会话选择器(用户定稿交互,替换掉「自动 attach 固定会话」) +**为什么改**:之前实现是连上就 `tmux -CC new -A -s main` —— 替用户决定了 attach 哪个会话。真实运维里服务器上常有多个会话各跑不同任务,必须让用户挑。 + +**新流程**:SSH 连上 → 普通 shell 里跑探测 → 弹选择器 sheet → 按选择 attach。**没装 tmux 时不弹框**(直接原生终端 + 安装引导横幅,不打扰)。 + +- **`TXCore/Tmux/TmuxSessionProbe.swift`**(纯逻辑 + 5 个单测):流式扫描 `BEGIN…END` 标记块,产出 `.available(version:sessions:)` / `.unavailable`。三个坑都在注释里: + 1. **PTY 回显**:命令本身会被回显,标记若字面出现在命令里,回显行会被当成结果起点 → 写成相邻字符串拼接 `"__TX""_SESSIONS_BEGIN"`(shell 输出完整标记,回显的是拼接前的样子)。 + 2. **三态要分开**:「没装 tmux」和「装了但没会话」的 stderr 都被 `2>/dev/null` 吞掉、标记之间同样是空的,只能靠 `tmux -V` 的版本行区分 —— 两者的正确 UI 完全不同(安装引导 vs 让用户新建)。 + 3. **别脏屏**:探测命令与输出会打在用户屏幕上 → 命令末尾接 `printf '\033[H\033[2J\033[3J'`(归位+清屏+清 scrollback,不依赖 terminfo)。刚连上时清掉的只有 motd/shell 欢迎语,首屏反而更干净。 +- **`OutputTap`**:raw 字节的旁路(同时喂终端渲染与探测扫描器)。抽 `RawOutputSink` 协议让 `TmuxRouter` 能接受 gate 或 tap —— **不动 `OutputGate` 的缓冲/flush 语义**(那是修白屏的关键)。 +- **`SessionPickerSheet.swift`**(按用户给的设计图):分段控件 `>_ Tmux` | `Herdr`(置灰) | `最近`(置灰) + 右侧「跳过 ▷|」;会话行 = 大字会话名 + 「活跃」徽标(被别的客户端占用时另标「已被接入」)+ 副行「N 个窗口 · M 分钟前」;底部「+ 新建 tmux 会话…」(点开就地输入名字)。`.presentationDetents([.medium, .large])`。 +- `TerminalSession`:`pendingSessionChoice` / `attachTmuxSession` / `createTmuxSession` / `skipTmux`(`tmuxSkipped` 让状态胶囊显示「原生窗口 · 不经 tmux」而不是谎报「无 tmux」);探测 6s 超时兜底按无 tmux 处理。 +- 无头命令:`ui/pick/<会话名>` · `ui/pick-new/<名>` · `ui/pick-skip`。 + +**时机修正(用户第二轮反馈)**:选择器属于**连接流程的一部分** —— 点主机后**留在首页**探测、在首页弹选择器、选完才进终端页;不是先把用户丢进空终端再弹框。 +- `SessionManager`:`open(host:)` 不再立刻 `router.enter`;改为 `awaitingEnter` 集合 + `@Published enterRequest`(视图层消费后进终端)。`evaluateEnter` 的就绪条件:不等选择 ∧ 不在探测 ∧(已建立 / 无 tmux / 选了原生 / 失败)。失败也进终端 —— 那里有「重试 / 返回首页」,比留在首页只有一行错误更有出路。 +- **坑**:`isProbingTmux` 必须在 `.connected` 那一刻就置位。否则「连接建立」与「探测开始(延迟 2.5s)」之间有空窗,`evaluateEnter` 看到 `isLive == true` 就把人送进终端页 —— 第一次改完就是这个症状(选择器弹在终端页上)。 +- `SessionManager` 也要转发每条会话的 `objectWillChange`(同 `TerminalSession` 转发 controller 那条坑),否则首页/选择器不会因会话内部变化刷新。 +- 首页新增 `ConnectingRow`(正在连接 / 等待选择 / 失败+重试+取消);`liveSessions` 排除正在连接的会话,避免同一会话既有进度行又有卡片(且卡片里的 tmux 信息此刻还没定,会先显示成「原生窗口」再跳变)。 +- 无头验证真实路径:`-txOpenAsHost 1`(把启动参数造成一台内存主机后走 `open(host:)`,即探测→选择→进终端;与老的 `-txHost` 直连路径互斥)。 + +**措辞修正**:右上不再是「跳过」(读起来像"跳过某个步骤"、含义模糊),改为**「原生终端」**+ terminal 图标 —— 它是与 tmux 并列的一种连接方式。`skipTmux()` → `useNativeTerminal()`,状态胶囊第三格显示「原生窗口 · 不经 tmux」。首版只有 Tmux 与原生两种,所以不做别的出口。 + +**真机验证(192.168.99.83)三条出路全过**: +- 探测 → `probe: tmux 3.7b sessions=["demo", "main"]`,选择器如设计图渲染,终端首屏干净(清屏生效) +- **attach 已有** → `attach -t demo` → `currentSession=demo` +- **新建** → `new -A -s uitest` → `currentSession=uitest`,服务器 `tmux ls` 出现 `uitest: 1 windows (attached)`(验完已 kill) +- **原生终端** → 状态胶囊 `direct | ssh | 原生窗口 · 不经 tmux` +- **完整真实路径**(`-txOpenAsHost`)→ 首页「正在连接 192.168.99.83」→ 首页上弹选择器(标题「连接到 …」)→ 选 `main` → 进终端页 `tmux main` + 轨道 window `0/1/2/3/4` + +### 12.4 首版明确推迟(Fable 裁决,勿在 UI-3~6 里偷偷做) +`mosh ` 实时数字(需改 vendor/mosh iosclient 导出 SRTT,属传输层)· ⌘K 命令面板 · 无 tmux 主机的多客户端窗口(保留横幅引导)· iPad 竖屏 pane 强制上下堆叠(会推翻「路线 b」,交给 tmux relayout)· 缩略卡 ANSI 彩色(用 `readViewportText()` 文本镜像,**绝不做离屏 Metal/双 surface**——IOSurfaceLayer 泄漏史)· 设置页连字/CJK 开关(ghostty 无运行时 mutation,灰显占位)。 diff --git a/docs/design/UI-DESIGN-HANDOFF.md b/docs/design/UI-DESIGN-HANDOFF.md new file mode 100644 index 0000000..efbfeb4 --- /dev/null +++ b/docs/design/UI-DESIGN-HANDOFF.md @@ -0,0 +1,241 @@ +# Handoff: terminalX — iPadOS 终端 App(沉浸轨道方案) + +## Overview + +terminalX 是面向 iPadOS / iOS 的原生终端运维工具,解决 iPad 远程运维 macOS / Linux 的痛点。技术选型:**libghostty**(终端引擎)· **mosh**(抗断线传输)· **Tailscale tsnet**(用户态入网)· **tmux control mode**(会话复用)。平台优先级 **iPadOS > iOS > macOS(Apple Silicon)**。 + +本包描述的是**终端主界面(沉浸轨道)+ 首页(主机管理)+ 设置**这一套已定稿的 UI,含侧边栏三态、分屏模型、关闭/最小化语义、竖屏与 iPhone 降级规则。 + +## About the Design Files + +`terminalX iPadOS.dc.html` 是**设计稿**,不是生产代码。它用 HTML/CSS 表达最终视觉与交互意图(1:1 像素尺寸),目的是让你在**目标工程里用原生技术重建**——本项目的目标环境是 **SwiftUI(iPadOS 26 / iOS 26)**,请使用工程已有的架构与组件模式实现,不要移植 HTML/CSS。 + +打开方式:浏览器直接打开该文件即可,画板可自由缩放平移。每块屏幕都带 `data-screen-label`,便于对照本文档。 + +## Fidelity + +**High-fidelity(hifi)**。颜色、字号、圆角、间距、行高均为最终值,可按本文档的精确数值实现。文案(简体中文,技术术语保留英文)也是最终稿。 + +唯一例外:终端内容区的命令输出是**示意数据**,用于验证排版密度与配色,不必照搬。 + +--- + +## Design Tokens + +### 颜色 — App chrome(Nocturne 设计系统) + +| 用途 | Hex | Nocturne token | +| --- | --- | --- | +| 画布底 / 屏幕底 | `#161826` | `--color-bg` | +| 侧边栏 / 轨道 / 卡片底 | `#1b1d2b` | surface-dark | +| 控件底(输入框、次要按钮、徽标) | `#232532` | `--color-surface` | +| 舞台底(pane 之间的缝) | `#11121c` | bg-deep | +| 分隔线(弱) | `#2c2f3d` | neutral-800 | +| 描边(常规) | `#3f424d` | neutral-700 | +| 描边(强调 / 对话框) | `#595d6c`、`#4a4d5e` | neutral-600 | +| 强调色(唯一) | `#9184d9` | `--color-accent` | +| 强调 - 边框 | `#796cbf` | accent-600 | +| 强调 - 选中底 | `#2b2741` | accent-900 | +| 强调 - 选中描边 | `#423a6a` | accent-800 | +| 强调 - 文字 | `#b5abfc` / `#d2cefd` | accent-400 / -300 | +| 强调 - 弱化文字 | `#5d5294`、`#968ae0` | accent-700 / -500 | +| 主文字 | `#e9e9ed` | `--color-text` | +| 次文字 | `#cfd3e5` / `#b2b6ca` | neutral-300 / -400 | +| 三级文字 | `#9397ab` | neutral-500 | +| 弱文字 / 占位 | `#75798c` | neutral-600 | +| 极弱(提示、注脚) | `#4a4d5e` | neutral-700 | + +**规则**:强调色只做线、点、选中底和文字,**绝不大面积铺色**;不使用纯黑纯白。 + +### 颜色 — 终端内容区(Catppuccin Mocha,libghostty 渲染) + +`base #1e1e2e`(pane 底)· `mantle #181825` · `surface0 #313244`(vim 状态栏)· `text #cdd6f4` · `subtext #a6adc8` · `overlay #6c7086`(次要输出)· `surface2 #585b70`(禁用/缓存态) +语义色:`green #a6e3a1`(成功、提示符)· `red #f38ba8`(error)· `yellow #f9e2af`(warning、git 标记)· `blue #89b4fa`(路径、字段)· `mauve #cba6f7`(关键字)· `teal #94e2d5`(勾选)· `peach #fab387`(重连中)· `pink #f5c2e7` + +设置页可切换 Mocha / Macchiato / Frappé / Latte,**chrome 的中性阶随之推导,强调色恒为 Nocturne blurple `#9184d9`**。 + +macOS 窗口按钮沿用系统色:关闭 `#ff5f57`,最小化 `#febc2e`(13pt 圆点,`inset 0 0 0 .5px rgba(0,0,0,.25)`,仅悬停/触摸时显出符号)。 + +### 字体 + +全局 **JetBrains Mono**(400 / 500 / 600 / 700),chrome 与终端同源。中文走系统回退(PingFang SC)。 + +| 场景 | 规格 | +| --- | --- | +| 屏幕主标题(首页 terminalX) | 500 34px, letter-spacing -.025em | +| 页面标题(设置、总览) | 500 24–28px, ls -.02em | +| 分区标题 | 500 22px, ls -.02em | +| 分组小标题(活动会话 / TMUX) | 600 10px, ls .09em, uppercase, `#75798c` | +| 列表主行 | 400–500 13px | +| 列表副行 / 元信息 | 400 10.5–11px, `#75798c` | +| 徽标 / 胶囊 | 500 11.5–12px | +| 终端正文(主 pane) | 400 13.5px / 1.55 | +| 终端正文(次 pane / 缩略) | 400 11–12px / 1.5 | +| 终端 pane 头 | 500 11px, `#6c7086` | +| 底部提示条 | 400 11.5px, `#75798c` | + +### 间距 / 圆角 / 阴影 + +- 间距:2 / 6 / 8 / 9 / 10 / 12 / 14 / 16 / 18 / 20 / 26 / 34 / 40(px) +- 圆角:徽标 5–7 · 控件 8–9 · 卡片 11–13 · 对话框 14–15 · 屏幕卡 18 · 手机卡 38 +- 阴影:浮起胶囊 `0 10px 30px rgba(0,0,0,.55)`;侧边栏浮层 `24px 0 60px rgba(0,0,0,.55)`;对话框 `0 30px 80px rgba(0,0,0,.7)`;卡片 `0 6px 18px rgba(0,0,0,.4)` +- 遮罩:`rgba(10,11,18,.42–.5)`,浮层用 `backdrop-filter: blur(20px)` + +### 图标 + +**Phosphor Icons**(regular)。用到:`magnifying-glass` `house` `plus` `gear-six` `caret-double-left` `caret-double-right` `caret-down` `caret-left` `push-pin` `columns` `terminal-window` `hard-drives` `desktop-tower` `cpu` `key` `shield-check` `lightning` `paint-brush` `text-aa` `keyboard` `export` `info` `warning` `x` `minus` `arrow-right` `arrow-left` `lightbulb` `moon` `check-circle` `wifi-high` `battery-high` `browsers` `rows` `git-branch` `plugs` `trash` `arrows-in-simple` `textbox` `sidebar-simple` `globe-hemisphere-west`。 + +SwiftUI 实现请换成等义 **SF Symbols**(例如 `house` / `magnifying-glass` → `house`、`magnifyingglass`;`caret-double-left` → `chevron.compact.left` 或 `sidebar.left`;`push-pin` → `pin`)。 + +--- + +## Layout 基准 + +- 画板尺寸:iPad Pro 13″ 横屏 **1366×1024pt**;竖屏 **1024×1366pt**;iPhone **393×852pt** +- 折叠轨道宽 **56pt**(竖屏 48pt),元素中心线统一在 28pt(竖屏 24pt) +- 展开侧边栏宽 **288pt** +- pane 之间的缝 **2pt**(拖拽热区 **12pt**,把手 44×3pt 圆角 2pt) +- 触控命中区不低于 **44pt**(键盘附加行按键高 38–40pt + 6pt 间距 = 44pt 节距) +- 终端主 pane 顶部预留 **86pt** 给浮起胶囊(2c 因多一条横幅为 122pt);预留带用 padding-top 实现,内容容器 `min-height:0; overflow:hidden` 防止向上溢出 +- Home indicator 条:200×4pt(iPhone 140×5pt) + +--- + +## Screens / Views + +### 1) 终端主界面 · 沉浸轨道 — `1b 终端-沉浸轨道` + +**Purpose**:真正干活的界面,终端面积最大化。 + +**Layout**:`HStack`:左 56pt 轨道 + 右舞台。舞台是 `VStack`:pane 区(flex)+ 硬件键盘提示条 34pt + home 区 14pt。pane 区 padding 2pt,上下两个 pane 比例 1.7 : 1,中间 12pt 拖拽条。 + +**轨道自上而下**:Logo 方块(34×34,圆角 11,底 `#232532`,inset 描边 `#3f424d`,字形 `›` 700 16px `#b5abfc`,**不可点**)→ 搜索图标 → 首页图标 → 1pt 分隔 → 主机头像(38×38 圆角 11,两字缩写;选中底 `#2b2741` + inset 描边 `#5d5294`;右上角 8pt 状态点,`box-shadow: 0 0 0 2px #1b1d2b` 挖底)→ 虚线 + 号 → 分隔 → `TMUX` 标签(600 9px ls .1em)+ window 数字(34×30,选中底 `#232532` + inset 描边 `#4a4d5e`)→ `Spacer` → 连接状态点 8pt → 展开键(`caret-double-right`,34×34 底 `#232532` 描边 `#3f424d`)→ 设置齿轮。 + +**浮起 chrome**(`position: absolute`,`backdrop-filter: blur(20px)`,底 `rgba(27,29,43,.86–.9)`,描边 `#3f424d`,圆角 11): +- 左上标题胶囊 `top 14, left 16`:红/黄圆点 → 1pt 竖分隔 → 6pt 绿点 → 主机名(500 12.5px)→ 「当前 window title · cwd」(400 11.5px `#75798c`) +- 右上状态胶囊 `top 14, right 16`:三格分层徽标 `tsnet` | `mosh 41ms` | `tmux dev`,格间 1×16pt 竖线 + +**底部提示条**:接硬件键盘时只留 34pt 提示条(快捷键清单);触控时替换为键盘附加行(两行,见 iPhone / 竖屏)。 + +### 2) 侧边栏三态 — `3a 侧边栏-遮罩态`、`3a 侧边栏-Pin 态` + +同一条栏的三个状态,**元素不增不减**,只是展示密度不同: + +| 状态 | 宽度 | 行为 | +| --- | --- | --- | +| 折叠 | 56pt | 只显图标与两字缩写 | +| 遮罩(默认展开) | 288pt | 面板**盖住**轨道(`left: 0`,不并排,避免两套入口),右侧终端压 `rgba(10,11,18,.5)` 遮罩,**点终端任意处收起**;终端不 reflow,不触发 PTY resize | +| Pin | 288pt | 点面板右上 `push-pin` 后钉住:遮罩消失,终端变窄,**只在钉住/解除时各 resize 一次**;再点解除回遮罩态 | + +**开合语义(务必唯一、不歧义)**:`›` 方块是 App logo,不可点。开合只有一枚按钮,两态方向相反——折叠态在轨道底部 `caret-double-right`(展开),展开态在标题右侧 `caret-double-left`(收起)。Pin 是独立按钮,只在展开态出现。快捷键 `⌘\` 开合。 + +**面板内容自上而下**:Logo + `terminalX`(500 14px)+ Pin + 收起 → 搜索框(32pt 高,圆角 8)→ **首页**行(`house` + 「首页」+ `⌘⇧H`)→ 分组「活动会话 n」→ 会话行 → 1pt 渐隐分隔 → 分组「TMUX · <主机> · <会话>」→ window 行 + 「新建 window ⌘T」→ `Spacer` → Tailscale 状态脚(绿点 + tailnet + 设备 IP + 齿轮)。 + +**行规格(左缘必须对齐)**:会话行 = 26pt 头像列 + 10pt gap;window 行 = 26×22pt 徽标列 + 10pt gap;两者标题左缘同为 **x=53**。选中态用 `box-shadow: inset 0 0 0 1px #423a6a` 而非 `border`,避免选中时内容位移 1pt。 + +### 3) 首页 · 主机管理(落地页)— `1e 主机-会话卡` + +**Purpose**:App 冷启动落点;「回到刚才那个会话」是首要动作。 + +**Layout**:状态栏 24pt → 页头(terminalX 500 34px + 副行)+ 右侧搜索 220pt / 新建连接 / 设置 → 分组「活动会话」→ **3 列会话缩略卡(flex:1)** → 分组「全部主机」+ 筛选 chips → **4 列主机卡(按内容高度)** → home 区。左右留白 40pt。 + +**会话缩略卡**(圆角 13,选中卡描边 `#423a6a` + `0 6px 18px rgba(0,0,0,.4)`):卡头 36pt(状态点 + 主机名 + tmux 会话 + RTT)→ 终端缩略(`#1e1e2e`,11px/1.5,**内容底对齐**,真实回滚)→ 卡脚 38pt(「n windows · 上次活跃」+ `⌘1/2/3` + 主动作「回到会话 →」)。离线卡加 `rgba(24,26,40,.72)` + `blur(1.5px)` 遮罩,内含 26pt 转圈 + 两行说明;**遮罩必须是 pane 的最后一个子节点**(pane 为 `position: relative`)。 + +**最小化回来的卡**:置顶并标「最小化中」,转场是从终端缩回该卡的动画。 + +### 4) 新建分屏 — `3b 新建分屏菜单` + +**不问方向**(竖分 `⌘D` / 横分 `⌘⇧D` 本来就是两个入口)。菜单只问「分到哪里」: + +- 分组「tmux 会话」:当前会话(带「当前」标签)、其余 detached 会话(title · n windows · 上次活跃)、「新建 tmux 会话…」 +- 分隔线 +- 「原生窗口 / 不经 tmux · 关 App 即结束」 + +选完**立即分屏**。菜单 360pt 宽,圆角 14,`#1b1d2b` + 描边 `#595d6c`;行高 ~40pt,前导列统一 20pt(状态点也要包在 20pt 居中列里,保证五行文字左缘一致 x=50)。 + +**模型约束**:分屏只发生在**同一台主机内**。卡片内部的分割 = 真 tmux pane(`split-window`,服务端持有,断线原样恢复);跨主机分屏**已取消**(层级过深),要同时看两台机器就切会话或 Pin 侧边栏。 + +### 5) 关闭 / 最小化 — `3c 关闭确认` + +标题胶囊左侧 macOS 红黄圆点: + +- **黄点(最小化)**:**不确认**,直接回首页,会话继续跑;卡片置顶标「最小化中」。手势:终端下滑。快捷键 `⌘M`。 +- **红点(关闭)**:**弹一次确认**,按主机能力分两套文案: + - tmux 会话 → 主按钮「仅断开 ⏎」(`detach`,服务端继续跑);左侧次要文字按钮「结束会话」(`kill-session`,需再确认) + - 原生窗口 → 琥珀警告「没有 tmux 兜底,前台进程会随之结束」;推荐「取消」,危险按钮「关闭窗口」(描边 `#8d5560`,底 `rgba(92,58,63,.35)`,字 `#f5a0ac`) + - 快捷键 `⌘W`;`esc` 取消;`⏎` 选推荐项 + +对话框 436pt / 404pt 宽,圆角 15。 + +### 6) 首页 ⇄ 终端 跳转关系 — `3d 页面跳转关系` + +**进入终端(2 条)**:点活动会话卡 / 会话行 → 恢复到**上一个焦点 pane**;新建连接或主机 + → 建好即进终端,侧边栏同步出现该会话。 +**离开终端(3 条)**:侧边栏首页行(`⌘⇧H`,会话完全不动)· 黄点最小化(同上但语义是"待会儿还回来")· 红点关闭(二次确认,tmux detach / 原生窗口结束)。 +换会话不必回首页——侧边栏直接切;回首页表示"换任务"。会话总览层(原 `⌘O` 缩放)**已取消**。 + +### 7) 无 tmux 主机 — `2c 无 tmux 主机` + +轨道下半段的 `TMUX` 段换成「窗口」段(客户端窗口,每个 = 一条独立 SSH channel / mosh 会话,可随意新建)。状态胶囊第三格写「无 tmux · 客户端窗口」而非报错。终端顶部一条可关闭的行内横幅:说明窗口由 App 维持、mosh 兜底,但**关掉 App 不会在服务端继续跑**,右侧给「安装并启用 tmux」次要按钮。不弹窗、不强推。 + +### 8) 竖屏与 iPhone — `2d iPad 竖屏`、`2d iPhone 竖屏`、`1h iPhone-终端`、`1h iPhone-主机` + +- **iPad 竖屏(1024×1366)**:轨道降到 48pt(元素中心 24pt,同样含搜索/首页/展开键);pane 由左右平铺改为**上下堆叠,最多两层**,多余 pane 折成顶部段控;状态胶囊压缩为「主机名 + RTT」两枚;底部常驻单行键盘附加行。 +- **iPhone(393×852)**:轨道下沉为**底部主机条**(ms / u2 / nh 胶囊);单 pane 全屏,顶部 3 段进度式 pane 指示 +「左右轻扫切 pane」;下拉标题打开会话列表;键盘附加行两行(esc/ctrl/tab/~///| 与 ↑↓←→/⌘K);主机页用底部 tab(主机 / 会话 / 密钥 / 设置)。 + +### 9) 设置 · 外观 — `1g 设置-外观` + +左 280pt 类别栏(外观与主题 / 终端与字体 / Tailscale / 密钥与安全 / mosh 与重连 / tmux 集成 / 键盘与手势 / 诊断日志,脚注版本行),右内容区 padding 28/40。内容:标题 + 说明 → 4 张 Catppuccin 主题卡(88pt 预览 + 5 色点 + 名称,选中描边 `#796cbf` + `check-circle`)→ 两栏(左「实时预览」终端框,右「排版」面板:字体、字号 13.5pt、行高 1.50、连字、CJK 等宽补偿、跟随系统深浅色)。 + +--- + +## Interactions & Behavior + +| 交互 | 行为 | +| --- | --- | +| 侧边栏开合 | `⌘\` 或开合键;遮罩态点终端收起;Pin 切换推挤/浮层 | +| 切会话 | 轨道主机头像 / 侧边栏会话行 / `⌘1…9`;恢复到该会话上一个焦点 pane | +| 切 window | 轨道数字 / 侧边栏 window 行 / `⌃⇥` | +| 分屏 | `⌘D` 竖分、`⌘⇧D` 横分 → 弹会话选择菜单 → 立即分屏 | +| 新建 | `⌘T` 新 tmux window;`⌘⌥T` 原生窗口 | +| 调整 pane 比例 | 拖 12pt 分隔条(把手 44×3pt);松手才向 tmux 发 `resize-pane` | +| 回首页 | `⌘⇧H` / 侧边栏首页行 | +| 最小化 | 黄点 / `⌘M` / 终端下滑;无确认 | +| 关闭 | 红点 / `⌘W`;二次确认,`⏎` = 推荐项,`esc` 取消 | +| 命令面板 | `⌘K`(模糊匹配主机、会话、动作) | +| 硬件键盘 | 检测到即收起附加行,只留 34pt 提示条;拔掉恢复附加行 | +| 长按 `⌘` | 显示全部快捷键面板(iPadOS 系统行为) | + +**动效**:状态切换 200–260ms `easeOut`;侧边栏浮层从左滑入 + 遮罩淡入;最小化 = 终端缩回首页卡片(matched geometry);重连转圈 1s 线性循环。 + +**状态徽标规则(分层,只染出问题的那层)**: +1. `tsnet` — 网络可达;断则本格转灰/琥珀 +2. `mosh ` — 数字实时跳动;`≤120ms` 绿 `#a6e3a1`,`120–200ms` 中性,`>200ms` 琥珀 `#fab387` +3. `tmux <会话名>` / 「无 tmux · 客户端窗口」 + +**重连**:指数退避,上限 30s;卡片/胶囊显示「重连中 · 第 n 次 · ms 后」;屏幕内容本地缓存,恢复后原样接回(目标 <3s)。 + +**安全**:host key TOFU 固定在钥匙串;不匹配时**拦截连接**并告警;私钥存 Secure Enclave(不可导出),密码回退默认关闭。 + +## State Management + +``` +AppRoute = .home | .terminal(sessionID) +SidebarState = .collapsed | .overlay | .pinned +Session { id, host, kind: .tmux(name) | .nativeWindow, windows[], focusedPaneID, + transport: .ssh | .sshOverTsnet | .mosh | .moshOverTsnet, + link: .direct(rtt) | .relay(derp, rtt) | .reconnecting(attempt, nextIn) | .offline, + lastActiveAt, isMinimized } +TmuxWindow { index, title, cwd, panes[] } // title 为 shell 默认值时 UI 回退显示 cwd +Host { id, name, group, user, addr, port, os, tsnetIP, hasTmux, hostKeyPinnedAt } +``` + +关键转换:`选择会话 → .terminal`;`首页/最小化 → .home`(会话保留);`关闭 → detach 或 kill → .home`;`Pin → .pinned`(触发一次 PTY resize)。 + +## Files + +- `terminalX iPadOS.dc.html` — 全部 13 块屏幕的设计稿(画板,可缩放平移) +- `CLAUDE.md` / `README.md`(源项目文档,若已在仓库中则以仓库版本为准) + +## Assets + +无位图资产。图标全部来自 Phosphor(实现时换 SF Symbols),字体 JetBrains Mono(Google Fonts),终端配色 Catppuccin。 diff --git a/docs/design/render-screens.py b/docs/design/render-screens.py new file mode 100644 index 0000000..511fc39 --- /dev/null +++ b/docs/design/render-screens.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""把设计稿画板拆成每屏一个独立 HTML,便于 headless chrome 逐屏截图。""" +import re, os, sys +from html.parser import HTMLParser + +# python3 docs/design/render-screens.py && \ +# for f in /tmp/tx-screens/*.html; do "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ +# --headless=new --window-size=1366,1024 --screenshot="${f%.html}.png" "file://$f"; done +SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "terminalX-iPadOS.dc.html") +OUT = "/tmp/tx-screens" +os.makedirs(OUT, exist_ok=True) +html = open(SRC, encoding="utf-8").read() + +# 提取 (helmet 内的全局样式) +style = "\n".join(re.findall(r"", html, re.S)) + +VOID = {"br", "img", "hr", "input", "meta", "link", "source", "area", "base", "col", "embed", "param", "track", "wbr"} + +class Finder(HTMLParser): + def __init__(self): + super().__init__(convert_charrefs=False) + self.stack = [] # 当前打开的 tag 名 + self.capture = None # (label, depth, start_offset) + self.results = [] # (label, start, end) + + def cur_off(self): + line, col = self.getpos() + return self.line_starts[line - 1] + col + + def handle_starttag(self, tag, attrs): + if tag in VOID: + return + d = dict(attrs) + if self.capture is None and "data-screen-label" in d: + self.capture = (d["data-screen-label"], len(self.stack), self.cur_off()) + self.stack.append(tag) + + def handle_startendtag(self, tag, attrs): + pass + + def handle_endtag(self, tag): + if tag in VOID: + return + while self.stack and self.stack[-1] != tag: + self.stack.pop() + if self.stack: + self.stack.pop() + if self.capture and len(self.stack) == self.capture[1]: + label, _, start = self.capture + end = self.cur_off() + len(f"") + self.results.append((label, start, end)) + self.capture = None + +f = Finder() +f.line_starts = [0] +for line in html.splitlines(keepends=True): + f.line_starts.append(f.line_starts[-1] + len(line)) +f.feed(html) + +HEAD = """ + + +""" + +manifest = [] +for i, (label, s, e) in enumerate(f.results): + frag = html[s:e] + m = re.search(r"width:(\d+)px;height:(\d+)px", frag) + w, h = (m.group(1), m.group(2)) if m else ("1366", "1024") + slug = re.sub(r"[^\w一-鿿]+", "_", label).strip("_") + path = f"{OUT}/{i:02d}_{slug}.html" + open(path, "w", encoding="utf-8").write(HEAD % style + frag + "") + manifest.append((path, w, h, label)) + print(f"{i:02d}\t{label}\t{w}x{h}\t{path}") + +with open(f"{OUT}/manifest.tsv", "w") as fh: + for p, w, h, l in manifest: + fh.write(f"{p}\t{w}\t{h}\t{l}\n") +print(f"total {len(manifest)}") diff --git a/docs/design/support.js b/docs/design/support.js new file mode 100644 index 0000000..cb009b6 --- /dev/null +++ b/docs/design/support.js @@ -0,0 +1,1911 @@ +// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/react.ts + function getReact() { + const R = window.React; + if (!R) throw new Error("dc-runtime: window.React is not available yet"); + return R; + } + function getReactDOM() { + const RD = window.ReactDOM; + if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); + return RD; + } + var h = ((...args) => getReact().createElement( + ...args + )); + + // src/parse.ts + function parseDcDocument(doc) { + const dc = doc.querySelector("x-dc"); + if (!dc) return null; + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template: dc.innerHTML, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDcText(src) { + const openMatch = /]*)?>/.exec(src); + if (!openMatch) return null; + const close = src.lastIndexOf(""); + if (close === -1 || close < openMatch.index) return null; + const template = src.slice(openMatch.index + openMatch[0].length, close); + const doc = new DOMParser().parseFromString(src, "text/html"); + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDataProps(raw) { + if (!raw) return { props: null, preview: null }; + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { props: null, preview: null }; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { props: null, preview: null }; + } + const obj = parsed; + const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; + const rest = {}; + for (const k of Object.keys(obj)) { + if (k[0] !== "$") rest[k] = obj[k]; + } + return { props: Object.keys(rest).length ? rest : null, preview }; + } + function dcNameFromPath(pathname) { + let p = pathname || ""; + try { + p = decodeURIComponent(p); + } catch { + } + const base = p.split("/").pop() || "Root"; + return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; + } + + // src/boot.ts + var BASE_CSS = ` + .sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent); + border:1px solid color-mix(in srgb,currentColor 50%,transparent); + border-radius:2px;box-sizing:border-box;overflow:hidden} + @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} + html.sc-dc-streaming .sc-placeholder, + html.sc-dc-streaming .sc-interp.sc-missing{position:relative; + background:color-mix(in srgb,currentColor 5%,transparent); + border-color:transparent} + html.sc-dc-streaming .sc-placeholder::before, + html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; + position:absolute;inset:0;pointer-events:none; + background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); + background-size:400% 100%;animation:sc-shine 1.4s ease infinite} + html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, + html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; + background:color-mix(in srgb,currentColor 8%,transparent)} + .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; + color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word} + .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; + vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;color:transparent; + user-select:none} + .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; + color:color-mix(in srgb,currentColor 50%,transparent); + background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px; + padding:0 3px} + .sc-host.sc-has-error{position:relative} + .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; + padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; + border-radius:4px;white-space:pre-wrap;pointer-events:none} + /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both + in sync until dc-runtime regains a build step. */ + @media print { + @page { margin: 0.5cm; } + figure, table { break-inside: avoid; } + #dc-root, #dc-root > .sc-host { height: auto; } + *, *::before, *::after { + print-color-adjust: exact; -webkit-print-color-adjust: exact; + backdrop-filter: none !important; -webkit-backdrop-filter: none !important; + animation-delay: -99s !important; animation-duration: .001s !important; + animation-iteration-count: 1 !important; animation-fill-mode: both !important; + animation-play-state: running !important; transition-duration: 0s !important; + } + } + `; + var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; + function rootNameForDocument(doc, loc) { + let bootPath = loc.pathname || ""; + if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { + try { + bootPath = new URL(doc.baseURI || "/").pathname; + } catch { + } + } + return dcNameFromPath(bootPath); + } + function safeDecode(s) { + try { + return decodeURIComponent(s); + } catch { + return s; + } + } + function boot(runtime, doc = document) { + const parsed = parseDcDocument(doc); + if (!parsed) return null; + const React = getReact(); + const rootName = rootNameForDocument(doc, location); + runtime.markFetched(rootName); + runtime.setRootName(rootName); + runtime.adoptParsed(rootName, parsed); + if (!window.__resources) { + fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { + const raw = t ? parseDcText(t) : null; + if (raw?.template) runtime.updateHtml(rootName, raw.template); + }).catch(() => { + }); + } + const dc = doc.querySelector("x-dc"); + const hostEl = doc.createElement("div"); + hostEl.id = "dc-root"; + dc.replaceWith(hostEl); + if (!parsed.preview) { + const s = doc.createElement("style"); + s.textContent = FULL_PAGE_CSS; + doc.head.appendChild(s); + } + const Root = runtime.getDC(rootName); + const entry = runtime.registry.get(rootName); + function StandaloneRoot() { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + entry.subs.add(sub); + return () => { + entry.subs.delete(sub); + }; + }, []); + const defaults = React.useMemo(() => { + const d = {}; + for (const k in entry.propsMeta || {}) { + const v = entry.propsMeta?.[k]?.default; + if (v !== void 0) d[k] = v; + } + return d; + }, [entry.propsMeta]); + return h(Root, { ...defaults, ...entry.propOverrides || {} }); + } + const ReactDOM = getReactDOM(); + if (ReactDOM.createRoot) + ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); + else ReactDOM.render(h(StandaloneRoot), hostEl); + return rootName; + } + + // src/expr.ts + var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; + var NUMBER_RE = /^-?\d+(\.\d+)?$/; + function resolve(vals, src) { + const expr = String(src).trim(); + if (!expr) return void 0; + if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { + return resolve(vals, expr.slice(1, -1)); + } + const eq = findTopLevelEquality(expr); + if (eq) { + const lv = resolve(vals, expr.slice(0, eq.index)); + const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); + switch (eq.op) { + case "===": + return lv === rv; + case "!==": + return lv !== rv; + case "==": + return lv == rv; + default: + return lv != rv; + } + } + if (expr[0] === "!") return !resolve(vals, expr.slice(1)); + if (expr === "true") return true; + if (expr === "false") return false; + if (expr === "null") return null; + if (expr === "undefined") return void 0; + if (NUMBER_RE.test(expr)) return Number(expr); + if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { + return expr.slice(1, -1); + } + return resolvePath(vals, expr); + } + function parensWrapWhole(expr) { + let depth = 0; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") depth++; + else if (expr[i] === ")") { + depth--; + if (depth === 0) return false; + } + } + return true; + } + function findTopLevelEquality(expr) { + let depth = 0; + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + if (c === "[" || c === "(") depth++; + else if (c === "]" || c === ")") depth--; + else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { + if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; + if (!expr.slice(0, i).trim()) continue; + const op = expr[i + 2] === "=" ? c + "==" : c + "="; + return { index: i, op }; + } + } + return null; + } + function resolvePath(vals, expr) { + const head = expr.match(IDENT_RE); + if (!head) return void 0; + let cur = vals == null ? void 0 : vals[head[0]]; + let i = head[0].length; + while (i < expr.length) { + if (expr[i] === ".") { + const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); + if (!m) return void 0; + cur = cur == null ? void 0 : cur[m[0]]; + i += 1 + m[0].length; + } else if (expr[i] === "[") { + let depth = 1; + let j = i + 1; + while (j < expr.length && depth > 0) { + if (expr[j] === "[") depth++; + else if (expr[j] === "]") { + depth--; + if (depth === 0) break; + } + j++; + } + if (depth !== 0) return void 0; + const key = resolve(vals, expr.slice(i + 1, j)); + cur = cur == null ? void 0 : cur[key]; + i = j + 1; + } else { + return void 0; + } + } + return cur; + } + + // src/encode.ts + var CAMEL_ATTR = "sc-camel-"; + var INLINE_TEXT_TAGS = new Set( + "a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr".split( + " " + ) + ); + var RAW_WRAP = { + select: "sc-raw-select", + table: "sc-raw-table", + tbody: "sc-raw-tbody", + thead: "sc-raw-thead", + tfoot: "sc-raw-tfoot", + tr: "sc-raw-tr", + td: "sc-raw-td", + th: "sc-raw-th", + caption: "sc-raw-caption" + }; + var RAW_UNWRAP = Object.fromEntries( + Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) + ); + var EVENT_MAP = { + onclick: "onClick", + onchange: "onChange", + oninput: "onInput", + onsubmit: "onSubmit", + onkeydown: "onKeyDown", + onkeyup: "onKeyUp", + onkeypress: "onKeyPress", + onmousedown: "onMouseDown", + onmouseup: "onMouseUp", + onmouseenter: "onMouseEnter", + onmouseleave: "onMouseLeave", + onfocus: "onFocus", + onblur: "onBlur", + ondoubleclick: "onDoubleClick", + oncontextmenu: "onContextMenu", + onmousemove: "onMouseMove", + onmouseover: "onMouseOver", + onmouseout: "onMouseOut", + onpointerdown: "onPointerDown", + onpointerup: "onPointerUp", + onpointermove: "onPointerMove", + onpointerenter: "onPointerEnter", + onpointerleave: "onPointerLeave", + onpointercancel: "onPointerCancel", + onpointerover: "onPointerOver", + onpointerout: "onPointerOut", + ongotpointercapture: "onGotPointerCapture", + onlostpointercapture: "onLostPointerCapture", + ontouchstart: "onTouchStart", + ontouchend: "onTouchEnd", + ontouchmove: "onTouchMove", + ontouchcancel: "onTouchCancel", + ondragstart: "onDragStart", + ondragend: "onDragEnd", + ondragenter: "onDragEnter", + ondragleave: "onDragLeave", + ondragover: "onDragOver", + onanimationstart: "onAnimationStart", + onanimationend: "onAnimationEnd", + onanimationiteration: "onAnimationIteration", + ontransitionend: "onTransitionEnd" + }; + var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; + var IMPORT_SELF_CLOSE_RE = new RegExp( + "<(x-import|dc-import)(" + ATTRS + ")/>", + "gi" + ); + var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; + function encodeCamelAttrs(html) { + return html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + } + function encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = encodeCamelAttrs(html); + for (const [real, alias] of Object.entries(RAW_WRAP)) { + html = html.replace( + new RegExp("(])", "gi"), + "$1" + alias + ); + } + return html; + } + function kebabToCamel(s) { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + } + function cssToObj(css) { + const o = {}; + for (const decl of css.split(";")) { + const i = decl.indexOf(":"); + if (i < 0) continue; + const prop = decl.slice(0, i).trim(); + o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); + } + return o; + } + function compileAttr(raw) { + const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); + if (whole) { + const path = whole[1]; + return (vals) => resolve(vals, path); + } + if (raw.includes("{{")) { + const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); + return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); + } + return () => raw; + } + + // src/compile.ts + function collectProps(node, kind, host) { + const propGetters = []; + const pseudoClasses = []; + let hintSize = null; + for (const { name, value } of [...node.attributes]) { + if (name === "sc-name" || name === "data-dc-tpl") continue; + let key = name; + if (key.startsWith(CAMEL_ATTR)) + key = kebabToCamel(key.slice(CAMEL_ATTR.length)); + if (key === "hint-size") { + hintSize = value; + continue; + } + if (key.startsWith("style-")) { + pseudoClasses.push(host.pseudoClass(key.slice(6), value)); + continue; + } + if (kind !== "dom") { + if (key.includes("-") && !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-")))) + key = kebabToCamel(key); + } else { + if (key === "class") key = "className"; + else if (key === "for") key = "htmlFor"; + else if (key.startsWith("on")) + key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); + } + propGetters.push([key, compileAttr(value)]); + } + return { propGetters, pseudoClasses, hintSize }; + } + var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ + "position", + "left", + "right", + "top", + "bottom", + "inset", + "width", + "height", + "z-index", + "transform" + ]); + function hostPositionStyle(style) { + const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; + if (!all) return void 0; + const out = {}; + for (const [k, v] of Object.entries(all)) { + const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); + if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; + } + return Object.keys(out).length ? out : void 0; + } + function compileTemplate(html, host) { + const tpl = document.createElement("template"); + //! nosemgrep: direct-inner-html-assignment + tpl.innerHTML = encodeCase(html); + let tplN = 0; + (function stamp(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + node.setAttribute("data-dc-tpl", String(tplN++)); + } + for (const c of node.childNodes) stamp(c); + })(tpl.content); + const builders = walkChildren(tpl.content, host); + const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); + render.__annotated = tpl.innerHTML; + return render; + } + function walkChildren(node, host) { + return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); + } + var SLIDE_ID_VALUE_RE = /^[0-9a-f]{8}$/; + var DECK_CONTROL_FLOW_RE = /^(sc-if|sc-for|sc-else|dc-import|x-import)$/; + var DECK_AUX_RE = /^(template|script|style|sc-helmet|helmet)$/; + function isDeckMountTag(el) { + if (el.localName === "deck-stage") return true; + return el.localName === "x-import" && (el.getAttribute("component-from-global-scope") || "") === "deck-stage"; + } + function walkDeckChildren(el, host) { + const pairs = [...el.childNodes].map((c) => ({ c, b: walk(c, host) })).filter((p) => p.b !== null); + const kids = pairs.map((p) => p.b); + const seen = /* @__PURE__ */ new Set(); + const wsSeen = /* @__PURE__ */ new Map(); + const keys = []; + const nextSlideId = new Array(pairs.length); + { + let upcoming = null; + for (let j = pairs.length - 1; j >= 0; j--) { + const n = pairs[j].c; + if (n.nodeType === Node.ELEMENT_NODE) { + const t = n.localName; + upcoming = !DECK_AUX_RE.test(t) && !DECK_CONTROL_FLOW_RE.test(t) ? n.getAttribute("data-om-slide-id") : null; + } + nextSlideId[j] = upcoming; + } + } + for (let j = 0; j < pairs.length; j++) { + const { c } = pairs[j]; + if (c.nodeType === Node.TEXT_NODE) { + if ((c.nodeValue ?? "").trim() === "") { + const base = nextSlideId[j] ? "omid-ws:" + nextSlideId[j] : "omid-ws:aux"; + const n = wsSeen.get(base) ?? 0; + wsSeen.set(base, n + 1); + keys.push(n === 0 ? base : base + ":" + n); + continue; + } + return { kids, keys: null }; + } + if (c.nodeType !== Node.ELEMENT_NODE) { + keys.push(j); + continue; + } + const child = c; + const tag = child.localName; + if (DECK_AUX_RE.test(tag)) { + keys.push(j); + continue; + } + if (DECK_CONTROL_FLOW_RE.test(tag)) return { kids, keys: null }; + const v = child.getAttribute("data-om-slide-id"); + if (!v || !SLIDE_ID_VALUE_RE.test(v) || seen.has(v)) { + return { kids, keys: null }; + } + seen.add(v); + keys.push("omid:" + v); + } + return { kids, keys }; + } + function renderDeckKids(kids, kidKeys, vals, ctx) { + return kids.map((b, j) => { + const k = kidKeys ? kidKeys[j] : j; + const out = b(vals, ctx, k); + return kidKeys != null && typeof out === "string" ? h(getReact().Fragment, { key: k }, out) : out; + }); + } + function walk(node, host) { + if (node.nodeType === Node.TEXT_NODE) return walkText(node); + if (node.nodeType !== Node.ELEMENT_NODE) return null; + const el = node; + const tag = el.tagName.toLowerCase(); + if (tag === "sc-for") return walkFor(el, host); + if (tag === "sc-if") return walkIf(el, host); + if (tag === "x-import") return walkXImport(el, host); + if (tag === "sc-helmet") return host.helmet(el); + if (tag === "dc-import") return walkComponent(el, host); + return walkElement(el, host); + } + var warnedHoles = /* @__PURE__ */ new Set(); + function warnUnresolved(ctx, what) { + const key = (ctx?.__name || "?") + "\0" + what; + if (warnedHoles.has(key)) return; + warnedHoles.add(key); + console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); + } + function walkText(node) { + const txt = node.nodeValue ?? ""; + if (!txt.includes("{{")) { + if (!txt.trim() && !txt.includes(" ")) return null; + return () => txt; + } + const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); + return (vals, ctx, key) => h( + getReact().Fragment, + { key }, + ...parts.map((p, i) => { + if (!(i & 1)) return p; + const v = resolve(vals, p); + if (v === void 0) { + if (!ctx?.__streamingNow) { + if (document.body?.hasAttribute("data-dc-editor-on")) { + return h( + "span", + { key: i, className: "sc-interp sc-unresolved" }, + "{{ " + p.trim() + " }}" + ); + } + warnUnresolved( + ctx, + "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" + ); + return null; + } + return h( + "span", + { key: i, className: "sc-interp sc-missing" }, + p.trim() + ); + } + if (getReact().isValidElement(v) || Array.isArray(v)) { + return h(getReact().Fragment, { key: i }, v); + } + if (v === null || typeof v === "boolean") return null; + return h("span", { key: i, className: "sc-interp" }, String(v)); + }) + ); + } + function walkFor(el, host) { + const listGet = compileAttr(el.getAttribute("list") || ""); + const asName = el.getAttribute("as") || "item"; + const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); + const kids = walkChildren(el, host); + const listSrc = el.getAttribute("list") || ""; + return (vals, ctx, key) => { + let list = listGet(vals); + if (!Array.isArray(list)) { + if (!ctx?.__streamingNow) { + if (list !== void 0 && list !== null) { + warnUnresolved( + ctx, + 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" + ); + } + list = []; + } else { + list = hintN > 0 ? Array(hintN).fill(void 0) : []; + } + } + return h( + getReact().Fragment, + { key }, + list.map((item, i) => { + const sub = { ...vals, [asName]: item, $index: i }; + return h( + getReact().Fragment, + { key: i }, + kids.map((b, j) => b(sub, ctx, j)) + ); + }) + ); + }; + } + function walkIf(el, host) { + const valGet = compileAttr(el.getAttribute("value") || ""); + const hintRaw = el.getAttribute("hint-placeholder-val"); + const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + let v = valGet(vals); + if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); + return v ? h( + getReact().Fragment, + { key }, + kids.map((b, j) => b(vals, ctx, j)) + ) : null; + }; + } + function walkComponent(el, host) { + const name = el.getAttribute("name") || el.getAttribute("component") || ""; + el.removeAttribute("name"); + el.removeAttribute("component"); + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const { propGetters, hintSize } = collectProps(el, "dc-import", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key, + __hintSize: hintSize, + __tplId: tplId, + __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 + }; + for (const [k, g] of propGetters) { + const v = g(vals); + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return h(host.component(name), props); + }; + } + function walkXImport(el, host) { + const globalNameGet = compileAttr( + el.getAttribute("component-from-global-scope") || "" + ); + const exportNameGet = compileAttr( + el.getAttribute("component") || el.getAttribute("name") || "" + ); + const fromRaw = el.getAttribute("from") || (el.getAttribute("component-from-global-scope") ? "" : el.getAttribute("src") || el.getAttribute("import") || ""); + const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; + const url = urls.length ? urls[urls.length - 1] : ""; + const kindOf = (u) => /\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js"; + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const wrap = tplId != null || styleGet != null; + const { propGetters, hintSize } = collectProps(el, "x-import", host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const deckKeyed = hasContent && isDeckMountTag(el) ? walkDeckChildren(el, host) : null; + const kids = deckKeyed ? deckKeyed.kids : hasContent ? walkChildren(el, host) : []; + const kidKeys = deckKeyed?.keys ?? null; + const urlBindable = fromRaw.includes("{{"); + if (urls.length && !urlBindable) { + let prev; + for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); + } + const evalName = (g, vals) => { + const v = g(vals); + const s = v == null ? "" : String(v); + return s.includes("{{") ? "" : s; + }; + return (vals, ctx, key) => { + const globalName = evalName(globalNameGet, vals); + const name = globalName || evalName(exportNameGet, vals); + const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); + const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; + const wrapper = wrap ? { + key, + className: "sc-host-x", + "data-dc-tpl": tplId, + style: hostStyle || { display: "contents" } + } : null; + if (!C) { + const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + const props = wrapper ? {} : { key }; + let unresolvedHole = false; + for (const [k, g] of propGetters) { + if (k === "component" || k === "componentFromGlobalScope" || k === "from") { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (unresolvedHole && ctx?.__htmlStreamingNow) { + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error: null + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + if (kids.length) { + props.children = renderDeckKids(kids, kidKeys, vals, ctx); + } + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function contentKey(el) { + const clone = el.cloneNode(true); + for (const d of clone.querySelectorAll("*")) { + while (d.attributes.length) d.removeAttribute(d.attributes[0].name); + } + const s = clone.innerHTML; + let h2 = 5381; + for (let i = 0; i < s.length; i++) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0; + return s.length + "." + (h2 >>> 0).toString(36); + } + var NEVER_CONTENT_KEYED = new Set( + "script style textarea option title select canvas iframe video audio".split( + " " + ) + ); + var NOT_INLINE_SELECTOR = ":not(" + [...INLINE_TEXT_TAGS].join(",") + ")"; + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const inlineOnly = el.childNodes.length > 0 && !NEVER_CONTENT_KEYED.has(realTag) && el.querySelector(NOT_INLINE_SELECTOR) === null; + const keySuffix = inlineOnly ? "|" + contentKey(el) : ""; + const { propGetters, pseudoClasses } = collectProps(el, "dom", host); + const deckKeyed = isDeckMountTag(el) ? walkDeckChildren(el, host) : null; + const kids = deckKeyed ? deckKeyed.kids : walkChildren(el, host); + const kidKeys = deckKeyed?.keys ?? null; + return (vals, ctx, key) => { + const props = { + key: key + keySuffix, + "data-dc-tpl": tplId + }; + for (const [k, g] of propGetters) { + let v = g(vals); + if (k === "style" && typeof v === "string") v = cssToObj(v); + if ((k === "value" || k === "checked") && v === void 0) { + v = k === "checked" ? false : ""; + } + props[k] = v; + } + if (pseudoClasses.length) { + props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); + } + return h(realTag, props, ...renderDeckKids(kids, kidKeys, vals, ctx)); + }; + } + + // src/logic.ts + var StreamableLogic = class { + constructor(props) { + __publicField(this, "props"); + __publicField(this, "state", {}); + /** Back-pointer to the wrapper component, installed after construction. */ + __publicField(this, "__host"); + this.props = props || {}; + } + setState(update, cb) { + this.__host && this.__host.__setLogicState(update, cb); + } + forceUpdate() { + this.__host && this.__host.forceUpdate(); + } + componentDidMount() { + } + componentDidUpdate(_prevProps) { + } + componentWillUnmount() { + } + /** The flat object the template renders against (merged over props). */ + renderVals() { + return {}; + } + }; + function evalDcLogic(src) { + //! nosemgrep: eval-and-function-constructor + const fn = new Function( + "DCLogic", + "StreamableLogic", + "React", + src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' + ); + return fn(StreamableLogic, StreamableLogic, getReact()); + } + + // src/component.ts + function shallowEqual(a, b) { + if (!b) return false; + const ak = Object.keys(a).filter((k) => k !== "children"); + const bk = Object.keys(b).filter((k) => k !== "children"); + if (ak.length !== bk.length) return false; + for (const k of ak) if (a[k] !== b[k]) return false; + return true; + } + function Placeholder({ + name, + hintSize, + streaming, + error + }) { + const [w, hgt] = (hintSize || "100%,60px").split(","); + return h( + "div", + { + className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), + style: { width: w.trim(), height: hgt && hgt.trim() }, + title: name + }, + error ? h( + "div", + { className: "sc-placeholder-error" }, + (name ? name + ": " : "") + error + ) : null + ); + } + function hintToMin(hint) { + if (!hint) return void 0; + const [w, hgt] = hint.split(","); + return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; + } + function createComponentFactory(registry, ensureFetched) { + const React = getReact(); + const AncestorContext = React.createContext([]); + class StreamableComponent extends React.Component { + constructor(props) { + super(props); + __publicField(this, "__name"); + __publicField(this, "__sub"); + __publicField(this, "__needsDidMount", false); + /** Snapshot of the registry's streaming flags taken at render time — + * builders read it off the RenderCtx (this) to pick placeholder vs + * render-nothing for unresolved values. */ + __publicField(this, "__streamingNow", false); + __publicField(this, "__htmlStreamingNow", false); + /** When a construct throws, remember the (class, registry.ver, props) + * triple so render-time reconcile doesn't re-attempt it on every parent + * re-render. A registry bump (new class, template, external module + * resolving via bumpAll) changes `ver` and breaks the memo so an + * env-dependent constructor can self-heal. */ + __publicField(this, "__failedLogic", null); + __publicField(this, "__failedUserProps", null); + __publicField(this, "__failedVer", -1); + /** Per-instance constructor error — kept here (not on the registry entry) + * so one instance's successful construct can't hide a sibling's failure, + * and a construct can never wipe an eval error `updateJs` recorded on + * `r.logicError`. */ + __publicField(this, "__ctorError", null); + __publicField(this, "logic"); + this.__name = props.__name; + this.state = { __v: 0, __err: null }; + this.__sub = () => { + if (this.state.__err) this.setState({ __err: null }); + this.forceUpdate(); + }; + this.__makeLogic(registry.get(this.__name).Logic, null); + ensureFetched(this.__name); + } + /** Error-boundary hook: a render crash anywhere in this DC's subtree + * (its own template, an x-import'd component, a child DC without its + * own deeper boundary) lands here instead of unmounting the page. */ + static getDerivedStateFromError(e) { + return { __err: e instanceof Error && e.message ? e.message : String(e) }; + } + componentDidCatch(e, info) { + console.error( + "[dc-runtime] render error in <" + this.__name + ">:", + e, + info?.componentStack || "" + ); + } + /** Instantiate the logic class (or the no-op base) and adopt `prevState` + * over its initial state — used both at mount and on hot-swap. */ + __makeLogic(Logic, prevState) { + const L = Logic || StreamableLogic; + try { + this.logic = new L(this.__userProps()); + this.__failedLogic = null; + this.__failedUserProps = null; + this.__ctorError = null; + } catch (e) { + console.error(e); + this.__failedLogic = Logic; + this.__failedUserProps = this.__userProps(); + this.__failedVer = registry.get(this.__name).ver; + this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); + this.logic = new StreamableLogic( + this.__userProps() + ); + } + this.logic.__host = this; + if (prevState) + this.logic.state = { ...this.logic.state || {}, ...prevState }; + } + /** The props the author's logic + template see — internal __-prefixed + * wiring stripped. */ + __userProps() { + const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; + return rest; + } + __setLogicState(update, cb) { + const prev = this.logic.state; + const patch = typeof update === "function" ? update(prev) : update; + this.logic.state = { ...prev, ...patch }; + this.setState((s) => ({ __v: s.__v + 1 }), cb); + } + /** Swap the logic instance when the registry's Logic class changed + * (streaming completion, hot reload). State carries over; didMount + * re-fires after the swap commits so refs exist. */ + __reconcileLogic() { + const r = registry.get(this.__name); + const Next = r.Logic; + const Cur = this.logic.constructor; + if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) { + return; + } + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + this.__makeLogic(Next, this.logic.state); + this.__needsDidMount = true; + } + componentDidMount() { + registry.get(this.__name).subs.add(this.__sub); + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } + componentDidUpdate(prevProps) { + this.logic.props = this.__userProps(); + if (this.__needsDidMount) { + if (this.state.__err || !registry.get(this.__name).tpl) return; + this.__needsDidMount = false; + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } else { + try { + this.logic.componentDidUpdate(prevProps); + } catch (e) { + console.error(e); + } + } + } + componentWillUnmount() { + registry.get(this.__name).subs.delete(this.__sub); + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + } + render() { + const r = registry.get(this.__name); + const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); + const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; + const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; + const hostBase = { + className: cls, + style: hostStyle, + "data-sc-name": this.__name, + "data-dc-tpl": this.props.__tplId + }; + const chain = Array.isArray(this.context) ? this.context : []; + if (chain.includes(this.__name)) { + const cycle = [ + ...chain.slice(chain.indexOf(this.__name)), + this.__name + ].join(" \u2192 "); + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: "circular import: " + cycle + }) + ); + } + if (this.state.__err) { + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + this.__name + ": " + this.state.__err + ), + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: this.state.__err + }) + ); + } + this.__reconcileLogic(); + if (!r.tpl) { + return h( + "div", + hostBase, + h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) + ); + } + const userProps = this.__userProps(); + this.logic.props = userProps; + let vals = userProps; + let renderErr = r.logicError || this.__ctorError; + try { + vals = { ...userProps, ...this.logic.renderVals() || {} }; + } catch (e) { + console.error(e); + renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); + } + this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); + this.__htmlStreamingNow = !!r.htmlStreaming; + return h( + "div", + { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, + renderErr && h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + renderErr + ), + h( + AncestorContext.Provider, + { value: [...chain, this.__name] }, + r.tpl(vals, this) + ) + ); + } + } + __publicField(StreamableComponent, "contextType", AncestorContext); + const named = /* @__PURE__ */ new Map(); + function getDC(name) { + const hit = named.get(name); + if (hit) return hit; + function Dispatcher(p) { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + registry.get(name).subs.add(sub); + return () => { + registry.get(name).subs.delete(sub); + }; + }, []); + ensureFetched(name); + return h(StreamableComponent, { ...p, __name: name }); + } + Dispatcher.displayName = name; + named.set(name, Dispatcher); + return Dispatcher; + } + return { + getDC, + StreamableComponent + }; + } + + // src/bundled.ts + function bundledBlob(url) { + const blobs = window.__resourceBlobs; + const b = blobs ? blobs[url.split("#")[0]] : void 0; + return b instanceof Blob ? b : null; + } + + // src/cdn.ts + var REACT_URL = "https://unpkg.com/react@18.3.1/umd/react.production.min.js"; + var REACT_SRI = "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z"; + var REACT_DOM_URL = "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"; + var REACT_DOM_SRI = "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1"; + var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"; + var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"; + function cdnScriptFor(url, sri) { + const res = window.__resources; + const v = res ? res[url] : void 0; + return typeof v === "string" && v ? { src: v } : { src: url, integrity: sri }; + } + + // src/external.ts + var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); + function isRenderableType(g) { + if (typeof g === "function") return !isElementClass(g); + return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; + } + function resolveDottedPath(root, name) { + let cur = root; + for (const seg of name.split(".")) { + if (cur == null) return void 0; + cur = cur[seg]; + } + return cur; + } + var GLOBAL_POLL_INTERVAL_MS = 50; + var GLOBAL_POLL_TIMEOUT_MS = 3e4; + function createExternalModules(onResolved) { + const cache = /* @__PURE__ */ new Map(); + let babelLoading = null; + const reportedMissing = /* @__PURE__ */ new Map(); + const polling = /* @__PURE__ */ new Set(); + function ensureBabel() { + if (window.Babel) return Promise.resolve(); + if (babelLoading) return babelLoading; + const babel = cdnScriptFor(BABEL_URL, BABEL_SRI); + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = babel.src; + if (babel.integrity) { + s.integrity = babel.integrity; + s.crossOrigin = "anonymous"; + } + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + const pending = /* @__PURE__ */ new Map(); + function load(kind, url, after) { + const existing = pending.get(url); + if (existing) return existing; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = Promise.all([ + kind === "jsx" ? ensureBabel() : Promise.resolve(), + after ?? Promise.resolve() + ]); + const p = ready.then(() => { + const pre = bundledBlob(url); + if (pre) return pre.text(); + return fetch(url).then((r) => { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.text(); + }); + }).then((src) => { + const code = kind === "jsx" ? window.Babel.transform(src, { + filename: url, + presets: ["react", "typescript"] + }).code : src; + const module = { exports: {} }; + const before = new Set(Object.keys(window)); + //! nosemgrep: eval-and-function-constructor + new Function("React", "module", "exports", "require", code)( + getReact(), + module, + module.exports, + () => ({}) + ); + const globals = {}; + for (const k of Object.keys(window)) { + if (!before.has(k) && typeof window[k] === "function") { + globals[k] = window[k]; + } + } + cache.set(url, { mod: module.exports, globals }); + console.info( + "[dc-runtime] x-import: loaded", + url, + "\u2014 exports:", + Object.keys(module.exports), + "window globals:", + Object.keys(globals) + ); + onResolved(); + }).catch((e) => { + cache.set(url, { + mod: {}, + globals: {}, + error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) + }); + console.error( + "[dc-runtime] x-import: FAILED to load", + url, + "(" + kind + ")", + e + ); + onResolved(); + }); + pending.set(url, p); + return p; + } + function resolve2(url, name) { + const entry = cache.get(url); + if (!entry) return null; + const { mod, globals } = entry; + const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; + if (typeof C === "function") return C; + const key = url + "\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set( + key, + entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" + ); + console.error( + "[dc-runtime] x-import: module", + url, + "loaded but has no component named", + JSON.stringify(name), + "\u2014 available exports:", + Object.keys(mod), + "window globals:", + Object.keys(globals), + ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." + ); + } + return null; + } + function waitForGlobal(name) { + if (polling.has(name)) return; + polling.add(name); + const started = Date.now(); + const isCE = isCustomElementName(name); + const tick = () => { + const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); + if (found) { + polling.delete(name); + onResolved(); + return; + } + if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { + console.warn( + "[dc-runtime] x-import: global", + JSON.stringify(name), + "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" + ); + return; + } + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + }; + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + } + function resolveGlobal(url, name) { + const isCE = isCustomElementName(name); + if (!url) { + if (isCE) { + if (customElements.get(name)) return name; + waitForGlobal(name); + return null; + } + const g2 = resolveDottedPath(window, name); + if (isRenderableType(g2)) return g2; + waitForGlobal(name); + return null; + } + const entry = cache.get(url); + if (!entry) return null; + if (isCE && customElements.get(name)) return name; + const g = entry.globals[name] ?? resolveDottedPath(window, name); + if (isRenderableType(g)) return g; + if (name.includes(".")) return null; + const key = url + "\0global\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set(key, null); + if (isCE && !customElements.get(name)) { + console.warn( + "[dc-runtime] x-import:", + url, + "loaded but no custom element", + JSON.stringify(name), + "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." + ); + } + } + return name; + } + function getError(url, name) { + const entry = cache.get(url); + if (entry?.error) return entry.error; + return reportedMissing.get(url + "\0" + name) || null; + } + return { load, resolve: resolve2, resolveGlobal, getError }; + } + function isElementClass(g) { + try { + return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; + } catch { + return false; + } + } + + // src/atomics.ts + var ATOMIC_CSS = ( + // layout + ".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}" + ); + + // src/helmet.ts + var DESIGN_DOC_MODE_RE = /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; + var CANVAS_BG_LIGHT = "#f0eee6"; + var CANVAS_BG_DARK = "#2e2c26"; + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + let designDocMode = null; + let canvasStyleEl = null; + let appTheme = "light"; + try { + const ds = doc.documentElement.dataset.theme; + appTheme = ds === "dark" || ds === "light" ? ds : new URLSearchParams(doc.defaultView?.location.search ?? "").get( + "theme" + ) === "dark" ? "dark" : "light"; + } catch { + } + function applyCanvasBg() { + if (!canvasStyleEl) return; + const bg = appTheme === "dark" ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; + canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; + } + function postDesignMode(mode) { + if (window.parent === window) return; + try { + window.parent.postMessage({ type: "__dc_design_mode", mode }, "*"); + } catch { + } + } + function setDesignDocMode(mode) { + if (mode === designDocMode) return; + designDocMode = mode; + postDesignMode(mode); + if (mode === "canvas") { + doc.documentElement.setAttribute("data-dc-canvas", ""); + canvasStyleEl = doc.createElement("style"); + canvasStyleEl.setAttribute("data-dc-canvas", ""); + applyCanvasBg(); + doc.head.appendChild(canvasStyleEl); + } else { + doc.documentElement.removeAttribute("data-dc-canvas"); + canvasStyleEl?.remove(); + canvasStyleEl = null; + } + } + window.addEventListener("message", (e) => { + const type = e.data && e.data.type; + if (type === "__dc_theme") { + const t = e.data.theme; + if (t === "light" || t === "dark") { + appTheme = t; + applyCanvasBg(); + } + return; + } + if (!designDocMode || type !== "__dc_probe") return; + postDesignMode(designDocMode); + }); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) { + mounted.add("__dc-atomics"); + const el = doc.createElement("style"); + el.id = "__dc-atomics"; + el.textContent = ATOMIC_CSS; + doc.head.appendChild(el); + } + return (_vals, ctx) => { + const name = ctx && ctx.__name || ""; + const streaming = !!(name && isStreaming(name)); + for (let i = 0; i < raw.length; i++) { + const child = raw[i]; + const tag = child.tagName; + const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; + if (tag === "SCRIPT") { + if (mayBePartial) continue; + const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); + if (mounted.has(key)) continue; + mounted.add(key); + const el = doc.createElement("script"); + for (const { name: an, value } of [...child.attributes]) + el.setAttribute(an, value); + if (child.textContent) el.textContent = child.textContent; + doc.head.appendChild(el); + } else if (tag === "LINK" || tag === "META") { + if (mayBePartial) continue; + const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); + if (mounted.has(key)) continue; + mounted.add(key); + if (tag === "LINK") { + const rel = (child.getAttribute("rel") || "").toLowerCase().split(/\s+/); + const href = (child.getAttribute("href") || "").trim(); + const res = window.__resources; + const pre = res && rel.includes("stylesheet") && !rel.includes("alternate") ? res[href] : void 0; + const blob = typeof pre === "string" && pre ? bundledBlob(pre) : null; + if (blob) { + const el = doc.createElement("style"); + if (child.hasAttribute("disabled")) { + el.setAttribute("media", "not all"); + } else if (child.getAttribute("media")) { + el.setAttribute("media", child.getAttribute("media")); + } + if (child.getAttribute("title")) + el.setAttribute("title", child.getAttribute("title")); + void blob.text().then((css) => { + el.textContent = css; + }); + doc.head.appendChild(el); + continue; + } + } + doc.head.appendChild(child.cloneNode(true)); + } else { + const key = name + "|" + i; + let el = live.get(key); + if (!el || el.tagName !== tag) { + if (el) el.remove(); + el = doc.createElement(tag.toLowerCase()); + live.set(key, el); + doc.head.appendChild(el); + } + for (const { name: an, value } of [...child.attributes]) { + if (el.getAttribute(an) !== value) el.setAttribute(an, value); + } + if (el.textContent !== child.textContent) + el.textContent = child.textContent; + } + } + return null; + }; + } + return { compile, setDesignDocMode }; + } + + // src/pseudo.ts + function scanUnquotedUrl(css, i) { + if (css[i] !== "u" && css[i] !== "U" || css.slice(i, i + 4).toLowerCase() !== "url(" || /[a-z0-9_-]/i.test(css[i - 1] ?? "")) { + return -1; + } + let j = i + 4; + while (j < css.length && /\s/.test(css[j])) j++; + if (css[j] === '"' || css[j] === "'") return -1; + while (j < css.length && css[j] !== ")") { + if (css[j] === "\\") j++; + j++; + } + return j < css.length ? j + 1 : css.length; + } + function stripComments(css) { + let out = ""; + let quote = ""; + for (let i = 0; i < css.length; i++) { + const c = css[i]; + if (quote) { + if (c === "\\") { + out += c + (css[i + 1] ?? ""); + i++; + continue; + } + if (c === quote) quote = ""; + out += c; + } else if (c === "'" || c === '"') { + quote = c; + out += c; + } else if (c === "/" && css[i + 1] === "*") { + const end = css.indexOf("*/", i + 2); + i = end === -1 ? css.length : end + 1; + out += " "; + } else { + const end = scanUnquotedUrl(css, i); + if (end === -1) out += c; + else { + out += css.slice(i, end); + i = end - 1; + } + } + } + return out; + } + function importantify(css) { + css = stripComments(css); + const decls = []; + let start = 0; + let depth = 0; + let quote = ""; + for (let i = 0; i < css.length; i++) { + const c = css[i]; + if (quote) { + if (c === "\\") i++; + else if (c === quote) quote = ""; + } else if (c === "'" || c === '"') quote = c; + else if (c === "(") depth++; + else if (c === ")") depth = Math.max(0, depth - 1); + else if (c === ";" && depth === 0) { + decls.push(css.slice(start, i)); + start = i + 1; + } else { + const end = scanUnquotedUrl(css, i); + if (end !== -1) i = end - 1; + } + } + decls.push(css.slice(start)); + return decls.map((d) => d.trim()).filter(Boolean).map((d) => /!\s*important$/i.test(d) ? d : d + " !important").join(";"); + } + function createPseudoSheet(doc) { + let el = null; + const cache = /* @__PURE__ */ new Map(); + let n = 0; + return (pseudo, css) => { + const k = pseudo + "|" + css; + const hit = cache.get(k); + if (hit) return hit; + if (!el) { + el = doc.createElement("style"); + doc.head.appendChild(el); + } + const cls = "scp" + (n++).toString(36); + const isPseudoElement = pseudo === "before" || pseudo === "after"; + const sel = isPseudoElement ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule( + sel + "{" + (isPseudoElement ? css : importantify(css)) + "}", + el.sheet.cssRules.length + ); + cache.set(k, cls); + return cls; + }; + } + + // src/registry.ts + function createRegistry() { + const entries = /* @__PURE__ */ Object.create(null); + function get(name) { + return entries[name] || (entries[name] = { + html: "", + tpl: null, + Logic: null, + jsStreaming: false, + htmlStreaming: false, + ver: 0, + subs: /* @__PURE__ */ new Set(), + fetched: false + }); + } + function bump(name) { + const r = get(name); + r.ver++; + for (const fn of r.subs) fn(); + } + return { + entries, + get, + bump, + bumpAll() { + for (const n in entries) bump(n); + } + }; + } + + // src/runtime.ts + var COMPONENT_DIR = "."; + function createRuntime(doc = document) { + const registry = createRegistry(); + const pseudoClass = createPseudoSheet(doc); + const helmet = createHelmetManager( + doc, + (name) => registry.get(name).htmlStreaming + ); + const external = createExternalModules(() => registry.bumpAll()); + const factory = createComponentFactory(registry, ensureFetched); + const host = { + component: (name) => factory.getDC(name), + placeholder: (props) => h(Placeholder, props), + helmet: (node) => helmet.compile(node), + loadExternal: (kind, url, after) => external.load(kind, url, after), + resolveExternal: (url, name) => external.resolve(url, name), + resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), + resolveExternalError: (url, name) => external.getError(url, name), + pseudoClass + }; + function ensureFetched(name) { + const r = registry.get(name); + if (r.fetched) return; + r.fetched = true; + const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html"; + const res = window.__resources; + const pre = res ? res[url] : void 0; + const target = typeof pre === "string" && pre ? pre : url; + const blob = bundledBlob(target); + (blob ? blob.text() : fetch(target).then((res2) => { + if (!res2.ok) { + console.error( + '[dc-runtime] sibling fetch for "' + name + '" failed:', + url, + "returned", + res2.status, + "\u2014 the reference renders as an empty placeholder." + ); + return ""; + } + return res2.text(); + })).then((t) => { + if (!t) return; + const parsed = parseDcText(t); + if (!parsed) { + console.error( + '[dc-runtime] sibling fetch for "' + name + '":', + url, + "has no block \u2014 not a Design Component." + ); + return; + } + if (parsed.props) r.propsMeta = parsed.props; + if (parsed.preview) r.preview = parsed.preview; + if (parsed.template && !r.html) updateHtml(name, parsed.template); + if (parsed.js && !r.Logic) updateJs(name, parsed.js); + }).catch( + (e) => console.error( + '[dc-runtime] sibling fetch for "' + name + '" threw:', + url, + e + ) + ); + } + let rootName = null; + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + if (name === rootName) { + const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; + if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); + } + try { + r.tpl = compileTemplate(html, host); + } catch (e) { + console.error("[dc-runtime] template compile FAILED for", name, e); + } + registry.bump(name); + } + function updateJs(name, src) { + const r = registry.get(name); + const seq = r.jsSeq = (r.jsSeq || 0) + 1; + try { + const Cls = evalDcLogic(src); + if (r.jsSeq !== seq) return; + if (typeof Cls !== "function") { + r.logicError = name + ".dc.html: + + + + + + + + + + +

+
3去掉跨主机分屏 · 新建分屏交互 · 会话标题 · 首页与关闭/最小化
+
+
跨主机分屏已移除
分屏只发生在同一台主机内,层级回到「主机 → 会话 → window → pane」四层。要同时看两台机器,用轨道或侧边栏一键切换。第 2 轮的 2b 稿已作废删除。
+
新建分屏
不问方向(竖分/横分本就是两个按钮),只列这台主机的 tmux 会话,末尾追加「原生窗口」,选完立即分屏。见 3b
+
tmux 会话标题
会话行第二行显示当前 window title,window 行显示各自 title(仍是默认 zsh 时退回 cwd),不新增层级。见 3a
+
首页入口
默认遮罩态:288pt 面板盖住轨道(非并排,故无两套入口),点终端即收起;点 Pin 才钉在左侧、终端变窄。栏内仍是上「活动会话」下「TMUX」两段。见 3a
+
关闭 / 最小化
沿用 macOS 红黄圆点:黄点最小化不确认,红点关闭才确认;tmux 默认仅断开,原生窗口明说进程会结束。见 3c
+
对你方案的两点修改
① 最小化后落地页卡片置顶并标「最小化中」,配缩回动画,否则用户不知道东西去哪了;② 关闭的确认按主机能力分成两套文案,tmux 不默认 kill(只有次要按钮才是 kill)。见 3d
+
+
+
+
3a侧边栏:遮罩态与 Pin 态 折叠与展开只是同一条栏的两个状态,元素不增不减:折叠 56pt 收成图标(搜索 / 首页 / 主机 / TMUX / 设置 / 展开键),展开 288pt 显示完整标题。左上角 是 App logo,不可点;开合只有一枚按钮——折叠态是轨道底部的 »,展开态是标题右侧的 «,方向相反不歧义。左=默认遮罩态:288pt 面板盖住 56pt 轨道(不是并排,因此没有两套入口),右侧压一层遮罩,点终端任意处即收起。右=点 Pin 之后:面板钉在左侧、遮罩消失、终端变窄并只 resize 一次;再点一次 Pin 回到遮罩态。栏内布局沿用轨道的分区:上半「活动会话」,下半「TMUX(当前主机的 window 列表)」。
+
+
+
+
+
+
+
+
+
ms
u2
nh
+
+
+
TMUX
+
0
1
2
+
+
+
+
+
+
+
+
+
+
terminalx git:(main) git fetch --all --prune
Fetching origin … done
terminalx git:(main) swift build -c release --arch arm64
[142/142] Compiling TXCore
[ 8/8 ] Linking TerminalX
✓ 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
DCS passthrough detected
backoff caps at 30s
mosh three-phase resume
◇ 50 tests passed after 0.412s
 
terminalx git:(main) xcodegen generate --spec project.yml
✅ Created TerminalX.xcodeproj
 
terminalx git:(main) git status --short
M apps/TerminalX/iOS/RailSidebar.swift
?? docs/ui/sidebar-states.md
 
terminalx git:(main) git fetch --all --prune
 
terminalx git:(main)
+
+
+
+
0.1 · 日志 · log stream --style compact168×17
+
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
12:03:12 terminalx: ghostty frame 16.7ms
+
+
+
+
tsnet
+
+
mosh41ms
+
+
tmuxdev
+
+
+
+
+ + mac-studio + 运行测试 · ~/src/terminalx +
+
遮罩态 · 点终端任意处收起⌘\ 收起Pin 钉在左侧⌘⇧H 回首页 切换会话
+
+
+
+
+
+
+ terminalX +
+
+
+
+
+
搜索主机 · 会话
+
首页⌘⇧H
+
活动会话3
+
+
ms
mac-studio · dev
运行测试 · 3 windows
41ms
+
u2
ubuntu-cn2 · build
集成测试 · 1 window
186ms
+
nh
nas-home
原生窗口 · ~
重连中
+
连接新主机…
+
+
+
TMUX · mac-studio · dev
+
+
0
运行测试
~/src/terminalx · 2 panes
+
1
日志
~/src/terminalx
+
2
nvim Theme.swift
packages/TXCore
+
新建 window ⌘T
+
+ +
Tailscale 已连接
ipad-pro · 100.92.14.7
+
+
+
+
+
+
+
+
+ terminalX +
+
+
+
+
+
搜索主机 · 会话
+
首页⌘⇧H
+
活动会话3
+
+
ms
mac-studio · dev
运行测试 · 3 windows
41ms
+
u2
ubuntu-cn2 · build
集成测试 · 1 window
186ms
+
nh
nas-home
原生窗口 · ~
重连中
+
连接新主机…
+
+
+
TMUX · mac-studio · dev
+
+
0
运行测试
~/src/terminalx · 2 panes
+
1
日志
~/src/terminalx
+
2
nvim Theme.swift
packages/TXCore
+
新建 window ⌘T
+
+ +
Tailscale 已连接
ipad-pro · 100.92.14.7
+
+
+
+
+
terminalx git:(main) git fetch --all --prune
Fetching origin … done
terminalx git:(main) swift build -c release --arch arm64
[142/142] Compiling TXCore
[ 8/8 ] Linking TerminalX
✓ 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
DCS passthrough detected
backoff caps at 30s
mosh three-phase resume
◇ 50 tests passed after 0.412s
 
terminalx git:(main) xcodegen generate --spec project.yml
✅ Created TerminalX.xcodeproj
 
terminalx git:(main) git status --short
M apps/TerminalX/iOS/RailSidebar.swift
?? docs/ui/sidebar-states.md
 
terminalx git:(main) git fetch --all --prune
 
terminalx git:(main)
+
+
+
+
0.1 · 日志 · log stream --style compact132×17
+
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
12:03:12 terminalx: ghostty frame 16.7ms
+
+
+
+
tsnet
+
+
mosh41ms
+
+
tmuxdev
+
+
+
+
+ + mac-studio + 运行测试 · ~/src/terminalx +
+
Pin 态 · 终端变窄,仅在钉住/解除时各 resize 一次Pin 解除钉住⌘\ 收起为轨道⌘⇧H 回首页
+
+
+
+
+
+
+
+
3b新建分屏:只问"分到哪个会话" 竖分 / 横分本来就是两个不同的按钮(⌘D / ⌘⇧D,触控是轨道 + 的两枚小图标),所以菜单不再问方向。菜单只列出这台主机上的 tmux 会话(含当前会话、可接管的 detached 会话、新建会话),最后追加一项原生窗口。选完立即分屏——此稿右侧就是选中「dev(当前会话)」后竖分的结果。
+
+
+
+
+
+
+
+
ms
u2
nh
+
+
+
TMUX
+
0
1
2
+
+
+
+
+
+
+
+
+
+
terminalx git:(main) swift package resolve
Fetching TXCore … resolved
Fetching swift-log 1.6.1 … resolved
 
terminalx git:(main) xcodebuild -showsdks | head -2
iOS SDKs: iOS 26.2 (iphoneos26.2)
macOS SDKs: macOS 26.2 (macosx26.2)
 
terminalx git:(main) git fetch --all --prune
Fetching origin … done
terminalx git:(main) swiftformat --lint Sources
✓ 0 issues in 84 files
 
terminalx git:(main) xcrun simctl list devices booted
-- iOS 26.2 --
iPad Pro 13-inch (M4) (Booted)
 
terminalx git:(main) swift package resolve
Fetching TXCore … resolved
Fetching swift-log 1.6.1 … resolved
 
terminalx git:(main) xcodebuild -showsdks | head -2
iOS SDKs: iOS 26.2 (iphoneos26.2)
macOS SDKs: macOS 26.2 (macosx26.2)
terminalx git:(main) git fetch --all --prune
Fetching origin … done
terminalx git:(main) swift build -c release --arch arm64
[142/142] Compiling TXCore
[ 8/8 ] Linking TerminalX
✓ 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
DCS passthrough detected
backoff caps at 30s
mosh three-phase resume
◇ 50 tests passed after 0.412s
 
terminalx git:(main) xcodegen generate --spec project.yml
✅ Created TerminalX.xcodeproj
 
terminalx git:(main) git status --short
M apps/TerminalX/iOS/RailSidebar.swift
M apps/TerminalX/iOS/SessionRouter.swift
?? docs/ui/home-and-close.md
 
 
terminalx git:(main)
+
+
+
+
0.1 · 日志 · log stream --style compact
+
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
12:06:41 terminalx: tmux %window-renamed 0
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
12:06:41 terminalx: tmux %window-renamed 0
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
12:06:41 terminalx: tmux %window-renamed 0
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
12:06:41 terminalx: tmux %window-renamed 0
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
+
+
+
+
tsnet
+
+
mosh41ms
+
+
tmuxdev
+
+
+
+
+ + mac-studio + 运行测试 · ~/src/terminalx +
+
竖分屏 · 选择分到哪个会话⌘D 竖分⌘⇧D 横分↑↓ 移动 立即分屏
+
+
+
+
竖分屏到…mac-studio
+
+
tmux 会话
+
+ +
dev
运行测试 · 3 windows · 4 panes
+ + 当前 +
+
+ +
scratch
zsh · 1 window · detached
+ + 3 天前 +
+
+ +
deploy
部署脚本 · 2 windows · detached
+ + 昨天 +
+
新建 tmux 会话…
+
+
原生窗口
不经 tmux · 关 App 即结束
+
+
+
+
+
+
+
+
3c关闭 / 最小化:红黄两枚圆点 标题胶囊左侧沿用 macOS 语汇: = 关闭, = 最小化,悬停/触摸才显出符号。黄点不确认,直接回首页、会话继续跑(卡片置顶标「最小化中」)。红点弹一次确认:tmux 默认仅断开,结束会话是次要按钮;原生窗口的文案换成"前台进程会结束"。
+
+
+
+
+
+
+
+
ms
u2
nh
+
+
+
TMUX
+
0
1
2
+
+
+
+
+
+
+
+
+
+
terminalx git:(main) git fetch --all --prune
Fetching origin … done
terminalx git:(main) swift build -c release --arch arm64
[142/142] Compiling TXCore
[ 8/8 ] Linking TerminalX
✓ 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
DCS passthrough detected
backoff caps at 30s
mosh three-phase resume
◇ 50 tests passed after 0.412s
 
terminalx git:(main) xcodegen generate --spec project.yml
✅ Created TerminalX.xcodeproj
 
terminalx git:(main) git status --short
M apps/TerminalX/iOS/RailSidebar.swift
M apps/TerminalX/iOS/SessionRouter.swift
?? docs/ui/home-and-close.md
 
 
terminalx git:(main)
+
+
+
+
0.1 · 日志 · log stream --style compact80×17
+
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
12:06:41 terminalx: tmux %window-renamed 0
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:20 terminalx: window title → "运行测试"
+
+
+
+
tsnet
+
+
mosh41ms
+
+
tmuxdev
+
+
+
+
+ + mac-studio + 运行测试 · ~/src/terminalx +
+
关闭当前会话 · 等待确认⌘W 关闭⌘M 最小化 仅断开esc 取消
+
+
+
+
+
关闭 mac-studio · dev?
+
tmux 会话(3 windows / 4 panes)。仅断开后服务端继续跑,下次原样接回。
+
+
+ 结束会话 +
+ 取消 + 仅断开 ⏎ +
+
+
+
+
对照原生窗口(无 tmux)
+
+
关闭 nas-home · 窗口 1?
+
没有 tmux 兜底,前台进程会随之结束。想留着就点黄点最小化。
+
+
+ 取消 + 关闭窗口 +
+
+
+
+
+
+
+
3d首页 ⇄ 终端:四条路径与返回规则 把两屏之间所有出入口画在一起。进入只有两种(恢复已有会话 / 新建会话),离开有三种(首页键、最小化、关闭),三者都落到同一张落地页——差别只在会话的去向:继续跑 / 后台 detach / 结束。换会话不必回首页,侧边栏里点一下即可。
+
+
+
+

首页(主机管理) ⇄ 终端主界面

+
App 冷启动永远落在首页;终端是"进入某个会话"的结果,不是独立 tab。
+
+
+
+
首页 · 主机管理(落地页)
+
活动会话(最上排)
三张缩略卡:前台运行 / 后台 detach / 重连中,点卡片即进终端
全部主机(下排)
点主机 → 若有会话则列出可接管的,否则直接新建
从最小化回来
卡片右上角标「最小化中」,点一下瞬间恢复,光标位置不变
冷启动 / ⌘⇧H
都停在这里,不自动进任何会话
+
+
+
进入终端
+
+ +
恢复已有会话
点活动会话卡 / 主机下的会话行 → 直接进终端,恢复到上一个焦点 pane
+
+
+ +
新建会话
点「新建连接」或主机上的 + → 建好即进终端,侧边栏同步出现该会话
+
+
离开终端
+
+ +
首页键(侧边栏第一行)
会话完全不动,随时切回;用于"去看看别的主机"
+
+
+ +
最小化(黄点)
等同首页键,但明确表达"我待会儿还回来",卡片置顶并标记
+
+
+ +
关闭(红点)· 二次确认
tmux 默认 detach(后台跑);原生窗口会结束进程,文案单独写
+
+
+
+
终端主界面 · 沉浸轨道
+
侧边栏第一行:首页
折叠态显示为图标,展开态显示完整标题;同一条栏、同一个位置,⌘⇧H 随时可用
标题胶囊左侧:红点 / 黄点
红点关闭需二次确认后回首页;黄点最小化不确认,直接回首页、会话继续
活动会话就在侧边栏
切会话不必回首页,回首页是"换任务"而不是"切窗口"
Pin 后常驻
侧边栏钉在左侧时,切会话与看终端可同时进行,无需任何中间层
+
+
+
+
+ +
建议一:"最小化"在 iPad 上没有 Dock 承接,容易被理解成"藏起来找不到了"。仍保留这枚按钮,但把落点讲清楚——回到首页后该卡片置顶并标记「最小化中」,动画从终端缩回卡片,用户就知道东西去哪了。
+
+
+ +
建议二:"关闭"对 tmux 与原生窗口的后果完全不同,别用同一句确认。tmux 默认 detach 而非 kill,把 kill 放在第二项;原生窗口则明说"进程会结束"。另外给最小化配下滑手势、关闭配 ⌘W,二次确认支持 ⏎ 直接选推荐项。
+
+
+
+
+
+
+
+ +
+
2沉浸轨道深化 · 无 tmux 主机 / 竖屏降级(跨主机分屏、浮层侧边栏、会话总览三稿已作废)
+
+
展开策略(已更新)
原「浮层压在轨道旁」的方案已作废(会出现两套入口)。现行方案:遮罩态面板盖住轨道,Pin 后才推挤终端。见第 3 轮 3a
+
分屏模型(已更新)
跨主机分屏已取消,分屏只在同一台主机内:tmux pane 由服务端持有、断线保持;原生窗口不经 tmux。新建交互见第 3 轮 3b
+
无 tmux 主机
轨道隐藏 TMUX 段,换成客户端「窗口」段;胶囊第三格写"无 tmux"。可随时新建与 tmux 无关的窗口。见 2c
+
竖屏降级
iPad 竖屏:轨道 48pt,pane 改上下堆叠,最多两层,多余折成段控。iPhone:轨道下沉为底部主机条,单 pane + 轻扫。见 2d
+
会话总览(已取消)
「⌘O 缩放成卡片」的总览层已去掉,轨道底部相应按钮也移除。切会话直接用侧边栏,换任务用「首页」。跳转关系见第 3 轮 3d
+
保持不变
状态徽标顺序(tsnet → mosh RTT → tmux/窗口)、Nocturne chrome + Catppuccin 终端、硬件键盘时附加行自动收起——五张新稿全部沿用。
+
+
+
+
2c主机没有 tmux:轨道收起 TMUX 段 轨道下半段不再显示 tmux window 数字条,改为客户端「窗口」段(每个窗口 = 一条独立 SSH channel / mosh 会话),可随意新建。状态胶囊第三格显示「无 tmux」而不是报错。若探测到装了 tmux 但没会话,才在终端顶部给一条可忽略的行内提示——不弹窗、不强推。
+
+
+
+
+
+
+
+
ms
u2
nh
+
+
+
窗口
1
2
+
+
+
+
+
+
+
+
+
+
nas:~$ zfs get compressratio tank
tank compressratio 1.34x
 
nas:~$ docker stats --no-stream | head -3
NAME CPU % MEM USAGE
plex 4.10% 1.21GiB
syncthing 0.80% 312MiB
 
nas:~$ zfs get compressratio tank
tank compressratio 1.34x
nas:~$ uname -sr
Linux 6.12.9-truenas
 
nas:~$ docker ps --format 'table {{.Names}}'
NAMES STATUS
plex Up 6 days
syncthing Up 6 days
caddy Up 12 hours
postgres Up 6 days (healthy)
 
nas:~$ zpool status -x
all pools are healthy
 
nas:~$ df -h /tank
Filesystem Size Used Avail Use%
tank 18T 11T 7.0T 62%
 
nas:~$ zfs list -t snapshot | tail -3
tank/photos@auto-2026-07-24 1.2G
tank/docs@auto-2026-07-24 84M
tank/vm@auto-2026-07-24 3.4G
 
nas:~$ tail -4 /var/log/syncthing.log
folder "photos" sync complete
folder "docs" idle
puller: 0 conflicts
scanner: 12,418 files ok
 
nas:~$ smbstatus --shares | head -3
Service pid Machine
media 4412 100.92.14.7
 
nas:~$ uptime
12:08:03 up 41 days, 3:22, 1 user
 
nas:~$ uname -sr
Linux 6.12.9-truenas
 
 
nas:~$
+
+
+
+ +
这台主机上没有 tmux。窗口由 terminalX 客户端维持,mosh 负责断线续传;但关掉 App 后不会像 tmux 那样在服务端继续跑
+
安装并启用 tmux
+ +
+
+
tsnet
+
+
mosh88ms
+
+
无 tmux · 客户端窗口
+
+
nas-home~ · 窗口 1 / 2 · 132×46
+
客户端窗口模式 · 每个窗口一条独立通道⌘T 新建窗口⌘1…9 切窗口⌘W 关闭窗口⌘K 命令面板
+
+
+
+
+
+
+
2d竖屏 iPad 与 iPhone 竖屏宽度不够并排,规则改为:轨道保留但降到 48pt(拇指可达的左缘),pane 由左右平铺改为上下堆叠,最多两层,第三个 pane 折成顶部段控。状态胶囊压缩成一枚。iPhone 再降一级:轨道变成底部主机条,单 pane 全屏,pane 之间左右轻扫,下拉标题打开会话列表。
+
+
+
+
+
+
+
+
+
ms
+
u2
+
nh
+
+
0
+
1
+
+
+
+
+
+
terminalx git:(main) git fetch --all --prune
Fetching origin … done
terminalx git:(main) swiftformat --lint Sources
✓ 0 issues in 84 files
 
terminalx git:(main) xcodebuild -showsdks | head -2
terminalx git:(main) swift build -c release --arch arm64
[142/142] Compiling TXCore
[ 8/8 ] Linking TerminalX
✓ build complete (14.2s)
 
terminalx git:(main) swift test --package-path packages/TXCore
◇ Test run started.
↳ Suite "tmux control mode"
parses %output escaped octal
layout diff keeps pane identity
DCS passthrough detected
%window-add spawns pane tree
↳ Suite "SessionMachine"
backoff caps at 30s
mosh three-phase resume
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) xcrun simctl list devices booted
-- iOS 26.2 --
iPad Pro 13-inch (M4) (Booted)
 
terminalx git:(main) git pull --rebase
From tsnet:athom/terminalx main
Successfully rebased onto main.
 
terminalx git:(main) git status --short
M apps/TerminalX/iOS/ContentView.swift
M apps/TerminalX/iOS/Theme.swift
 
terminalx git:(main)
+
+
+
+
0.1 · log stream --style compact2/3 pane · 第 3 个已折叠
+
12:02:31 terminalx: tsnet peer refresh (7)
12:02:58 terminalx: mosh heartbeat 38ms
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:02 terminalx: ghostty frame 16.4ms
12:06:20 terminalx: tsnet peer nas-home down
12:06:41 terminalx: retry scheduled in 8s
12:07:03 terminalx: tmux %window-add 3
12:01:02 terminalx: tmux %output window 0
12:01:19 terminalx: mosh heartbeat 41ms
12:01:44 terminalx: pane 0.1 resized 60x18
12:02:03 terminalx: keychain read host-key
12:02:31 terminalx: tsnet peer refresh (7)
12:02:58 terminalx: mosh heartbeat 38ms
12:03:12 terminalx: ghostty frame 16.7ms
12:03:41 terminalx: tmux %layout-change
12:04:02 terminalx: session dev attached
12:04:31 terminalx: link degraded, rtt 210ms
12:04:33 terminalx: mosh resume ok, 0 lost
12:05:02 terminalx: scrollback trimmed 4k
12:05:44 terminalx: pasteboard bridge ok
12:06:02 terminalx: ghostty frame 16.4ms
12:06:20 terminalx: tsnet peer nas-home down
12:06:41 terminalx: retry scheduled in 8s
12:07:03 terminalx: tmux %window-add 3
+
+
+
+
mac-studiodev
+
41ms
+
+
+
esc
ctrl
tab
~
/
|
+
+
+
+
+
+
+
+
9:41 78%
+
+ +
mac-studio · dev
+
41ms
+
+
左右轻扫切 pane
+
tx git:(main) git fetch --all --prune
Fetching origin … done
tx git:(main) swiftformat --lint Sources
✓ 0 issues in 84 files
 
tx git:(main) xcodebuild -showsdks | head -2
tx git:(main) swift build -c release --arch arm64
[142/142] Compiling TXCore
[ 8/8 ] Linking TerminalX
✓ build complete (14.2s)
 
tx git:(main) swift test --package-path packages/TXCore
◇ Test run started.
↳ Suite "tmux control mode"
parses %output escaped octal
layout diff keeps pane identity
DCS passthrough detected
%window-add spawns pane tree
↳ Suite "SessionMachine"
backoff caps at 30s
mosh three-phase resume
host-key pin survives wipe
◇ 50 tests passed after 0.412s
 
tx git:(main) xcodegen generate --spec project.yml
✅ Created TerminalX.xcodeproj
 
tx git:(main) xcrun simctl list devices booted
-- iOS 26.2 --
iPad Pro 13-inch (M4) (Booted)
 
tx git:(main) git pull --rebase
From tsnet:athom/terminalx main
Successfully rebased onto main.
 
 
tx git:(main)
+
+
esc
ctrl
tab
~
/
|
+
⌘K
+
+
+
ms
+
u2
+
nh
+ +
+
+
+
+
+
+
+
+ +
+
1terminalX · iPadOS 优先 · 选定方案:1b 沉浸轨道终端 + 1e 会话缩略卡
+ +
+
+
+
设计前提
+
画布尺寸 iPad Pro 13″ 横屏 1366×1024pt(1:1)。App chrome 全部走 Nocturne 令牌;终端内容区保留 Catppuccin Mocha,与 chrome 通过同一深靛蓝底相接。文案简体中文,技术术语(mosh / tmux / tsnet)保留英文。
+
+
+
分层状态徽标
+
三层链路各占一枚徽标,从下到上:tsnet(网络可达)→ mosh RTT(传输健康,数字实时跳动,>200ms 转琥珀)→ tmux 会话名(复用层)。断哪层哪层变色,其余保持中性——避免"全红恐慌"。
+
+
+
iPad 输入取舍
+
触控时底部常驻 44pt 键盘附加行(Esc/Ctrl/方向/PgUp);检测到硬件键盘则整行收起、只留一条 4pt 提示带,把面积还给终端。pane 分隔线为 12pt 拖拽热区。
+
+
+
+ +
+
选定终端主界面 · 沉浸轨道1b — chrome 全部浮起,终端面积最大
+
+
1b终端 · 沉浸轨道 边栏压成 56pt 图标轨道(搜索 · 首页 · 主机 · TMUX window · 设置 · 展开键一应俱全,只是收成图标),chrome 全部浮起为胶囊,终端面积最大。点轨道底部的展开键或 ⌘\ 变成 288pt 完整侧边栏,见 3a
+
+
+
+
+
+
+
+
ms
u2
nh
+
+
+
TMUX
+
0
1
2
+
+
+
+
+
+
+
+
+
+
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
[ 8/8 ] Linking TerminalX
✓ build complete (14.2s)
 
terminalx git:(main) swift test --package-path packages/TXCore
◇ Test run started.
↳ Suite "tmux control mode"
parses %output escaped octal
layout diff keeps pane identity
DCS passthrough detected
%window-add spawns pane tree
↳ Suite "SessionMachine"
backoff caps at 30s
mosh three-phase resume
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) xcrun simctl list devices booted
 
terminalx git:(main)
+
+
+
+
0.1 · ubuntu-cn2 · journalctl -f80×14
+
12:01:19 cn2 mosh-server[891]: heartbeat 41ms
12:01:44 cn2 build-runner: job 39 finished
12:02:03 cn2 sshd[9931]: session opened for ci
12:02:31 cn2 tailscaled: derp hkg 24ms
12:02:58 cn2 mosh-server[891]: heartbeat 38ms
12:03:12 cn2 systemd: rotated build.log
12:03:41 cn2 ninja: 148/148 objects
12:04:12 cn2 tailscaled: endpoint DIRECT (udp)
12:04:31 cn2 kernel: wlan0 rssi -74
12:04:33 cn2 mosh-server: roaming → :51004
12:04:33 cn2 mosh-server: resume ok, 0 lost
12:05:02 cn2 systemd: Started build-runner@42
12:05:44 cn2 sshd: Accepted publickey for ci
12:06:02 cn2 runner: artifact 18.4 MB uploaded
12:06:20 cn2 tailscaled: peer nas-home offline
12:06:41 cn2 cron: rotated /var/log/build.log
12:07:03 cn2 systemd: build-runner@42 ok
+
+
+
+
tsnet
+
+
mosh41ms
+
+
tmuxdev
+
+
+ mac-studio~/src/terminalx · 168×44 +
+
+ 已连接妙控键盘 · 附加行已收起 + ⌘K 命令面板⌘D 竖分屏⌘⇧D 横分屏⌃⇥ 切 window长按 ⌘ 全部快捷键 +
+
+
+
+
+
+
+
选定主机管理 · 会话缩略卡1e — 活动会话优先,回到刚才那个会话是首要动作
+
+
1e主机 · 会话缩略卡 把「回到刚才那个会话」当作首要动作:上排是活着的会话(真实终端缩略图 + 最后一行输出),下排才是主机网格。适合 2–4 个并行会话来回跳。
+
+
+
9:41 周五 Wi-Fi · 78%
+
+
+

terminalX

+
3 个会话在跑 · 7 台主机 · Tailscale athom.ts.net
+
+
+
搜索⌘K
+
新建连接
+
+
+
+
活动会话
+
+
+
mac-studiotmux dev41ms
+
tx git:(main) git fetch --all --prune
Fetching origin … done
tx git:(main) swiftformat --lint Sources
✓ 0 issues in 84 files
 
tx git:(main) swift build -c release --arch arm64
[142/142] Compiling TXCore
[ 8/8 ] Linking TerminalX
✓ build complete (14.2s)
 
tx git:(main) swift test --package-path packages/TXCore
◇ Test run started.
↳ Suite "tmux control mode"
parses %output escaped octal
layout diff keeps pane identity
DCS passthrough detected
%window-add spawns pane tree
↳ Suite "SessionMachine"
backoff caps at 30s
mosh three-phase resume
host-key pin survives wipe
◇ 50 tests passed after 0.412s
 
tx git:(main) xcodegen generate --spec project.yml
✅ Created TerminalX.xcodeproj
 
tx git:(main) xcrun simctl list devices booted
-- iOS 26.2 --
iPad Pro 13-inch (M4) (Booted)
 
 
tx git:(main)
+
3 windows · 4 panes · 12 秒前⌘1回到会话
+
+
+
ubuntu-cn2tmux build186ms
+
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 ~ ./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
▸ 12/14 passed, 2 flaky (retrying)
↻ tmux/resize-under-loss
↻ tsnet/derp-failover
 
cn2 ~ tailscale netcheck | head -5
* UDP: true
* IPv4: yes, 203.0.113.9:41641
* Nearest DERP: Hong Kong
 
cn2 ~
+
1 window · relay hkg · 4 分钟前⌘2回到会话
+
+
+
nas-home重连中
+
nas:~$ zfs get compressratio tank
tank compressratio 1.34x
 
nas:~$ docker stats --no-stream | head -3
NAME CPU % MEM USAGE
plex 4.10% 1.21GiB
nas:~$ uname -sr
Linux 6.12.9-truenas
 
nas:~$ docker ps --format 'table {{.Names}}'
NAMES STATUS
plex Up 6 days
syncthing Up 6 days
caddy Up 12 hours
postgres Up 6 days (healthy)
 
nas:~$ zpool status -x
all pools are healthy
 
nas:~$ df -h /tank
Filesystem Size Used Avail Use%
tank 18T 11T 7.0T 62%
 
nas:~$ zfs list -t snapshot | tail -3
tank/photos@auto-2026-07-24 1.2G
tank/docs@auto-2026-07-24 84M
tank/vm@auto-2026-07-24 3.4G
 
nas:~$ tail -4 /var/log/syncthing.log
folder "photos" sync complete
folder "docs" idle
 
nas:~$
mosh 会话保活中,8s 后第 3 次重连
屏幕内容已缓存,恢复后原样接回
+
tsnet 节点离线 · 2 分钟前⌘3立即重试
+
+
+
全部主机
全部生产家里
+
+
+
mac-studio
macOS · M4 Max
+
moshtsnetSE key
+
+
+
ubuntu-cn2
Ubuntu 26.04
+
moshtsnet
+
+
+
edge-sg1
Debian 13
+
SSHtsnet
+
+
+
edge-sg2
离线 · 4 小时
+
SSH
+
+
+
nas-home
重连中
+
mosh
+
+
+
pi-lab
Raspberry Pi OS
+
SSH
+
+
+
mosh-lab
Fedora 44
+
mosh
+
+
添加主机
+
+
+
+
+
+
+
共用屏设置 · iPhone 对照跟随选定方案
+ +
+
1g设置 · 外观 左类别、右内容。核心是「app chrome 与终端同源换肤」:选一套 Catppuccin,chrome 的中性阶跟着走,Nocturne 的 blurple 始终作为唯一强调色不被覆盖。
+
+
+
9:41 周五 Wi-Fi · 78%
+
+
+

设置

+
外观与主题
终端与字体
Tailscale
密钥与安全
mosh 与重连
tmux 集成
键盘与手势
诊断日志
+
terminalX 0.6 (M4) · libghostty · mosh 1.4 · tsnet
+
+
+
+

外观与主题

+
App chrome 与终端配色同源:切一次,两边一起变。强调色恒为 Nocturne blurple。
+
+
+
Catppuccin 主题
+
+
+
+
~ git status
M Theme.swift
+
+
+
Mocha
+
+
+
+
~ git status
M Theme.swift
+
+
+
Macchiato
+
+
+
+
~ git status
M Theme.swift
+
+
+
Frappé
+
+
+
+
~ git status
M Theme.swift
+
+
+
Latte浅色
+
+
+
+
+
+
实时预览
+
terminalx git:(main) ls -l packages
drwxr-xr-x yz staff 192B TXCore
drwxr-xr-x yz staff 256B TXTransport
-rw-r--r-- yz staff 4.1K Package.swift
 
terminalx git:(main) git diff --stat
SessionMachine.swift | ++++++++---
TmuxController.swift | +++++-
2 files changed, 13(+), 4(-)
 
terminalx git:(main) 中文等宽对齐测试:终端渲染 CJK
宽字符占两格 emoji 归一
制表符对齐 连字 -> ≠
└─ 行高 1.50 · 字号 13.5pt
 
terminalx git:(main) swift build 2>&1 | tail -4
error: cannot find 'foo' in scope
warning: unused variable 'gate'
note: add explicit _ = to silence
✓ 编译完成 · 14.2s
 
terminalx git:(main) tailscale status | head -3
100.71.203.4 mac-studio direct
100.88.9.61 ubuntu-cn2 relay hkg
100.104.2.18 nas-home offline
 
terminalx git:(main) tmux list-panes
0.0 [124x38] zsh (active)
0.1 [ 60x18] btop
0.2 [ 60x19] tail -f build.log
 
terminalx git:(main) echo $TERM
xterm-256color · truecolor
 
terminalx git:(main)
+
+
+
排版
+
+
终端字体
JetBrains Mono
+
字号13.5pt
+
行高1.50
+
连字(ligatures)
-> ≠ 合并显示
+
CJK 等宽补偿
宽字符严格占两格
+
+
+
跟随系统浅/深色
深色 Mocha ⇄ 浅色 Latte
+
+
+ +
终端配色由 libghostty 渲染;chrome 的中性阶与所选主题同源推导,强调色始终取 Nocturne blurple。
+
+
+
+
+
+
+
+
+
+ + +
+
1hiPhone 紧凑对照 同一套令牌降级:边栏→底部 tab,pane 平铺→单 pane + 顶部 pane 段控,附加行两行滚动。
+
+
+
+
9:41 78%
+
+ +
mac-studio
tmux dev · 0 build
+
41ms
+ +
+
+
0.0 zsh
+
0.1 btop
+
0.2 log
+ 80×34 +
+
tx git:(main) git fetch --all --prune
Fetching origin … done
tx git:(main) swiftformat --lint Sources
✓ 0 issues in 84 files
 
tx git:(main) xcodebuild -showsdks | head -2
iOS SDKs: iOS 26.2 (iphoneos26.2)
tx git:(main) swift build -c release --arch arm64
[142/142] Compiling TXCore
[ 8/8 ] Linking TerminalX
✓ build complete (14.2s)
 
tx git:(main) swift test --package-path packages/TXCore
◇ Test run started.
↳ Suite "tmux control mode"
parses %output escaped octal
layout diff keeps pane identity
DCS passthrough detected
%window-add spawns pane tree
↳ Suite "SessionMachine"
backoff caps at 30s
mosh three-phase resume
host-key pin survives wipe
◇ 50 tests passed after 0.412s
 
tx git:(main) xcodegen generate --spec project.yml
✅ Created TerminalX.xcodeproj
 
tx git:(main) xcrun simctl list devices booted
-- iOS 26.2 --
iPad Pro 13-inch (M4) (Booted)
 
tx git:(main) git pull --rebase
From tsnet:athom/terminalx main
Successfully rebased onto main.
 
tx git:(main) git status --short
M apps/TerminalX/iOS/ContentView.swift
M apps/TerminalX/iOS/Theme.swift
?? docs/ui/ipad-rail.md
 
 
tx git:(main)
+
+
esc
ctrl
tab
~
/
|
+
⌘K
+
+
+
+
+
+
+
9:41 78%
+

主机

+
搜索
+
活动会话
+
+
+
mac-studio
tmux dev · 3 windows
41ms
+
swift test --package-path TXCore
◇ 50 tests passed after 0.412s
+
+
nas-home
重连中 · 8s 后第 3 次
重试
+
+
全部主机
+
+
ubuntu-cn2
ci@100.88.9.61 · mosh
+
edge-sg1
root@100.77.4.2 · SSH
+
nas-home
yz@100.104.2.18
+
pi-lab
pi@100.104.2.9 · SSH
+
edge-sg2
离线 4 小时
+
+
Tailscale 已连接
iphone · 100.92.14.11
+
主机
会话
密钥
设置
+
+
+
+
+
+
+
+

下一步:细化 1b 的 pane 拖拽 / 缩放态,或把 1e1b 串成可点原型。

+
+ + + diff --git a/packages/TXCore/Sources/TXCore/Tmux/TmuxSessionProbe.swift b/packages/TXCore/Sources/TXCore/Tmux/TmuxSessionProbe.swift new file mode 100644 index 0000000..7b00f94 --- /dev/null +++ b/packages/TXCore/Sources/TXCore/Tmux/TmuxSessionProbe.swift @@ -0,0 +1,102 @@ +import Foundation + +/// 在**普通 shell**(还没进 control mode)里探测服务器的 tmux 能力与会话列表。 +/// +/// 为什么需要它:点主机连接时要先让用户挑「attach 哪个会话 / 新建 / 跳过用原生终端」, +/// 而这个选择必须在进 control mode **之前**做出 —— 一旦发了 `tmux -CC new` 就已经建了会话。 +/// +/// **PTY 回显坑**:命令本身会被 PTY 回显。若标记字面量出现在命令里,回显行会被误当成结果起点。 +/// 故命令里把标记写成相邻字符串拼接(`"__TX""_SESSIONS_BEGIN"`):shell 输出完整标记, +/// 回显的却是拼接前的样子,扫描器只认完整标记,天然不会命中回显。 +/// +/// **为什么要 `tmux -V`**:`tmux list-sessions` 在「没装 tmux」和「装了但没有会话」两种情况下 +/// 都只往 stderr 写东西(被 `2>/dev/null` 吞掉),标记之间同样是空的 —— 只靠会话行分不开这两态, +/// 而它们的正确 UI 完全不同(装安装引导 vs 让用户新建会话)。版本行用来定这一刀。 +public struct TmuxSessionProbe: Sendable { + public static let beginMark = "__TX_SESSIONS_BEGIN" + public static let endMark = "__TX_SESSIONS_END" + /// 会话行前缀(与版本行区分)。 + public static let sessionPrefix = "S" + /// 字段分隔符:与 `TmuxController.fieldSep` 同源理由(tab 经 PTY 会被替换成下划线)。 + public static let fieldSep = "|:|" + + /// 要发进 shell 的探测命令(自带换行)。用 `printf` 而非 `echo`:跨 shell(fish/bash/zsh)一致。 + /// + /// **末尾清屏**:命令本身会被 PTY 回显、结果也会打在屏幕上,用户不该看见这些内部细节。 + /// 探测发生在刚连上、用户还没输入任何东西时,清掉的只有 motd/shell 欢迎语,换来干净的首屏。 + /// 用裸 ANSI(`[H` 归位 + `[2J` 清屏 + `[3J` 清 scrollback)而非 `clear`:不依赖 terminfo。 + public static var command: String { + let begin = #"printf '%s\n' "__TX""_SESSIONS_BEGIN""# + let end = #"printf '%s\n' "__TX""_SESSIONS_END""# + let version = "tmux -V 2>/dev/null" + let list = "tmux list-sessions -F '\(sessionPrefix)\(fieldSep)#{session_name}\(fieldSep)#{session_windows}\(fieldSep)#{session_attached}\(fieldSep)#{session_activity}' 2>/dev/null" + let clear = #"printf '\033[H\033[2J\033[3J'"# + return "\(begin); \(version); \(list); \(end); \(clear)\n" + } + + /// 一条会话记录(探测阶段拿到的最小信息)。 + public struct Session: Equatable, Sendable { + public let name: String + public let windows: Int + public let isAttached: Bool + /// `#{session_activity}`(Unix 秒)→ 选择器副行的「N 分钟前」。 + public let lastActivity: Date? + + public init(name: String, windows: Int, isAttached: Bool, lastActivity: Date? = nil) { + self.name = name + self.windows = windows + self.isAttached = isAttached + self.lastActivity = lastActivity + } + } + + public enum Result: Equatable, Sendable { + /// tmux 可用。`sessions` 可能为空 —— 装了但还没有任何会话,UI 只给「新建 / 跳过」。 + case available(version: String, sessions: [Session]) + /// 没有 tmux(`tmux -V` 没输出)→ 走原生终端 + 安装引导横幅。 + case unavailable + } + + private var buffer = "" + private var inBlock = false + private var version: String? + private var sessions: [Session] = [] + private var done = false + + public init() {} + + /// 流式喂入终端输出。命中 END 标记时返回结果;之后恒返回 nil(一次性)。 + public mutating func feed(_ text: String) -> Result? { + guard !done else { return nil } + buffer += text + // 按行切:PTY 行尾是 CRLF,而 Swift 视 "\r\n" 为单个 Character,故用 isNewline 判定。 + while let idx = buffer.firstIndex(where: { $0.isNewline }) { + let raw = String(buffer[buffer.startIndex ..< idx]) + buffer.removeSubrange(buffer.startIndex ... idx) + let line = raw.trimmingCharacters(in: .whitespacesAndNewlines) + if !inBlock { + if line == Self.beginMark { inBlock = true } + continue + } + if line == Self.endMark { + done = true + guard let version else { return .unavailable } + return .available(version: version, sessions: sessions) + } + if line.hasPrefix(Self.sessionPrefix + Self.fieldSep) { + if let s = Self.parseSession(line) { sessions.append(s) } + } else if line.hasPrefix("tmux ") { + version = line // 例:tmux 3.7b + } + } + return nil + } + + /// `S|:|name|:|windows|:|attached|:|activity` → Session + static func parseSession(_ line: String) -> Session? { + let f = line.components(separatedBy: fieldSep) + guard f.count >= 4, f[0] == sessionPrefix, !f[1].isEmpty, let w = Int(f[2]) else { return nil } + let activity = f.count >= 5 ? Double(f[4]).map { Date(timeIntervalSince1970: $0) } : nil + return Session(name: f[1], windows: w, isAttached: f[3] == "1", lastActivity: activity) + } +} diff --git a/packages/TXCore/Tests/TXCoreTests/TmuxSessionProbeTests.swift b/packages/TXCore/Tests/TXCoreTests/TmuxSessionProbeTests.swift new file mode 100644 index 0000000..859f5de --- /dev/null +++ b/packages/TXCore/Tests/TXCoreTests/TmuxSessionProbeTests.swift @@ -0,0 +1,69 @@ +import Foundation +import Testing +@testable import TXCore + +@Suite("tmux 会话探测(连接前选择用)") +struct TmuxSessionProbeTests { + /// 命令里的标记必须是**拼接写法** —— 否则 PTY 回显会被误当成结果起点。 + @Test("命令不含完整标记字面量,避免 PTY 回显误命中") + func commandAvoidsEchoSelfMatch() { + let cmd = TmuxSessionProbe.command + #expect(!cmd.contains(TmuxSessionProbe.beginMark)) + #expect(!cmd.contains(TmuxSessionProbe.endMark)) + #expect(cmd.contains("tmux -V")) + #expect(cmd.contains("list-sessions")) + } + + @Test("解析会话列表(CRLF 行尾 + 版本行)") + func parsesSessions() { + var p = TmuxSessionProbe() + // 先喂一段回显噪声(含拼接前的命令文本),不应触发。 + #expect(p.feed("kid@Air ~> printf '%s\\n' \"__TX\"\"_SESSIONS_BEGIN\"\r\n") == nil) + #expect(p.feed("\(TmuxSessionProbe.beginMark)\r\n") == nil) + #expect(p.feed("tmux 3.7b\r\n") == nil) + #expect(p.feed("S|:|main|:|5|:|0|:|1784988802\r\n") == nil) + #expect(p.feed("S|:|demo|:|2|:|1|:|1784901778\r\n") == nil) + let result = p.feed("\(TmuxSessionProbe.endMark)\r\n") + guard case .available(let version, let sessions) = result else { + Issue.record("期望 available,实际 \(String(describing: result))") + return + } + #expect(version == "tmux 3.7b") + #expect(sessions.count == 2) + #expect(sessions[0] == TmuxSessionProbe.Session( + name: "main", windows: 5, isAttached: false, + lastActivity: Date(timeIntervalSince1970: 1784988802))) + #expect(sessions[1].name == "demo") + #expect(sessions[1].isAttached) + // 一次性:命中后不再返回结果。 + #expect(p.feed("S|:|late|:|1|:|0|:|1\r\n") == nil) + } + + @Test("装了 tmux 但没有会话 → available 且列表为空(UI 只给新建/跳过)") + func availableWithNoSessions() { + var p = TmuxSessionProbe() + _ = p.feed("\(TmuxSessionProbe.beginMark)\n") + _ = p.feed("tmux 3.5a\n") + let r = p.feed("\(TmuxSessionProbe.endMark)\n") + #expect(r == .available(version: "tmux 3.5a", sessions: [])) + } + + @Test("没装 tmux(无版本行)→ unavailable,直接走原生终端") + func unavailableWithoutVersion() { + var p = TmuxSessionProbe() + _ = p.feed("\(TmuxSessionProbe.beginMark)\n") + let r = p.feed("\(TmuxSessionProbe.endMark)\n") + #expect(r == .unavailable) + } + + @Test("畸形会话行被跳过而非崩溃") + func skipsMalformedLines() { + var p = TmuxSessionProbe() + _ = p.feed("\(TmuxSessionProbe.beginMark)\ntmux 3.7\n") + _ = p.feed("S|:|\n") // 字段不足 + _ = p.feed("S|:||:|3|:|0\n") // 空名字 + _ = p.feed("S|:|ok|:|x|:|0\n") // windows 非数字 + let r = p.feed("\(TmuxSessionProbe.endMark)\n") + #expect(r == .available(version: "tmux 3.7", sessions: [])) + } +} diff --git a/packages/TXTransport/Sources/TXTransport/SSHSession.swift b/packages/TXTransport/Sources/TXTransport/SSHSession.swift index 0f4b9c5..9799c22 100644 --- a/packages/TXTransport/Sources/TXTransport/SSHSession.swift +++ b/packages/TXTransport/Sources/TXTransport/SSHSession.swift @@ -102,6 +102,30 @@ public final class SSHSession: Transport, @unchecked Sendable { // MARK: - Connect / Handshake + /// 带超时的 TCP connect:临时置非阻塞 → connect → `poll` 等可写 → 校验 SO_ERROR → 恢复阻塞。 + /// 不用裸 `Darwin.connect`:那会阻塞到系统 TCP 超时(75s+),UI 上就是长时间「连接中」。 + private static func connectWithTimeout(_ fd: Int32, _ addr: UnsafeMutablePointer, + _ len: socklen_t, seconds: Int) -> Bool { + let flags = fcntl(fd, F_GETFL, 0) + guard flags >= 0, fcntl(fd, F_SETFL, flags | O_NONBLOCK) >= 0 else { + return Darwin.connect(fd, addr, len) == 0 // 拿不到 flags 时退回阻塞语义 + } + defer { _ = fcntl(fd, F_SETFL, flags) } // 恢复阻塞(libssh2 用阻塞模式) + + if Darwin.connect(fd, addr, len) == 0 { return true } + guard errno == EINPROGRESS else { return false } + + var pfd = pollfd(fd: fd, events: Int16(POLLOUT), revents: 0) + let ready = withUnsafeMutablePointer(to: &pfd) { poll($0, 1, Int32(seconds * 1000)) } + guard ready == 1, pfd.revents & Int16(POLLOUT) != 0 else { return false } + + // 可写不代表成功:必须查 SO_ERROR(拒绝连接也会让 fd 变可写)。 + var soErr: Int32 = 0 + var soLen = socklen_t(MemoryLayout.size) + guard getsockopt(fd, SOL_SOCKET, SO_ERROR, &soErr, &soLen) == 0, soErr == 0 else { return false } + return true + } + private func connect() throws { let fd: Int32 if preconnectedFD >= 0 { @@ -124,12 +148,16 @@ public final class SSHSession: Transport, @unchecked Sendable { while let addr = node { dialed = socket(addr.pointee.ai_family, addr.pointee.ai_socktype, addr.pointee.ai_protocol) if dialed >= 0 { - if Darwin.connect(dialed, addr.pointee.ai_addr, addr.pointee.ai_addrlen) == 0 { break } + if Self.connectWithTimeout(dialed, addr.pointee.ai_addr, + addr.pointee.ai_addrlen, + seconds: config.connectTimeoutSec) { break } close(dialed); dialed = -1 } node = addr.pointee.ai_next } - guard dialed >= 0 else { throw SSHError.socket("连接失败: \(config.host):\(config.port)") } + guard dialed >= 0 else { + throw SSHError.socket("连接失败(\(config.connectTimeoutSec)s 超时): \(config.host):\(config.port)") + } fd = dialed } diff --git a/packages/TXTransport/Sources/TXTransport/Transport.swift b/packages/TXTransport/Sources/TXTransport/Transport.swift index 354a4f8..3b9ac0b 100644 --- a/packages/TXTransport/Sources/TXTransport/Transport.swift +++ b/packages/TXTransport/Sources/TXTransport/Transport.swift @@ -35,6 +35,9 @@ public struct SSHConfig: Sendable { /// (trusted 或 firstUse 已 pin),false=拒绝(mismatch)→ SSHSession 断开并抛 hostKeyMismatch。 /// nil 时不校验(历史行为)。校验/pin 的存储与 UI 由 app 层实现。 public var hostKeyVerifier: (@Sendable (Data, Int32) -> Bool)? + /// TCP connect 超时(秒)。**必须有**:`Darwin.connect` 默认阻塞到系统 TCP 超时(75s+), + /// 连不上的主机会让用户对着「连接中」干等。tsnet 路径(preconnectedFD)不经此处。 + public var connectTimeoutSec: Int public enum Authentication: Sendable { case password(String) @@ -51,9 +54,11 @@ public struct SSHConfig: Sendable { terminalType: String = "xterm-256color", initialCols: UInt16 = 80, initialRows: UInt16 = 24, - hostKeyVerifier: (@Sendable (Data, Int32) -> Bool)? = nil + hostKeyVerifier: (@Sendable (Data, Int32) -> Bool)? = nil, + connectTimeoutSec: Int = 12 ) { self.host = host + self.connectTimeoutSec = connectTimeoutSec self.port = port self.username = username self.authentication = authentication diff --git a/scripts/tx-shot.sh b/scripts/tx-shot.sh new file mode 100755 index 0000000..64a8de2 --- /dev/null +++ b/scripts/tx-shot.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# 无头截图取证:装 → 启(可带启动参数)→ 截图 → 修正方向。 +# +# 本环境没有 GUI,模拟器帧缓冲恒为竖屏;带 -txLandscape 1 时 app 会经 +# requestGeometryUpdate 把「内容」转成横屏(设计基准 iPad Pro 13″ 1366×1024), +# 但帧缓冲仍是竖的,故截完用 sips 转回来,Read 出来才是设计稿的方向。 +# +# 用法: +# scripts/tx-shot.sh /tmp/shot.png # 只截当前画面 +# scripts/tx-shot.sh /tmp/shot.png -- -txUIFixture 1 -txLandscape 1 # 重启并带参数 +set -euo pipefail + +UDID="${TX_UDID:-2EA9055E-4557-4B20-82AF-56A68AC1D454}" +BUNDLE=ai.athom.terminalx +OUT="${1:?usage: tx-shot.sh [-- ]}" +shift || true + +if [ "${1:-}" = "--" ]; then + shift + APP=$(find ~/Library/Developer/Xcode/DerivedData/TerminalX-*/Build/Products/Debug-iphonesimulator \ + -maxdepth 1 -name TerminalX.app | head -1) + [ -n "$APP" ] || { echo "TerminalX.app not found — build first" >&2; exit 1; } + xcrun simctl terminate "$UDID" "$BUNDLE" 2>/dev/null || true + xcrun simctl install "$UDID" "$APP" + xcrun simctl launch "$UDID" "$BUNDLE" --args "$@" >/dev/null + # 等 surface attach + OutputGate flush(tmux/多 pane 场景需要更久) + sleep "${TX_SETTLE:-6}" + LANDSCAPE=0 + for a in "$@"; do [ "$a" = "-txLandscape" ] && LANDSCAPE=1; done +else + LANDSCAPE="${TX_LANDSCAPE:-1}" +fi + +xcrun simctl io "$UDID" screenshot "$OUT" >/dev/null 2>&1 +if [ "$LANDSCAPE" = "1" ]; then + sips -r 90 "$OUT" >/dev/null # landscapeRight 内容 → 正向 +fi +sips -g pixelWidth -g pixelHeight "$OUT" | tail -2 +echo "wrote $OUT"