news 2026/9/11 14:08:34

niri IPC 协议详解:通过 `niri msg` 与事件流构建 Wayland 合成器生态集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
niri IPC 协议详解:通过 `niri msg` 与事件流构建 Wayland 合成器生态集成

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

运行该命令可以看到所有可用的子命令。核心能力分为两大类:

  • 查询类outputsworkspaceswindowslayerskeyboard-layoutsfocused-outputfocused-windowoverview-statecastsversion等,读取合成器当前状态。
  • 操作类action子命令下挂载了完整的动作列表(聚焦/移动窗口、切换工作区、截屏、开关 Overview、修改输出配置等),以及output子命令用于临时改变输出配置。

从源码看,命令与请求类型的映射位于 src/ipc/client.rs:niri msg outputs对应Request::Outputsniri msg action ...对应Request::Action(action)niri msg本质上是niri-ipccrate 中Socket助手的一个薄封装——它在niri-ipcclapfeature 支持下直接把命令行参数解析成对应的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

--jsonniri 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.rsReply = Result<Response, String>,见 niri-ipc/src/lib.rs)。请求成功时是{"Ok": ...},失败时是{"Err": "错误信息"},脚本需要先解开这一层再取数据。

2.1 版本输出与 CLI/合成器版本一致性检查

一个值得一提的细节是niri msg --json version的返回结构与其他命令不同,它会返回一个包含compositorcli两个字段的 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键盘布局配置变化 / 当前布局切换
其他OverviewOpenedOrClosedOverview 开关
其他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 也会逐个处理,请求之间时间照常流逝,因此同一批发出的WorkspacesWindows请求可能返回不一致的快照(见 niri-ipc/src/lib.rs)。动作类请求同理——例如同时发送FocusWindowCloseWindow { id: None },可能因为中间焦点变化而关错窗口。这也是推荐用事件流而非"批量查询"来维护一致状态的原因。

4. 程序化访问:直接读写 IPC socket

niri msg --json只是"对 socket 读写的一层薄封装"。文档明确建议:当实现更复杂的脚本和模块时,鼓励直接访问 socket。这既适用于追求异步的 Rust 程序,也适用于任何支持 UNIX domain socket + JSON 的编程语言。

4.1 协议线格式(wire format)

协议定义非常简洁,四条规则即可描述:

  1. 通过环境变量$NIRI_SOCKET找到 socket 的文件系统路径,并连接它。
  2. 把请求编码为 JSON,写成一行,后面跟一个换行符;或者写完一行后 flush 并 shutdown 连接的写端。
  3. 读取响应:同样是一行 JSON。
  4. 之后可以继续逐行写请求、逐行读响应;一旦请求了事件流,niri 就开始持续逐行输出事件。

对应的服务端解析逻辑在 src/ipc/server.rs:handle_clientread_until(b'\n')逐行读取请求,serde_json::from_slice解析,处理后把响应序列化并以换行结尾写回。niri-ipcSocket助手(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 序列化的结构定义。请求类型一览:

请求对应命令说明
Versionversion运行中 niri 实例的版本字符串
Outputsoutputs已连接输出(以输出名为 key 的映射)
Workspacesworkspaces工作区列表
Windowswindows打开窗口列表
Layerslayerslayer-shell 表面列表
KeyboardLayoutskeyboard-layouts已配置键盘布局
FocusedOutputfocused-output焦点输出(可能为null
FocusedWindowfocused-window焦点窗口(可能为null
PickWindowpick-window用鼠标拾取窗口并返回其信息
PickColorpick-color用鼠标从屏幕拾取颜色
Action(Action)action ...执行一个动作
Output { output, action }output ...临时修改输出配置
EventStreamevent-stream开启事件流
ReturnErrorrequest-error返回一个错误(用于测试错误处理)
OverviewStateoverview-stateOverview 开关状态
Castscasts当前录制会话列表

5.1 Action 动作族:通过niri msg action触发

Action枚举覆盖了平铺窗口管理、工作区/监视器导航、布局控制、截屏、录制、配置重载等大量操作(见 niri-ipc/src/lib.rs)。从源码结构看,它们大致可分为:

  • 焦点管理FocusWindowFocusColumn*FocusWorkspace*FocusMonitor*FocusWindowInColumnFocusFloating/FocusTiling等。
  • 窗口移动MoveWindowUp/DownMoveWindowToWorkspace*MoveWindowToMonitor*ConsumeOrExpelWindowLeft/RightSwapWindowLeft/Right等。
  • 布局与尺寸SetWindowWidth/HeightSetColumnWidthSwitchPresetColumnWidthCenterColumnToggleColumnTabbedDisplay/SetColumnDisplayMaximizeColumn/MaximizeWindowToEdges等。
  • 工作区操作FocusWorkspaceMoveWorkspaceUp/DownMoveWorkspaceToIndexSetWorkspaceName/UnsetWorkspaceName等。
  • 浮窗与透明度ToggleWindowFloatingMoveWindowToFloating/TilingMoveFloatingWindowToggleWindowRuleOpacity
  • 截屏与录制ScreenshotScreenshotScreenScreenshotWindowSetDynamicCastWindow/MonitorClearDynamicCastTargetStopCast
  • 其他QuitSpawn/SpawnShPowerOffMonitors/PowerOnMonitorsToggleOverviewShowHotkeyOverlaySwitchLayoutLoadConfigFileDoScreenTransition等。

一个值得留意的细节是引用型参数:很多动作以"目标 id 可选、缺省作用于焦点对象"为约定。例如CloseWindow { id: Option<u64> }idNone时关闭焦点窗口;ScreenshotWindow同理。而工作区引用使用WorkspaceReferenceArg(见 niri-ipc/src/lib.rs),支持按Id、按Index(0–255)或按Name三种方式指定,其FromStr实现(niri-ipc/src/lib.rs)约定:命令行参数能解析为 0–255 的整数就按索引处理,否则按工作区名称处理。

SizeChangePositionChange的解析约定(见 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:设置模式(autoWxH@刷新率)。
  • CustomMode/Modeline:自定义模式与 VESA CVT modeline。
  • Scale:缩放(auto或数值)。
  • Transform:旋转/翻转(90180270flippedflipped-90等)。
  • Position:位置(autoset 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):idtitleapp_idpidworkspace_idis_focusedis_floatingis_urgentlayoutfocus_timestamp。其中id在窗口存活期间保持不变,但不要假设 id 一定从 1 开始或单调递增——这是实现细节,未来可能改为随机生成。
  • Workspace(niri-ipc/src/lib.rs):id(跨移动、跨输出恒定)、idx(在输出上的当前序号,会随重排变化)、nameoutputis_urgentis_activeis_focusedactive_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. 实战:一个最小的事件驱动状态栏数据源

综合以上内容,可以勾勒出一个事件驱动状态栏组件的标准实现路径:

  1. 读取$NIRI_SOCKET获取 socket 路径,建立连接。
  2. 发送单行 JSON:{"EventStream":{}}(即Request::EventStream)。
  3. 读取首条Handled响应,然后持续读取事件:先收到携带完整快照的WorkspacesChangedWindowsChangedKeyboardLayoutsChangedOverviewOpenedOrClosedConfigLoadedCastsChanged等(由replicate()产生),再收到后续增量事件。
  4. 在本地维护一份EventStreamState式的状态(工作区 map + 窗口 map + 键盘布局 + Overview + 配置 + 录制),用每个事件更新对应部分;对指向未知 id 的引用保持宽容(参考 niri-ipc/src/state.rs 中apply的实现模式)。
  5. 需要触发操作时另开一条连接发送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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 14:06:08

Dokku 进程管理完全指南:ps 插件详解与源码级原理剖析

Dokku 进程管理完全指南&#xff1a;ps 插件详解与源码级原理剖析 【免费下载链接】dokku A docker-powered PaaS that helps you build and manage the lifecycle of applications 项目地址: https://gitcode.com/GitHub_Trending/do/dokku 导读 本文是 Dokku&#xf…

作者头像 李华
网站建设 2026/9/11 14:06:04

MongoDB 与 Antithesis:网络模糊测试拓扑构建与测试编排实践指南

MongoDB 与 Antithesis&#xff1a;网络模糊测试拓扑构建与测试编排实践指南 【免费下载链接】mongo The MongoDB Database 项目地址: https://gitcode.com/GitHub_Trending/mo/mongo 导读 本文基于 MongoDB 官方仓库中的 docs/antithesis/README.md 编写&#xff0c;系…

作者头像 李华
网站建设 2026/9/11 14:04:28

软考中级考试科目全解析与备考指南

1. 软考中级考试概述计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;是我国IT行业最具权威性的专业技术资格认证之一。作为行业内的"硬通货"&#xff0c;软考证书不仅是专业能力的证明&#xff0c;更是职称评定、积分…

作者头像 李华
网站建设 2026/9/11 14:03:26

SPI通信协议详解:从基础原理到实战优化

1. SPI通信的本质&#xff1a;同步串行的主从对话 SPI&#xff08;Serial Peripheral Interface&#xff09;本质上是一种全双工、同步串行通信协议。我第一次接触SPI是在调试一个温湿度传感器时&#xff0c;当时被它简洁的四线制结构所吸引。与UART需要精确匹配波特率不同&…

作者头像 李华
网站建设 2026/9/11 14:03:04

嵌入式KWS静态审计:ARM Cortex-M上TinyML落地的工程可靠性保障

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华