Linux 托盘方案
托盘是 moonbit-libyue 平台差异最大的一块:Linux 没有可直接依赖的托盘运行库, 本项目为此用纯 MoonBit 实现了 StatusNotifierItem(SNI)协议栈,会话总线直连 面板,不依赖任何 AppIndicator 运行库。本文讲清方案的设计动机、架构分层、 后端降级与桌面兼容性;API 用法速查见 docs/components.md 的 「菜单 / 托盘」一节,真实桌面实测出的坑与验证结论统一记录在 docs/adaptation.md。
背景与约束
- Ubuntu 24.04 已移除传统
libappindicator3运行库;ayatana 分支装了也不被 libyue 认可,其探测列表只认libappindicator3。 - libyue 内置托盘在运行库加载失败时只打日志、对象静默失效,消费方拿不到 任何错误——这正是本方案要消灭的行为。
- 设计底线:后端缺失必须给出结构化结果(
Err(Unsupported)/is_supported() == false),绝不静默失效。
方案总览
Tray 统一 API(yue/tray.mbt) 消费方零平台代码
│ 后端选择与降级
├─ Sni(@traybus.Item) watcher 在线(Linux 主路径)
│ └─ yue/traybus/ 纯 MoonBit 协议栈
│ ├─ wire.mbt DBus 线路格式编解码(小端;对齐零点随作用域)
│ ├─ bus.mbt 会话总线:地址解析、SASL EXTERNAL 握手、Hello、
│ │ 消息收发分发(glib fd 监视接入 GTK 主循环)
│ ├─ sni.mbt SNI 属性/信号/Activate 分发 + DBusMenu 菜单
│ ├─ detect.mbt 桌面环境识别(XDG_CURRENT_DESKTOP,仅辅助诊断)
│ ├─ icon.mbt 程序内置生成托盘位图(32×32 月牙,ARGB 大端序)
│ └─ sys.mbt fd 级系统调用面(8 个,全部经 shim 转发)
└─ Native(NativeTray) 回退:libyue 原生 AppIndicatorshim 只转发 8 个 fd 级系统调用:unix connect / read / write / poll / close / watch_fd / getuid / getenv,非 Linux 平台为失败桩,traybus 据此优雅降级, 绝不触碰真实系统调用。除这 8 个入口外,托盘全链路(编解码、握手、协议状态机、 菜单模型)都在 MoonBit 层完成。
主循环接入
单进程一条会话总线连接,全部运行在主线程。sys_watch_fd 经 shim 挂到 glib fd 监视源接入 GTK 主循环,DBus 消息到达后按接口与方法名分发到 SNI / DBusMenu 处理器;应答经串号(pending map)回配。
后端选择与降级
Tray::new 按以下优先级选后端:
- SNI watcher 在线 → 纯 MoonBit 托盘。能连上会话总线只算候选,watcher 是否存在以注册时的
NameHasOwner(org.kde.StatusNotifierWatcher)为准; 注册成功即用@traybus.Item。失败(watcher 不在线等)继续向下回退。 - watcher 不在线 → 回退 nativeui AppIndicator(libyue 原生托盘;shim 侧已加空指针防御,探测失败不崩)。此路径仅作回退。
- 两者皆无 →
Err(Unsupported)。Tray::is_supported()同口径,消费方 可据此决定是否展示托盘相关功能。
desktop_environment() 返回识别出的桌面环境名(XFCE / GNOME / KDE…,识别不出 为 "unknown"),仅作诊断与降级提示;运行时真伪始终以 watcher 是否在线为准, 环境名不参与后端选择。
统一 API 与平台差异
消费方 API 三大平台完全一致:Tray::new / is_supported / set_title / set_icon / set_icon_name / set_tooltip / on_click / set_menu / remove。平台差异在库内 消化,对外的可见表现:
| 方法 | Linux SNI 后端 | Windows / macOS 原生后端 |
|---|---|---|
set_title | 支持(部分面板不渲染,属面板表现) | 同左,部分面板不渲染 |
set_icon | 空操作(PNG 像素解码未内置,保持现有图标) | 支持 |
set_icon_name | 支持,跟随系统主题(如 "utilities-terminal") | 空操作 |
set_tooltip | 支持 | 空操作 |
on_click | 面板 Activate 信号 | 图标点击 |
set_menu | 构建 DBusMenu + ContextMenu 自绘回退 | 原生 SetMenu |
Linux 图标说明:Tray::new 传入的图片路径只用于取文件名作托盘 Id,位图由 icon.mbt 程序内置生成(外圆与偏移挖空圆构成月牙,2×2 超采样抗锯齿,输出 SNI 规范要求的 ARGB 大端序),不依赖任何图片资源与解码器;要跟随系统主题 换图标用 set_icon_name。
菜单:set_menu 的两条通路
不同面板消费托盘菜单的方式不同,SNI 后端同时备好两条通路:
- 面板镜像 DBusMenu 渲染(XFCE 4.18 实测走这条):
set_menu遍历统一Menu模型的一级项与分隔线构建 DBusMenu(id = 数组下标 + 1,0 为根), 点击菜单项经MenuItem::Click触发原回调;子菜单暂不支持。DBusMenu 的AboutToShow恒回 false——回 true 会让面板把左键当菜单键、不再发 Activate(ksni 同款语义)。 - ContextMenu(x, y) → 应用自绘(Qt 模式):面板调
ContextMenu带图标 屏幕坐标,应用用Menu::popup_at(x, y)在该点弹出自己的菜单,回调经set_context_menu_handler注册。
XFCE 的 libdbusmenu 客户端只发批量版 EventGroup / AboutToShowGroup, 单条版会被 UnknownMethod 静默拒掉——traybus 单条、批量两组方法都有实现。 抓包定位过程见 docs/adaptation.md。
桌面环境兼容性
| 桌面环境 | 状态 | 说明 |
|---|---|---|
| XFCE 4.18 | ✅ 实测 | 图标、Activate 点击、右键菜单贯通 |
| KDE / MATE / Cinnamon / Budgie / LXQt | ❓ 待实测 | 协议层原生支持 SNI,预期可用 |
| GNOME + AppIndicator 扩展 | ❓ 待实测 | 协议层可用,待真机验证 |
| 纯净 GNOME | ❌ | 无托盘协议,Err(Unsupported) 属预期行为 |
| Windows 10 / 11 | ✅ 实测 | 原生 Shell_NotifyIconW 后端 |
| macOS | ❓ 未实测 | 原生后端 |
状态标记与 docs/adaptation.md 一致: ✅ 实测通过 / ⚠️ 部分可用或带条件 / ❌ 不可用 / ❓ 未实测。 逐一真机实测后,把版本号与差异按维护约定回写 adaptation.md 并更新本表。
调试与验证
- 协议互操作问题首选
dbus-monitor抓真实会话总线:判据与案例 (批量版信号、通知Notify未发出等)见 adaptation.md 的 「DBus 线路协议坑」「桌面环境」两节。 - 单测只证编解码自洽(对齐、签名、长度前缀等线路层规则有独立测试), 互操作结论必须来自真总线、真面板——这是仓库级验证规则,不是本模块 的可选要求。