Platform Integration zh CN - D0n9X1n/SonicTerm GitHub Wiki
本页说明 macOS、Windows 与 Linux 的差异,平台矩阵可供快速对照。构建包见 打包,验证步骤见开发与发布,发布步骤见发布流程。
flowchart TD
app["sonicterm-app<br/>winit 生命周期 · 窗口/标签页/窗格 · PTY 接线 · 输入 · 重绘 · 目标打开"]
mac["sonicterm-mac<br/>AppKit · NSMenu · Apple Event · NSPasteboard"]
win["sonicterm-windows<br/>Win32/DPI · muda · DWM · OLE · WiX metadata"]
linux["sonicterm-linux<br/>X11/Wayland identity · Linux 启动 · desktop metadata"]
app --> mac
app --> win
app --> linux
不需要 AppKit、Win32、X11 或 Wayland handle 的行为应放在 sonicterm-app 或更低层。
平台 crate 只负责必须使用原生主线程对象、平台 ABI、桌面 identity 或安装器 metadata 的工作。
终端解析位于 sonicterm-vt;本地 PTY/ConPTY 封装在
sonicterm-io::PtyHandle 后;渲染位于 sonicterm-gpu。
三个二进制共用诊断、配置、日志、资源、状态机和 shell 启动流程;准确顺序见
运行时生命周期。用户状态位于 ~/.sonicterm;打包资源由
sonicterm-cfg::assets 查找。
终端输入法几何属于共享 app:每个窗口发送活动窗格的物理光标矩形,只加一次窗格原点和 内容内边距。去重键包含窗格身份、物理位置和物理尺寸,不仅是行列。命令面板和搜索框保留 各自的输入锚点。
路径扫描和可操作性探测属于跨平台 app。有限队列 worker 会在原生调用前再次核对完全相同的
目标类型和操作,并阻止特殊文件。macOS 和 Linux 会解析符号链接并对解析后的目标操作。
在 Windows 上,解析指向网络共享的链接,或打开映射网络驱动器上的路径,都会连接该服务器,
因此探测会持有路径每一部分的句柄,而不是检查路径字符串。它只用 QueryDosDeviceW 解析一次
盘符;这是命名空间查询,既不访问文件系统也不访问网络。只有当盘符恰好指向
\Device\HarddiskVolume<N> 卷、且其根目录报告为本地磁盘时,才会遍历该驱动器;之后通过
NT 设备路径打开根目录,不再经由盘符。其后的每一部分都相对于父目录已持有的句柄按单个名称
打开,不跟随该处的链接,并在检查或原生操作结束前一直保持打开且不共享删除权限,因此已持有
的部分无法被重命名或删除,最后一部分之上已持有的文件夹也无法被改成链接。每一部分都以读取
权限(文件夹为列出内容)打开;读取被拒绝时改用执行权限(文件夹为遍历),从不请求写入或删除
权限。用户既不能读取也不能执行的部分,或被其他程序以不共享读取的方式占用的部分,都会被拒绝;
检查进行期间,其他程序无法重命名或删除已持有的部分。Windows 只在本地
固定磁盘之间跟随符号链接或目录联接,目标须为 NT 盘符路径或相对于链接所在文件夹的路径。
映射网络驱动器、subst 驱动器、光驱和 RAM 盘、挂载到盘符的动态磁盘卷或卷影副本上的路径,
以及经过以 GUID 挂载卷的文件夹的
路径都会被拒绝;UNC 和设备目标、位于可移动驱动器上的链接、云文件占位符等其他
reparse point、目标类型与链接自身文件夹标志不符的链接,以及需要超过 31 次链接跳转的路径
也会被拒绝;31 是 Windows 为指向完整路径的链接记录的上限。遍历从不沿链接离开本地固定磁盘,
也从不打开远端卷;未指向任何驱动器的盘符上的路径会被拒绝,而不是报告为不存在。调用时会再次遍历整条链接链,并在持有每一部分期间把遍历得到的无链接路径
(而非屏幕上显示的文本)交给 shell,因此经由链接到达的文件会在其真实文件夹中被选中。保证
到此为止:之后由 shell、资源管理器和文件的处理程序自行打开该路径,最后一部分仍可能被原地
更改,shell 调用返回后不再持有任何句柄。用户自己登录会话中的进程可以重新定义盘符并直接
访问网络,因此不在保护范围内。
普通文件无论扩展名、执行权限或内容如何,都只被选中。
候选按最短优先探测,因此存在的去掉正文标点的较短路径(如 main.rs)优先于带标点的较长
字面候选(如 main.rs,);只有较短路径不存在时才会打开字面候选。
路径与 URL 检测是各操作系统共用的一个组件,因为 Windows 窗格也可能显示 POSIX 路径,例如在
WSL shell 中。crates/sonicterm-cfg/src/url_scan.rs 实现 POSIX 与 Windows 两种路径语法,
不含操作系统条件编译,其测试在每个主机上运行;PathStyle::native() 是它唯一读取构建目标的地方。
按操作系统区分的文件负责原生探测、在文件管理器中显示与打开(sonicterm-app 中的
path_target/{unix,macos,linux,windows}.rs),以及 URI 分发(sonicterm-cfg 中的
url_open/{macos,linux,windows}.rs)。app 使用 PathStyle::native() 选择语法,因此 Windows
构建会用 Windows 语法扫描每个窗格,包括运行 WSL shell 的窗格。
| 平台 | 调用边界 |
|---|---|
| macOS | 目录使用固定 /usr/bin/open --;文件使用 /usr/bin/open -R -- 选中而不打开 |
| Windows | 目录使用 ShellExecuteExW;文件使用 SHOpenFolderAndSelectItems,均在专用 COM apartment 中执行 |
| Linux | 目录使用 desktop portal,仅不可用时回退到固定 xdg-open;文件使用 org.freedesktop.FileManager1.ShowItems,不回退到打开文件 |
选中文件不会调用其关联应用或执行内容。macOS 应用或软件包目录使用 Finder 选择,而不启动。 Portal 明确拒绝不等于 portal 不可用,因此不会触发 fallback。
Windows 上,目录导航和非文件 URI 走同一条调用边界:由拥有独立 COM apartment
的 worker 线程直接调用 ShellExecuteExW,不经过任何命令解释器,也不会对参数字符串重新
分词。URI 以单个 NUL 结尾的 UTF-16 字符串传入,并且环境变量替换保持关闭,因此 %20、%USERNAME%
这类以百分号分隔的文本会按验证后的原样交给 handler,不会按进程环境展开。
共享的 macOS App::do_resumed 路径通过 winit 的
set_allows_automatic_window_tabbing(false) 在菜单 hook 和原生窗口创建前关闭进程级
自动标签页。二进制保留普通启动和 smoke 的 window-ready callback,将初始 NSWindow 的
setTabbingMode: 2 设为禁用。进程级设置与每窗口模式是两层控制;终端标签页仍由 SonicTerm 管理。
原生 smoke 在创建窗口前读回进程级属性。这证明设置已到达 AppKit,不证明每个窗口原生 标签栏的显示效果。
NSMenu 只能在 winit 创建 AppKit 事件循环后安装。Objective-C target 接收菜单 selector,
把菜单 tag 转换为共享 Action,再通过 event-loop proxy 唤醒循环。需要 NSWindow 的工作
在一次性 window-ready callback 中执行,此时 handle 已有效。
在 macOS 14 及更高版本上,每个注册的窗口(主窗口、新终端窗口和拖出的窗口)从其 AppKit 视图取得
NSView.displayLink。链接以 common modes 加入主 run loop,创建时处于暂停状态,只在该窗口有按显示
链接计节奏的流式帧待准入时运行(渲染模式)。它的
Objective-C target 只持有窗口 id、event-loop proxy 的副本和窗口的链接代际;链接触发时,target 通过
proxy 发送 UserEvent::DisplayLinkTick,从不访问 App。丢弃窗口的句柄会使链接失效并释放其 target。
低于 macOS 14 时,available!(macos = 14.0) 为 false,因此不安装链接,窗口保持计时器路径;
SonicTerm 不使用 CVDisplayLink。Windows 和 Linux 不安装链接。原生检查在
crates/sonicterm-app/tests/display_link_native.rs 中于进程主线程上的真实 NSView 运行,该测试没有
libtest 框架;它不断言显示器会触发 tick。
App bundle 以 LSHandlerRank=Alternate 声明 public.shell-script 和
com.apple.terminal.shell-script。进程级 observer 接收
NSApplicationWillFinishLaunchingNotification 后安装 kAEOpenDocuments Apple Event
handler。回调只把路径放入共享 open-script 队列;窗口与 PTY 仍由事件循环线程创建。
冷启动打开多个文件时会保持顺序且不额外创建空白标签页,之后的事件则追加标签页。
相对路径以进程启动时的工作目录为准。这是 Finder 的打开方式集成,不是全局默认终端选择器。
macOS 的操作系统交接后端把序列化 TabPayload 写入 general NSPasteboard,类型为
com.sonic-terminal.tab.v1。sonicterm-mac 仅在进程启动时、MacShell::run 之前检查
一次该 payload;若内容有效,就从 pasteboard 删除并作为 pending input 交给 shell。
已经运行的 peer 再次变为 active 时不会重新检查。因此接收端仅支持启动时读取,而且该
后端不会创建 NSDraggingSession,也没有原生光标预览。写入 pasteboard 返回
NotAcknowledged,所以源标签页继续保留,app 会走常规的进程内 tear-out 路径。同进程
移动使用共享的进程内标签页转移路径。
Bundle 从 Contents/Resources/assets 读取运行时资源。四个 Rec Mono St.Helens
字体仅保存在 assets/fonts,通过 ATSApplicationFontsPath=assets/fonts 让
AppKit/CoreText 解析同一份文件。安装包在 Contents/Frameworks 中包含 Cairo 的非系统
动态库依赖,使用 bundle 内相对加载路径,并附带许可证和来源清单。最低 macOS 版本反映
可执行文件/库的实际部署目标,而不只是打包主机架构。验证方式见打包。
winit 创建 HWND 前,sonicterm-windows 会请求
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2。这个进程级设置早于事件循环构造,因此保留,
不假设 winit 较晚的设置在所有启动路径上都等价。Release 构建使用 Windows GUI subsystem,
不会打开控制台窗口。一次性 window-ready callback 收到有效 HWND 后应用 DWM backdrop
并安装原生 muda 菜单。窗口移动、snap layout 和最小化/最大化/关闭控件仍由 Windows
原生 chrome 管理。
菜单把 muda 事件转换为共享 action。DWM 可请求 Mica、Acrylic 或 Tabbed material,
并可回退为 opaque。强制软件渲染使用不透明窗口,因为 GDI presenter 无法合成透明效果。
安装后的 executable 接受一个无损的 --open-script <PATH> 参数。启动逻辑会相对进程
最初工作目录解析相对参数,并在 WindowsShell::run 前入队,因此冷启动直接打开脚本标签页,
不会先创建 HOME 标签页。私有 tear-out payload 不能与 --open-script 同时使用。
--refresh-shell-associations 不创建窗口,只广播 SHCNE_ASSOCCHANGED。MSI 为 .ps1、
.cmd、.bat 和 .sh 注册 SonicTerm ProgID、Default Apps capabilities 和
OpenWithProgids,不会写扩展名默认值或 UserChoice。这是文件处理程序集成,不是 Windows
全局默认终端应用协议。
Windows 后端在 UI 线程初始化 OLE,并实现 COM IDataObject、IDropSource 和
IDropTarget。它注册私有 com.sonic-terminal.tab.v1 clipboard format
(CF_SONIC_TAB),使用 DoDragDrop 和 RegisterDragDrop。UTF-8 标签页 JSON 在清零的
可移动全局内存中拥有自己的 NUL 结束符;GlobalSize 表示分配容量而非 payload 长度,
因此分配器填充字节不会改变同进程 payload 匹配。
已安装的后端明确声明是否拥有原生 drop target。共享窗口设置在主窗口、新窗口、拆出窗口及 隐藏预热 HWND 创建前,仅为自定义所有者关闭 winit 默认 target。没有自定义后端时,保留 winit 默认文件拖放。隐藏预热窗口直到启用时才注册自定义 target。注册失败会在启动 shell 或 转移窗格前放弃隐藏目标;拆出失败会恢复源窗口。成功注册后保留窗口直到撤销注册;后端退出时 先释放剩余 target,再销毁 OLE guard。
文件拖放通过共享队列携带注册目标的 WindowId;稍后的焦点变化不会重定向它,已关闭目标
会丢弃其拖放。重叠标签栏只在实际接收的原生窗口内命中。同进程标签页移动还要求活动手势及
完全匹配的 payload;稳定的源 WindowId/TabId 仍是权威。来自其它进程或格式错误的标签页
payload 会被拒绝,不确认移动:目前不支持跨进程转移存活 PTY。OLE 返回 MOVE 却没有
已解析的本地结果时,会取消而不是虚构目标。
默认 Windows runtime smoke 安装生产 OLE 后端,要求主窗口、预热启用子窗口及新建子窗口共三对 成功注册/撤销,且没有残留注册或失败。原生单元测试使用真实隐藏 HWND 和 COM 数据对象, 覆盖所有权、Unicode 文件路径、精确目标、重复注册拒绝及清理。直接 COM 调用证明解码与路由, 不证明物理拖放手势的交付。
DirectWrite/GDI 桥向 analysis source 传入完整 UTF-16。映射位置、剩余文本及 locale 长度 都按 UTF-16 代码单元计算,不按 Rust 字符数量计算。返回零长度、越界或拆分代理项对的映射 范围时,整个原生回退请求失败,包含此前已积累的候选。调用方报告失败并继续其余已配置的 字体查找源。成功请求返回按顺序去重的候选字体列表,不是逐字符分配;已加载字体和 BMP 行为保留原有路径。
Windows 二进制只负责 GUI 胶水,不负责终端解析或 ConPTY。本地进程 hosting 仍封装在
sonicterm-io::PtyHandle 后。
Windows 上启用软件渲染降级时,sonicterm-gpu 在
crates/sonicterm-gpu/src/software_frame.rs 中合成 CPU BGRA 帧。仅在 Windows 编译的
software_windows.rs 桥接层通过 GDI 呈现完整帧;这条路径不把保留式 GPU 损伤区域
作为第二套呈现策略。crates/sonicterm-windows/src/software_presenter.rs 负责配置决策,
不负责帧合成或原生 blit。
发布 crate 是 sonicterm-linux,可执行文件名为 sonicterm。winit 使用 X11 或 Wayland。
Desktop entry、AppStream component、hicolor icon、Wayland application id 和 X11 class
统一为 com.d0n9x1n.SonicTerm;X11 instance name 为 sonicterm。统一 identity 可让
launcher activation、任务分组和 compositor identity 对齐。
Linux 没有 SonicTerm 原生菜单、桌面通知 bridge、前台进程标题 adapter、原生 material backdrop 或跨进程标签页拖放。Linux shell 会在共享 app runner 上安装纯平台收敛器。 启动和每次显式重载都会在配置被存储或应用前经过同一个接缝:Mica、Acrylic 与 Tabbed 会变为 opaque 并记录一次 warning;已经为 opaque 的输入保持不变且不写 warning。 预热、新建和拆出窗口因此都会使用同一已收敛值。macOS 与 Windows 安装 identity 行为, 保留各自支持的 backdrop 策略。共享窗格、标签页、窗口和进程内标签页移动仍可使用。
自动 shell 选择按顺序采用第一个可执行候选:$SHELL、getpwuid_r 返回的当前用户
passwd shell、最后 /bin/sh。显式 shell 配置优先。
便携包使用 executable 相邻的 assets/;Debian 安装使用
/usr/share/sonicterm/assets。启动时会检查四个内置 Rec Mono 字体,把字体目录在原生
Fontconfig discovery 前传给 FontStack,并在字体重载时继续保留。原生 fallback 仍可用。
三个发行二进制都接受隐藏的 --runtime-smoke 模式。平台提供真实 shell 命令(macOS/Linux
使用 /bin/sh,Windows 使用 cmd.exe);共享 runner 要求原生窗口、渲染器/设备、在实时
grid 中观察到非字面 PTY marker,并在之后完成一次原生呈现。默认场景随后使用生产默认预热池
创建并报告一个隐藏渲染器,通过标签页拆出采用完全相同的窗口,呈现子窗口,再关闭它、清除可能补充的
备用项,并要求 live_renderer_count 回到创建窗口前的基线。预热生命周期失败使用稳定退出码
16。
Windows 默认运行仍要求三次 OLE 注册及对应撤销;提前执行的 frame-validation 运行要求恰好 一对主窗口注册/撤销。设备恢复要求恰好两对可见窗口注册/撤销;隐藏备用窗口不会被采用或注册。 每种场景都要求 App 清理后没有原生失败或仍然存活的注册,并让 OLE guard 存活至后端释放完成。
smoke 会拆出一个临时的第二标签页,让原主窗口的 shell 及其 marker 历史在预热子窗口销毁后保留。 故障阶段统计实时 grid 与 scrollback 中包含 marker 的行;重复读取旧 marker 不能证明存活。 它们比较所有窗口的实际呈现调用与已确认帧总数,并要求实际尝试过渲染,且至少 250 毫秒没有呈现。 每个故障最多等待 5 秒;销毁钩子另有 5 秒轮询上限。应用的 watchdog 在 30 秒后请求退出;runner 强制执行 45 秒的外层硬期限。15 秒的差额留给已排队的 watchdog 事件、有界销毁轮询及清理。 这些是配置的期限,不是已测量的耗时。
预热生命周期之后,默认运行会检查每个打开的窗口是否与主窗口共享同一设备代次,然后驱动渲染器
隐藏文档的 GPU 故障钩子。隔离故障之后,主窗口必须再完成一次原生呈现。通过
force_rebuild_for_scale 注入的保留资源故障,必须使设备被记录为 Unusable、没有窗口呈现,
并在实时 grid 中观察到一个重新执行的 PTY marker。代次检查与这两个阶段失败时退出码为 17。
随后的设备销毁必须产生丢失记录和另一个重新执行的 marker,否则退出码为 18。
另一个独立进程用 scripts/native-smoke-runner.py --scenario frame-validation 启动,从可用
设备开始,在第一次主窗口呈现之后启用持续的帧验证故障。启用只创建有效的 probe:此时不发送
marker,也不开始静默观察区间。只有观察到一次实际故障渲染使设备变为 Unusable 后,smoke
才重新记录 marker 行数基线、再次发送 shell 命令,并开始新的 250 毫秒无呈现观察区间。
确认停止前输出的 marker 不能满足证明;等待故障渲染的时间也不能抵扣静默观察时间。独立的
5 秒期限始终从启用故障时起算,确认停止不会延长它。两个呈现计数仍与最初故障前的基线比较。
停止后缺少新 marker 或故障隔离检查失败时退出码为 17。runner 会为该次运行设置
SONICTERM_RUNTIME_SMOKE_SCENARIO=frame-validation,普通运行则移除继承的该值。应用收到未知的
环境变量值时,会在事件循环启动前以退出码 10 失败;runner 收到未知的 --scenario 参数时,
作为调用错误返回退出码 2,不启动子进程。
第三个进程选择 --scenario device-recovery。它先在两个可见窗口中呈现新执行的 shell marker,
同时保留一个预热渲染器,然后销毁共享设备,要求只请求一次替换、三个渲染器共同提交同一个新代次。
两个原 PTY 必须始终存活,并在该代次上呈现新的 marker。旧代次事件必须已被观察到且未导致再次
重建;随后释放子窗口与备用渲染器,恢复渲染器计数,最终 shell 关闭也必须完成原生清理。恢复检查
失败返回 19。默认与 frame-validation 场景禁用恢复,使停止设备的断言不能依赖替换 context 通过。
自动化会传入彼此分开的临时 config/ 与 logs/ 根目录,且不会替换 HOME。
scripts/native-smoke-runner.py 会移除继承的 NO_COLOR、保存 stdout/stderr 与日志工件、
执行 45 秒外层期限,在 POSIX 上终止子进程所属的进程组;离开此组的后代不在该期限的约束内。
PR 和 release gate 用带原生进程期限的独立步骤,在已构建的 macOS、Windows 二进制以及 X11 与
Wayland 上的两种 Linux 包布局中运行三个场景。打包说明 Linux 的场景参数和
隔离证据路径。其它阶段成功但原生 PTY 清理未完成时退出码为 20;更早的故障、设备丢失或恢复失败
保留原退出码。
| 能力 | macOS | Windows | Linux |
|---|---|---|---|
| 窗口后端 | winit + AppKit hook | winit + Win32 hook | winit + X11 或 Wayland |
| 本地 PTY | portable-pty Unix PTY | portable-pty ConPTY | portable-pty Unix PTY |
| 默认字形光栅器 | FreeType | DirectWrite,FreeType fallback | FreeType |
| 字体发现 | CoreText | DirectWrite/GDI | 打包字体目录 + Fontconfig |
| 标签页操作系统交接 | NSPasteboard 发布,无 NSDraggingSession
|
OLE/COM 拖放 | 仅进程内 |
| 原生菜单 | NSMenu | muda |
不可用;保留应用内操作 |
| Backdrop | AppKit blur/config | DWM Mica/Acrylic/Tabbed | opaque |
| 软件呈现 | wgpu adapter 路径 | CPU BGRA + GDI | wgpu Vulkan/lavapipe 或选中的平台 adapter |
| 安装包格式 | 各架构 .dmg 中的 .app
|
x64 WiX .msi
|
x86_64 .deb 与 .tar.gz
|
| 签名 | ad-hoc bundle 签名 | 未签名 | 未签名 |
| 边界 | 主要路径 |
|---|---|
| 共享平台 shell | crates/sonicterm-app/src/shell.rs |
| 路径与 URL 检测(各操作系统共用) |
crates/sonicterm-cfg/src/url_scan.rs、crates/sonicterm-app/src/app/path_target.rs
|
| 安全原生目标打开 |
crates/sonicterm-app/src/app/path_target.rs、crates/sonicterm-app/src/app/path_target/{unix,macos,linux,windows}.rs、crates/sonicterm-cfg/src/url_open/{macos,linux,windows}.rs
|
| macOS 入口/菜单/打开文档/标签页交接 | crates/sonicterm-mac/src/{main,menubar,open_documents,os_drag_mac,tab_drag_os}.rs |
| macOS 显示链接 | crates/sonicterm-app/src/app/display_link.rs |
| Windows 入口/CLI/菜单/backdrop/标签页拖放 | crates/sonicterm-windows/src/{main,cli,startup,menubar,backdrop,os_drag_win,tab_drag_os}.rs |
| Windows 软件呈现 |
crates/sonicterm-gpu/src/{software_frame,software_windows}.rs、crates/sonicterm-windows/src/software_presenter.rs
|
| Linux 入口与 identity |
crates/sonicterm-linux/src/main.rs、crates/sonicterm-linux/resources/
|
| 资源查找 | crates/sonicterm-cfg/src/assets.rs |