小类随手记

为 DMS + Wayfire 打造工作区插件

背景

在之前那篇 Wayland 窗口管理器选型 里,我对 Wayfire 的评价是"方向不对"。原因之一是 工作区对大部分 bar(如 Waybar、DMS)都不兼容,需要自己写插件/脚本实现

但吐槽归吐槽,实际需求摆在那里——手头确实有设备在跑 Wayfire,而 DMS(DankMaterialShell)又是我习惯的 bar/desktop shell。DMS 原生只内置了针对 Niri、Hyprland、Sway 和 dwl 的 WorkspaceSwitcher 控件,并不支持 Wayfire。

所以这篇文章就是填这个坑的完整记录——为 DMS 写一个 Wayfire 工作区插件

关于 DMS 本身:它是一个基于 Quickshell 框架的 Wayland 桌面 Shell,支持插件扩展。如果你刚接触 DMS,可以参考之前的 DMS 配置笔记

方案选型

要实现工作区显示,需要解决两个问题:

  1. 如何从 Wayfire 获取工作区信息?
  2. 如何让 DMS 显示这些信息?

Wayfire 端:IPC 协议

Wayfire 的 ipc 插件提供了一个 Unix Socket(位于 $XDG_RUNTIME_DIR/wayfire-*.socket),采用 JSON-RPC + 长度前缀的消息格式。搭配 wayfire-plugins-extra 里的 ipc-rules 插件后,可以获取完整的输出(output)/ 视图(view)信息,并订阅工作区切换、视图映射/取消映射等事件

主要 IPC 方法:

方法用途
window-rules/list-outputs列出所有输出(显示器)及当前工作区位置
window-rules/list-views列出所有视图(窗口)
window-rules/events/watch订阅事件,实时推送变更
vswitch/set-workspace切换工作区

DMS 端:插件系统

DMS 的插件系统基于 PluginComponent,可以自定义 bar 上的 widget。核心属性:

  • horizontalBarPill / verticalBarPill — bar 上显示的组件
  • popoutContent — 弹出菜单组件
  • pluginData — 存储在 plugin_settings.json 中的用户配置

架构设计

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Wayfire Compositor
  ├── ipc (socket) ──→ wayfire-workspace-state (Python daemon)
  │                        ├── 订阅事件,跟踪视图→工作区映射
  │                        └── 写入 /tmp/wayfire-workspace-state.json
  └── vswitch ──→ wayfire-workspace-state (switch X Y)

DMS (Quickshell)
  └── WayfireWorkspace Plugin
        ├── Timer 轮询读取状态文件
        ├── Repeater 渲染工作区按钮
        └── 点击触发切换命令

Python 守护进程作为中间层,订阅 Wayfire 事件,将视图与工作区的对应关系维护在内存中,实时写入 JSON 状态文件。DMS 插件通过定时轮询读取该文件更新显示。

实现细节

Python 守护进程

主要逻辑在 wayfire-workspace-state 脚本中:

  • IPC 连接:通过环境变量 _WAYFIRE_SOCKET 或扫描 $XDG_RUNTIME_DIR 寻找 socket
  • 事件订阅:监听 wset-workspace-changedview-mappedview-unmappedview-workspace-changed 等事件
  • 视图跟踪:维护 views_per_workspace 字典,每次事件更新后重建工作区列表
  • 状态写入:原子写入(先写 .tmprename),避免 QML 读到半截数据

关键代码结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class WorkspaceState:
    def __init__(self):
        self.views_per_workspace = {}  # ws_idx → [view_id]
        self.current_x = 0
        self.current_y = 0
        self.grid_width = 5
        self.grid_height = 1

    def handle_event(self, event):
        if event == "wset-workspace-changed":
            # 更新当前工作区
        elif event == "view-mapped":
            # 新窗口出现在当前工作区
        elif event == "view-unmapped":
            # 窗口关闭
        elif event == "view-workspace-changed":
            # 窗口移动到其他工作区
        self._rebuild_workspace_list()
        self._write()

DMS QML 插件

WayfireWorkspace.qml 的结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
PluginComponent {
    id: root
    layerNamespacePlugin: "wayfire-workspace"

    // 定时轮询
    Timer {
        interval: root.pollInterval
        running: true
        repeat: true
        onTriggered: root.readState()
    }

    // 读取 JSON → 更新 workspaces 属性
    function readState() {
        Proc.runCommand(null, ["cat", root.statePath],
            function(output, exitCode) {
                // 解析 state.workspaces, state.current 等
            })
    }

    // Bar 显示:字母/数字按钮 + 颜色指示
    horizontalBarPill: Component {
        Row {
            Repeater {
                model: root.getDisplayWorkspaces()
                // 每个工作区一个按钮
            }
        }
    }

    // 弹出网格视图
    popoutContent: Component {
        PopoutComponent {
            // 完整工作区网格
        }
    }
}

这里有一个坑值得提:DMS 的 Proc.runCommandqs.Common.Proc 单例提供的方法,签名是 runCommand(id, commandList, callback),其中 id 用于去重。一开始我按直觉传了 runCommand("cat", [path], callback),结果 id 占用了第一个参数,实际执行的命令变成了 [path] 漏掉了 cat。这个调试花了一些时间。

Wayfire 配置

需要在 wayfire.ini 中启用 IPC 插件:

1
2
3
4
5
6
7
8
[core]
plugins = \
  ipc \
  ipc-rules \
  ...

[autostart]
wayfire_ws = /path/to/wayfire-workspace-state daemon

最终效果

  • Bar 上显示所有工作区编号,活跃工作区高亮,有窗口的工作区显示指示点
  • 点击编号直接切换到该工作区
  • 点击空白区域弹出完整工作区网格视图
  • 颜色、轮询间隔、显示模式均可通过 DMS 设置面板调整

项目地址

完整的插件源码已经开源在 GitHub:

👉 joyanhui/dms-ext-wayfire-workspace

包含:

  • WayfireWorkspace.qml / WayfireWorkspaceSettings.qml — DMS 插件
  • wayfire-workspace-state — Python 守护进程/CLI
  • README.md — 详细的使用说明(不依赖 NixOS)

总结

这个插件的开发过程典型地展示了 Wayland 生态中的"组合式创新"模式:

  1. Wayfire 提供了 IPC socket 和事件推送 — 这是基础设施
  2. Python 守护进程作为胶水层,将 IPC 事件转化为静态文件
  3. DMS 插件系统提供了 bar widget 的扩展点
  4. Quickshell 的 Proc API 让 QML 可以安全地执行外部命令

每个组件都在做自己擅长的事,没有谁被强行改造。这也是 Linux 桌面生态最吸引人的地方——当现成的工具不满足需求时,你可以把积木换一块,而不是换掉整个乐高套装。

至于 Wayfire 本身在 选型文章 里的评价,这个插件的存在并不会改变"方向不对"的结论——堆叠式管理器的天花板确实在那里。但它至少让 Wayfire 用户在使用 DMS 时少了一个痛点。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Theme Stack