niri IPC 协议详解:通过niri msg与事件流构建 Wayland 合成器生态集成
【免费下载链接】niriA scrollable-tiling Wayland compositor.项目地址: https://gitcode.com/GitHub_Trending/ni/niri
niri 是一个可滚动平铺(scrollable-tiling)的 Wayland 合成器,它通过一套基于 UNIX domain socket 的 JSON 行协议对外提供 IPC 接口,让状态栏、启动器、脚本和各类 Wayland 桌面组件能够实时查询合成器状态并触发操作。本文以 docs/wiki/IPC.md 为主干,结合niri-ipc子 crate 与src/ipc/服务端实现,系统讲解niri msg命令的用法、--json输出模式、事件流(Event Stream)机制、直接读写 socket 的程序化访问方式,以及 IPC 的向后兼容性约定,帮助你基于 niri 构建可靠的桌面集成工具。
1. IPC 概览:一句话即可调用的合成器接口
niri 的 IPC 设计原则是"简单直接":运行中的 niri 实例会在文件系统中暴露一个 UNIX domain socket,任何进程只要向这个 socket 写入一行 JSON 请求、再读回一行 JSON 响应,就能与合成器对话。命令行层面,这一切被封装为niri msg子命令。
niri msg --help运行该命令可以看到所有可用的子命令。核心能力分为两大类:
- 查询类:
outputs、workspaces、windows、layers、keyboard-layouts、focused-output、focused-window、overview-state、casts、version等,读取合成器当前状态。 - 操作类:
action子命令下挂载了完整的动作列表(聚焦/移动窗口、切换工作区、截屏、开关 Overview、修改输出配置等),以及output子命令用于临时改变输出配置。
从源码看,命令与请求类型的映射位于 src/ipc/client.rs:niri msg outputs对应Request::Outputs,niri msg action ...对应Request::Action(action)。niri msg本质上是niri-ipccrate 中Socket助手的一个薄封装——它在niri-ipc的clapfeature 支持下直接把命令行参数解析成对应的Request枚举(见 niri-ipc/src/lib.rs 中Action枚举的clap::Parser派生)。
1.1 一个典型的查询示例
niri msg focused-window输出示例(人类可读格式):
Window ID 12 (focused) Title: "Alacritty" App ID: "Alacritty" Is floating: no PID: 1234 Workspace ID: 6 Layout: Tile size: 800 x 1080 ...2.--json输出模式:脚本友好的稳定接口
默认情况下niri msg输出经过格式化的人类可读文本,适合终端直接查看。但对于脚本和程序,文档与源码都明确建议使用--json标志:
niri msg --json outputs--json让niri msg把响应以 JSON 形式原样打印出来,而不是格式化输出。例如niri msg --json focused-window会得到类似下面的单行 JSON:
{"Ok":{"FocusedWindow":{"id":12,"title":"Alacritty","app_id":"Alacritty","workspace_id":6,"is_focused":true}}}需要注意,带--json的响应外层包着一层Ok/Err,这是Reply类型的序列化结果(niri-ipc/src/lib.rs中Reply = Result<Response, String>,见 niri-ipc/src/lib.rs)。请求成功时是{"Ok": ...},失败时是{"Err": "错误信息"},脚本需要先解开这一层再取数据。
2.1 版本输出与 CLI/合成器版本一致性检查
一个值得一提的细节是niri msg --json version的返回结构与其他命令不同,它会返回一个包含compositor与cli两个字段的 JSON 对象(见 src/ipc/client.rs):
{"cli":"26.4.0","compositor":"26.4.0"}这也是 niri 在升级后诊断版本错配问题的关键依据。
[!TIP] 如果你在升级 niri 之后遇到
niri msg报解析错误(parsing errors),请先确认是否已经重启了 niri 合成器本身。你可能正在用新版niri msgCLI 去请求旧版合成器,或者反过来。niri msg在遇到解析类错误时会自动重新连接并请求一次version,然后打印 CLI 与合成器版本号帮助你定位问题(见 src/ipc/client.rs)。
3. 事件流(Event Stream):告别轮询的实时状态同步
自 niri 0.1.9 起,IPC 增加了一个特殊请求:事件流(Request::EventStream,对应命令行niri msg event-stream)。它与其他"一问一答"式请求有本质区别:
大多数 IPC 请求只返回一条响应,而事件流请求会让 niri 持续向这条连接推送事件,直到连接被关闭。
这对实现各种状态栏和指示器(bar、widget、indicator)非常有价值——它们可以在状态变化发生的瞬间得到通知,而无需持续轮询。
3.1 事件流的核心设计:先给全量快照,再推增量更新
事件流协议的设计目标是让客户端状态永不与 niri 失步(desync):
事件流会先一次性下发完整的当前状态,然后跟进该状态的后续更新。这样你的状态永远不会与 niri 不同步,也不需要再额外发起其他 IPC 信息请求。
在实现上,这个设计由 niri-ipc/src/state.rs 中的EventStreamStatePart::replicate()完成——当客户端请求事件流时,服务端会把EventStreamState中工作区、窗口、键盘布局、Overview、配置、录制会话六个部分各自的replicate()事件全部先发出去(见 src/ipc/server.rs),例如第一个工作区事件必然是携带完整工作区列表的WorkspacesChanged。之后 niri 才在状态变化时推送增量事件。
3.2 原子性说明与使用注意
文档明确指出:
在合理的情况下,事件流的状态更新是原子的,但并非总是如此。例如,一个窗口可能带有一个已被删除的工作区 id——如果对应的
workspaces-changed事件先于对应的window-changed事件到达,就会发生这种情况。
因此客户端代码必须对未知/缺失的引用持有宽容态度,例如工作区 id 指向不存在的窗口、或窗口引用了刚被删除的工作区,都属于协议允许出现的中间状态。EventStreamState结构体的注释也提示:状态的不同部分在每条事件之间并不保证完全一致(见 niri-ipc/src/state.rs)。
3.3 快速体验事件流
niri msg event-stream文档将其定位为"更像是调试用途"的命令。要看原始 JSON 事件,可以加--json:
niri msg --json event-stream该命令会持续打印形如{"WorkspacesChanged":{"workspaces":[...]}}、{"WindowOpenedOrChanged":{"window":{...}}}的单行 JSON,直到被 Ctrl-C 中断。也可以手动连接 niri socket 并请求事件流(见下一节)。
3.4 完整事件类型清单
niri-ipccrate 的Event枚举(见 niri-ipc/src/lib.rs)定义了全部事件,大致可分为以下几组:
| 分组 | 事件 | 含义 |
|---|---|---|
| 工作区 | WorkspacesChanged | 工作区整体配置变更(完整替换,缺失即被删除) |
| 工作区 | WorkspaceUrgencyChanged | 工作区紧急(urgent)状态变化 |
| 工作区 | WorkspaceActivated | 某个工作区在某输出上被激活(可能同时成为焦点) |
| 工作区 | WorkspaceActiveWindowChanged | 工作区上活动窗口变化 |
| 窗口 | WindowsChanged | 窗口整体配置变更(完整替换) |
| 窗口 | WindowOpenedOrChanged | 新窗口打开或既有窗口属性变化 |
| 窗口 | WindowClosed | 窗口关闭 |
| 窗口 | WindowFocusChanged | 窗口焦点变化(id 为null表示无焦点,例如 layer-shell 表面获得焦点时) |
| 窗口 | WindowFocusTimestampChanged | 最近聚焦时间戳变化(用于 Alt-Tab 式 MRU 切换) |
| 窗口 | WindowUrgencyChanged | 窗口紧急状态变化 |
| 窗口 | WindowLayoutsChanged | 一个或多个窗口的布局信息变化 |
| 键盘 | KeyboardLayoutsChanged/KeyboardLayoutSwitched | 键盘布局配置变化 / 当前布局切换 |
| 其他 | OverviewOpenedOrClosed | Overview 开关 |
| 其他 | ConfigLoaded | 配置加载(连接事件流时总会收到一次,表示最近一次加载尝试) |
| 其他 | ScreenshotCaptured | 截图完成 |
| 其他 | CastsChanged/CastStartedOrChanged/CastStopped | 录制(screencast)会话变化 |
3.5 事件流的服务端实现细节
从源码可以确认几个与使用体验直接相关的实现事实(见 src/ipc/server.rs):
- 请求事件流后,服务端不再处理该连接上的后续请求(见 niri-ipc/src/lib.rs)。如果你需要"边读事件边发请求",必须使用两个独立的 IPC socket 连接。
- 消费速度保护:每个事件流客户端有一个有界缓冲区,容量为
EVENT_STREAM_BUFFER_SIZE = 64(见 src/ipc/server.rs)。如果客户端读取事件的速度跟不上、缓冲区累积超过 64 条,niri 会主动断开该事件流客户端并记录一条 warning(见 src/ipc/server.rs)。这提醒我们:事件流客户端必须及时消费,不能长时间阻塞。 - 每次请求都会被单独处理:即使你一次向 socket 写入多个请求,niri 也会逐个处理,请求之间时间照常流逝,因此同一批发出的
Workspaces与Windows请求可能返回不一致的快照(见 niri-ipc/src/lib.rs)。动作类请求同理——例如同时发送FocusWindow和CloseWindow { id: None },可能因为中间焦点变化而关错窗口。这也是推荐用事件流而非"批量查询"来维护一致状态的原因。
4. 程序化访问:直接读写 IPC socket
niri msg --json只是"对 socket 读写的一层薄封装"。文档明确建议:当实现更复杂的脚本和模块时,鼓励直接访问 socket。这既适用于追求异步的 Rust 程序,也适用于任何支持 UNIX domain socket + JSON 的编程语言。
4.1 协议线格式(wire format)
协议定义非常简洁,四条规则即可描述:
- 通过环境变量
$NIRI_SOCKET找到 socket 的文件系统路径,并连接它。 - 把请求编码为 JSON,写成一行,后面跟一个换行符;或者写完一行后 flush 并 shutdown 连接的写端。
- 读取响应:同样是一行 JSON。
- 之后可以继续逐行写请求、逐行读响应;一旦请求了事件流,niri 就开始持续逐行输出事件。
对应的服务端解析逻辑在 src/ipc/server.rs:handle_client用read_until(b'\n')逐行读取请求,serde_json::from_slice解析,处理后把响应序列化并以换行结尾写回。niri-ipc的Socket助手(niri-ipc/src/socket.rs)实现了同样的线格式,其中SOCKET_PATH_ENV = "NIRI_SOCKET"常量即是文档所说的环境变量名。
4.2 用 socat 手工测试
niri msg之外,可以用socat直接与 niri 对话。最简单的一问一答:
$ socat STDIO "$NIRI_SOCKET" "FocusedWindow" {"Ok":{"FocusedWindow":{"id":12,"title":"t socat STDIO /run/u ~","app_id":"Alacritty","workspace_id":6,"is_focused":true}}}这里第一行"FocusedWindow"是我们写入的请求(Request::FocusedWindow的 JSON 序列化形式),第二行是 niri 的响应。可以看到响应确实是Ok/Err包裹的结构,与niri msg --json的输出一致。
对于更复杂的请求,可以用socat监听一个临时 socket,观察niri msg究竟发送了什么 JSON:
$ socat STDIO UNIX-LISTEN:temp.sock # 然后在另一个终端里执行: $ env NIRI_SOCKET=./temp.sock niri msg action focus-workspace 2 # 回到 socat 终端可以看到 niri msg 实际发出的请求: {"Action":{"FocusWorkspace":{"reference":{"Index":2}}}}这个技巧非常适合用来逆向学习某个action的 JSON 结构,或者为其他语言实现客户端时对照格式。
4.3 用niri-ipccrate 编写 Rust 客户端
Rust 项目可以直接依赖niri-ipccrate,使用其Socket助手完成阻塞式的通信(见 niri-ipc/src/socket.rs 的示例):
use niri_ipc::{Request, Response}; use niri_ipc::socket::Socket; fn main() -> std::io::Result<()> { let mut socket = Socket::connect()?; let reply = socket.send(Request::EventStream)?; if matches!(reply, Ok(Response::Handled)) { let mut read_event = socket.read_events(); while let Ok(event) = read_event() { println!("Received event: {event:?}"); } } Ok(()) }对于追求异步或使用其他语言的场景,直接按 4.1 节的线格式手工读写 socket 即可。Socket只是一个相当简单的助手,文档也如此说明。
4.4 重要使用注意:事件流与请求不能共用一条连接
一旦你请求了事件流,niri 就不再读取该连接上的后续请求,只会持续写事件。因此文档和 crate 文档都提醒:如需同时读写,请开两条 socket 连接。这与 3.5 节的服务端实现完全对应。
5. 请求与响应类型速览
所有可用的请求(Request)与响应(Response)类型定义在niri-ipccrate 中(见 niri-ipc/src/lib.rs),它们同时也是 JSON 序列化的结构定义。请求类型一览:
| 请求 | 对应命令 | 说明 |
|---|---|---|
Version | version | 运行中 niri 实例的版本字符串 |
Outputs | outputs | 已连接输出(以输出名为 key 的映射) |
Workspaces | workspaces | 工作区列表 |
Windows | windows | 打开窗口列表 |
Layers | layers | layer-shell 表面列表 |
KeyboardLayouts | keyboard-layouts | 已配置键盘布局 |
FocusedOutput | focused-output | 焦点输出(可能为null) |
FocusedWindow | focused-window | 焦点窗口(可能为null) |
PickWindow | pick-window | 用鼠标拾取窗口并返回其信息 |
PickColor | pick-color | 用鼠标从屏幕拾取颜色 |
Action(Action) | action ... | 执行一个动作 |
Output { output, action } | output ... | 临时修改输出配置 |
EventStream | event-stream | 开启事件流 |
ReturnError | (request-error) | 返回一个错误(用于测试错误处理) |
OverviewState | overview-state | Overview 开关状态 |
Casts | casts | 当前录制会话列表 |
5.1 Action 动作族:通过niri msg action触发
Action枚举覆盖了平铺窗口管理、工作区/监视器导航、布局控制、截屏、录制、配置重载等大量操作(见 niri-ipc/src/lib.rs)。从源码结构看,它们大致可分为:
- 焦点管理:
FocusWindow、FocusColumn*、FocusWorkspace*、FocusMonitor*、FocusWindowInColumn、FocusFloating/FocusTiling等。 - 窗口移动:
MoveWindowUp/Down、MoveWindowToWorkspace*、MoveWindowToMonitor*、ConsumeOrExpelWindowLeft/Right、SwapWindowLeft/Right等。 - 布局与尺寸:
SetWindowWidth/Height、SetColumnWidth、SwitchPresetColumnWidth、CenterColumn、ToggleColumnTabbedDisplay/SetColumnDisplay、MaximizeColumn/MaximizeWindowToEdges等。 - 工作区操作:
FocusWorkspace、MoveWorkspaceUp/Down、MoveWorkspaceToIndex、SetWorkspaceName/UnsetWorkspaceName等。 - 浮窗与透明度:
ToggleWindowFloating、MoveWindowToFloating/Tiling、MoveFloatingWindow、ToggleWindowRuleOpacity。 - 截屏与录制:
Screenshot、ScreenshotScreen、ScreenshotWindow、SetDynamicCastWindow/Monitor、ClearDynamicCastTarget、StopCast。 - 其他:
Quit、Spawn/SpawnSh、PowerOffMonitors/PowerOnMonitors、ToggleOverview、ShowHotkeyOverlay、SwitchLayout、LoadConfigFile、DoScreenTransition等。
一个值得留意的细节是引用型参数:很多动作以"目标 id 可选、缺省作用于焦点对象"为约定。例如CloseWindow { id: Option<u64> }的id为None时关闭焦点窗口;ScreenshotWindow同理。而工作区引用使用WorkspaceReferenceArg(见 niri-ipc/src/lib.rs),支持按Id、按Index(0–255)或按Name三种方式指定,其FromStr实现(niri-ipc/src/lib.rs)约定:命令行参数能解析为 0–255 的整数就按索引处理,否则按工作区名称处理。
SizeChange与PositionChange的解析约定(见 niri-ipc/src/lib.rs 及单元测试)也值得一提:带%表示按工作区比例的尺寸/位置,带+/-前缀表示在现有基础上调整,无前缀表示设置为固定值。例如niri msg action set-column-width 50%会把列宽设为工作区宽度的 50%,+100则是在当前宽度上加 100 逻辑像素。
5.2 输出配置临时修改:niri msg output
output请求用于临时改变输出配置(不会写回配置文件;如果配置文件随后发生变化,这些临时修改会被遗忘),见 src/ipc/client.rs 与 niri-ipc/src/lib.rs。可用操作包括:
Off/On:关闭/打开输出(DPMS)。Mode:设置模式(auto或WxH@刷新率)。CustomMode/Modeline:自定义模式与 VESA CVT modeline。Scale:缩放(auto或数值)。Transform:旋转/翻转(90、180、270、flipped、flipped-90等)。Position:位置(auto或set x y)。Vrr:可变刷新率开关(on/off,支持on-demand)。MaxBpc:最大每通道位数(6/8/10/12/14/16)。
若目标输出当前未连接,响应为{"OutputConfigChanged":{"OutputWasMissing":{}}},niri 会在该输出接入时应用此配置(见 niri-ipc/src/lib.rs)。服务端还会对 modeline 与自定义模式做参数校验(如hdisplay < hsync_start < hsync_end < htotal等约束,见 niri-ipc/src/lib.rs),参数非法会返回Err。
5.3 响应数据结构要点
在编写解析代码时,几个关键数据结构值得注意(均定义于 niri-ipc/src/lib.rs):
Window(niri-ipc/src/lib.rs):id、title、app_id、pid、workspace_id、is_focused、is_floating、is_urgent、layout、focus_timestamp。其中id在窗口存活期间保持不变,但不要假设 id 一定从 1 开始或单调递增——这是实现细节,未来可能改为随机生成。Workspace(niri-ipc/src/lib.rs):id(跨移动、跨输出恒定)、idx(在输出上的当前序号,会随重排变化)、name、output、is_urgent、is_active、is_focused、active_window_id。想要唯一稳定标识,请使用id而非idx。WindowLayout(niri-ipc/src/lib.rs):同时提供平铺单元(tile)与窗口几何(window geometry)两套位置/尺寸信息;所有数值单位为逻辑像素(可为小数)。文档注释建议:做可视化展示时优先用 tile 属性(用户眼中"窗口"即 tile),窗口几何属性主要用于应用调试。Output/Mode/LogicalOutput(niri-ipc/src/lib.rs):输出名、厂商/型号/序列号、物理尺寸、可用模式列表(刷新率为毫赫兹)、当前模式索引、VRR 支持情况、逻辑坐标/缩放/变换、最大 bpc 等。运行niri msg outputs可查看可用模式。
6. 向后兼容性:脚本如何安全地跨版本演进
IPC 是 niri 对外部生态的稳定契约,文档明确给出了承诺:
JSON 输出应当保持稳定,具体来说:现有字段和枚举变体不应被重命名;非可选的现有字段不应被移除。
不过新字段和新的枚举变体仍会被添加,因此:
- 客户端应尽量优雅地处理未知字段或未知变体(例如 serde 默认忽略未知字段;枚举解析遇到未知变体时不要直接崩溃)。
- 人类可读输出(不加
--json)不被视为稳定接口。文档明确"保留对可读输出做任何修改的权利",因此脚本应优先使用 JSON 输出。
另外,niri-ipc子 crate在 Rust semver 意义上并不提供 API 稳定性——它与其他 niri 子 crate 一样跟随 niri 自身版本号。尤其是新的结构体字段与枚举变体会在 patch 版本中增加。crate 文档因此建议在依赖中使用精确版本号以避免意外破坏(见 niri-ipc/src/lib.rs):
[dependencies] niri-ipc = "=26.4.0"niri-ipccrate 还提供两个 feature:json-schema(为类型派生schemars::JsonSchema,可用于生成 JSON Schema 校验文档)和clap(为部分类型派生 CLI 解析 trait,niri 自身内部使用),见 niri-ipc/src/lib.rs。
7. 实战:一个最小的事件驱动状态栏数据源
综合以上内容,可以勾勒出一个事件驱动状态栏组件的标准实现路径:
- 读取
$NIRI_SOCKET获取 socket 路径,建立连接。 - 发送单行 JSON:
{"EventStream":{}}(即Request::EventStream)。 - 读取首条
Handled响应,然后持续读取事件:先收到携带完整快照的WorkspacesChanged、WindowsChanged、KeyboardLayoutsChanged、OverviewOpenedOrClosed、ConfigLoaded、CastsChanged等(由replicate()产生),再收到后续增量事件。 - 在本地维护一份
EventStreamState式的状态(工作区 map + 窗口 map + 键盘布局 + Overview + 配置 + 录制),用每个事件更新对应部分;对指向未知 id 的引用保持宽容(参考 niri-ipc/src/state.rs 中apply的实现模式)。 - 需要触发操作时另开一条连接发送
Action请求;保持事件流连接只读。
这样实现的状态栏可以做到:工作区激活/焦点变化、窗口开合与标题变化、布局变化、紧急状态、键盘布局切换、Overview 开关、配置重载、截图完成、录制会话变化,全部实时感知,且初始状态零误差。
参考资料
- 协议文档:docs/wiki/IPC.md
niri-ipccrate 源码:niri-ipc/src/lib.rs、niri-ipc/src/socket.rs、niri-ipc/src/state.rs- IPC 服务端实现:src/ipc/server.rs
niri msgCLI 客户端:src/ipc/client.rs、src/cli.rs
【免费下载链接】niriA scrollable-tiling Wayland compositor.项目地址: https://gitcode.com/GitHub_Trending/ni/niri
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考