09 · 菜单的数据模型¶
Omarchy 菜单看起来是一棵 QML UI,实际核心是一份 JSONC 数据图。显示层、纯 JS 模型、guard 批处理和 IPC wrapper 各自负责一段清晰的工作。
数据从哪里来¶
flowchart LR
D[default/omarchy/omarchy-menu.jsonc] --> Parse[MenuModel.js]
U[用户 extensions/omarchy-menu.jsonc] --> Parse
Parse --> Merge[按 entry id 字段合并]
Merge --> Guards[批量 guard 结果]
Guards --> UI[Menu.qml]
Provider[apps/fonts/power profiles] --> UI
两份文件在 Shell 启动时解析,并由 FileView 监视。快捷键 → IPC → 可见菜单的热路径不需要 fork shell 去重新解析 JSON。
dotted ID 就是树¶
trigger.share.file 是 trigger.share 的子项。没有单独的 parent 字段需要同步;ID 自带层级。
kind 也由字段推断:
- 有
action:可执行叶子; - 有
target:指向另一个 submenu 的链接; - 两者都无:submenu。
这样的 schema 避免 kind 与真实字段矛盾,但也意味着 action/target 同时出现属于需要测试防止的模糊输入。
用户覆盖是字段级的¶
同 ID 的用户 entry 只替换明确声明的字段,保留默认 entry 的其他字段和原位置;新 ID 追加。
例如只改标签:
不会要求重复 action、icon 和 guard。这比整对象替换更适合菜单,因为用户常只想修一两个可见属性。
JSONC 不是完整 JSONC parser¶
实现只移除整行 // 注释并接受 trailing comma;行尾 inline comment 会导致解析失败。坏掉的用户 extension 整体贡献零 entry,但 shipped defaults 继续工作。
这是一种故障隔离选择:宁可丢掉用户扩展,也不让主菜单消失。代价是错误可能显得“静默”,开发扩展时应看 Shell 日志与测试。
guard:可见、选中与禁用¶
三个字段都是 Bash 条件,但 UI 语义不同:
| guard | false 时 | 适合 |
|---|---|---|
when |
隐藏 entry | Remove 中隐藏未安装项 |
checked |
不显示 ✓ | 当前默认浏览器/DNS/channel |
disabled |
true 时变暗、打 ✓、不可选 | Install 中表示已经安装 |
Install 使用 disabled 而不是 when,是产品语义:列表仍是 Omarchy 可安装软件的目录,已安装项不会凭空消失。Remove 则只展示实际可移除项。
为什么 guard 要批量执行¶
菜单可能有大量 package/command 条件。每行 fork 一次 Bash/pacman 会让打开菜单明显卡顿。实现把所有 guard 合并到一个 Bash 进程:
- 菜单立即用上一次结果打开;
- 后台批次求值;
- package presence 从一次
pacman -Q快照回答; - 多行重复的 reader 命令只运行一次;
- 新结果更新 UI 缓存。
sequenceDiagram
participant U as 用户
participant M as Menu.qml
participant C as guard cache
participant B as 单个 Bash batch
U->>M: 打开菜单
M->>C: 读取上次结果
M-->>U: 立即显示
M->>B: 所有 when/checked/disabled
B-->>M: id:type:0|1
M-->>U: 更新可见/选中状态
UI 最多在 batch 运行时间内显示旧状态,但不会让 batch 延迟第一次绘制。GUARD_READERS 显式列出可共享的读命令,测试会发现新增的重复 reader 未加入优化表。
providers:运行时行源¶
静态 JSONC 无法枚举随机器变化的应用和字体,因此 submenu 可以声明 provider:
apps:直接使用共享 AppLibrary 的 desktop entries;fonts:Bash 返回 tab 分隔行;power-profiles:同样返回label\tvalue\tcurrent。
volatile provider 每次进入 submenu 重跑,但不会在每个搜索按键上重跑。provider rows 带来源 submenu ID;刷新时只替换旧 provider batch,不扰动静态 children。
App provider 的 entry 可搜索但不可充当菜单 route,防止 desktop Keywords(例如 system)抢占 system 菜单路径。
CLI 与 route 解析¶
omarchy-menu 是标准插件 IPC 的薄 wrapper:
omarchy menu
omarchy menu toggle system
omarchy menu summon style.theme
omarchy menu close
omarchy menu refresh
route 大小写不敏感,underscore 归一化为 dash;精确 ID 优先于 alias。route 若解析到 action 会直接执行,而不是打开一个没有 children 的菜单。
alias 只用于已建立的兼容名称,不建议给新 entry 随意添加;搜索已经覆盖 label、末段 ID 与 description。
select/input 模式的跨进程握手¶
omarchy-menu-select 和 omarchy-menu-input 把同一个菜单当系统 dmenu。CLI 创建 selectionFile 和 doneFile,通过 IPC 传给 Shell,然后阻塞等待:
sequenceDiagram
participant C as CLI process
participant S as Shell menu
participant F as 临时文件
C->>F: 创建 selection/done 路径
C->>S: summon mode + paths
S-->>C: 展示 UI
S->>F: 写 selection,touch done
C->>F: 读结果并清理
取消返回 exit 1。这个文件握手把异步 UI 变成传统 shell 可组合的同步命令,同时不要求 CLI 与 QML 共享复杂 RPC 状态。
关键源码索引¶
default/omarchy/omarchy-menu.jsonc— shipped menu datashell/plugins/menu/MenuModel.js— parse、merge、guard 与 provider 纯逻辑shell/plugins/menu/Menu.qml— 渲染、provider 与 IPCbin/omarchy-menu— route wrapperbin/omarchy-menu-select— selection handshaketest/shell.d/menu-guards-test.sh— 批量 guard 契约docs/menu.md— 上游菜单设计说明
下一章看另一条完整的数据编译链:10 · 主题编译流水线。