跳转至

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 追加。

例如只改标签:

{
  "style.theme": {
    "label": "我的主题"
  }
}

不会要求重复 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 进程:

  1. 菜单立即用上一次结果打开;
  2. 后台批次求值;
  3. package presence 从一次 pacman -Q 快照回答;
  4. 多行重复的 reader 命令只运行一次;
  5. 新结果更新 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 状态。

关键源码索引

下一章看另一条完整的数据编译链:10 · 主题编译流水线。