Seelen UI 插件机制详解:纯声明式 Plugin 资源的结构、路由原理与开发实战 📅 发布时间:2026/9/13 18:17:49 👁 浏览次数: Seelen UI 插件机制详解纯声明式 Plugin 资源的结构、路由原理与开发实战【免费下载链接】Seelen-UIThe Fully Customizable Desktop Environment for Windows 10/11.项目地址: https://gitcode.com/GitHub_Trending/se/Seelen-UI在 Seelen UI 中插件Plugin是一种纯声明式资源它只通过metadata.yml声明自己要注入数据的目标 widgettarget并携带一段自由格式的plugin数据载荷——真正的加载、解析与执行逻辑全部由目标 widget 自己拥有。本文以仓库中的 插件开发规范 为主体结合 Plugin 结构体源码、PluginValue 路由实现 与仓库自带的示例插件完整讲解插件资源的结构、核心层如何路由插件数据、内置 widget 各自的插件行为扩展 API以及从编写、加载到打包发布的完整工作流。读完后你可以独立编写一个面向工具栏、Dock 或平铺窗口管理器的插件资源。1. 插件是什么纯声明无运行时理解 Seelen UI 的插件机制首先要破除一个常见预期插件没有自己的逻辑。官方规范的原话是——插件merely plain, flat files, nothing more仅仅是平凡的扁平文件仅此而已。具体而言一个插件通过ID 指向一个 widget并向它提供任意数据widget 在运行时发现已安装的插件读取所有target指向自己的插件然后按自己的意愿使用这些数据Plugin这一资源类型不定义任何运行时、沙箱或执行模型。它只是一个信封id、target以及一个自由格式的plugin载荷不存在单数的插件系统。只有一个所有资源共享的插件信封而插件的行为数量等于愿意消费这个信封的 widget 数量。Seelen UI 核心从不以通用方式解析、校验或执行plugin的内容——它只把资源路由给target匹配的那个 widget。用一句话概括这套设计插件是按 widget 划分的扩展点。每个目标 widget 各自定义一套小型扩展 API——plugin里必须有什么schema、如何解析、如何执行在沙箱里eval一段 JS、遍历一棵声明式布局树、从一张查找表里取数据渲染……都由 widget 作者决定。规范文档举了三个典型形态工具栏插件用带 JS 回调定义一个新按钮平铺窗口管理器插件定义一棵纯静态布局树完全不含代码日历类 widget 的插件可能只是新增一个事件源。schema、解析与执行三件事全部位于目标 widget 内——既不在插件资源里也不在 Seelen UI 核心里。Seelen UI 自带的、接受插件的内置 widget各有专门指南指南目标 widget插件形态Toolbar Pluginsseelen/fancy-toolbarJS 脚本沙箱执行 声明式字段Dock Pluginsseelen/wegCanvas 绘制 / 自定义图标 脚本Window Manager Layoutsseelen/window-manager纯声明式布局树无脚本如果你自己在开发 widget 并希望支持插件你可以自由设计任何pluginschema——只需像上面三份指南为内置 widget 做的那样把它文档化给最终用户即可。2. 插件结构metadata.yml 逐字段说明一个插件资源的最小目录形态就是一个metadata.yml入口文件。规范给出的完整示例如下id: yourname/my-plugin metadata: displayName: My Plugin description: A short description of what this plugin adds. tags: - toolbar # Icon shown in the Seelen UI settings panel. # Must be a valid react-icons name (https://react-icons.github.io/react-icons/). # Defaults to PiPuzzlePieceDuotone if omitted. icon: PiPuzzlePieceDuotone # The widget this plugin is for target: someuser/some-widget # The plugin data — structure depends entirely on the target widget plugin: someField: someValue各字段说明字段必填说明id是插件资源 ID格式你的用户名/资源名。命名规则开头、用户名 3–32 位字母数字连字符、资源名至少 3 位见 Resource Guidelinesmetadata否展示在设置面板与市场中的元数据块。真正必填的子字段是displayName与descriptiontags等其余子字段可选icon否设置面板中显示的图标必须是一个合法的 react-icons 名称省略时默认为PiPuzzlePieceDuotonetarget是目标 widget 的资源 ID如someuser/some-widget核心据此路由插件数据plugin是插件数据载荷结构完全取决于目标 widget也就是说整个文件只有id、target、plugin三个字段是必需的。默认值这一点可以直接在源码中得到印证。Plugin 结构体 中icon字段的Default实现正是PiPuzzlePieceDuotone与文档描述完全一致pub struct Plugin { pub id: PluginId, pub metadata: ResourceMetadata, /// Optional icon to be used on settings. This have to be a valid react icon name. pub icon: String, #[serde(flatten)] pub plugin: PluginValue, } impl Default for Plugin { fn default() - Self { Self { id: Default::default(), metadata: Default::default(), icon: PiPuzzlePieceDuotone.to_string(), plugin: PluginValue::Any(Default::default()), } } }注意icon上注释与文档的呼应它必须是一个合法的 react-icons 名称。3.plugin字段自由格式数据与扩展 YAMLplugin字段是自由格式的——它可以是任意合法的 YAML 值映射、列表、字符串什么都行。资源层面没有任何 schema 约束目标 widget 原样接收它并负责校验和使用。你要知道它期望的结构只能去读目标 widget 的文档对内置 widget 就是前文列出的三份指南。在plugin块中可以使用扩展 YAML 的!include标签和资源文件其他位置一样。典型的工具栏插件会把 JS 片段拆到独立文件里再引入plugin: template: !include plugin/template.js tooltip: !include plugin/tooltip.js scopes: - Power关于!include的语义详见 Resource Guidelines 第 6 节路径相对于metadata.yml所在目录解析.scss/.sass文件会在插入前自动编译为 CSS其他文件一律作为纯文本插入——所以!include一个.js文件得到的就是一段字符串正好符合脚本字段是 JS 函数体字符串这一约定工具栏插件的template、tooltip、badge、onClick等都是这种字符串脚本由 widget 在沙箱中编译执行。此外metadata里的多语言字段可以用!extend把翻译拆到独立 YAML 文件!extend i18n/display_name.yml仓库内置插件正是这样组织的例如 tb_cpu_usage 插件。4. 核心层如何消费插件从源码看路由实现文档反复强调核心只做路由不做通用解析这一点在 PluginValue 实现 里可以看得非常清楚。Plugin.plugin在 Rust 侧是一个untagged 枚举按target字段优先尝试匹配三个内置 widget#[serde(untagged)] pub enum PluginValue { Known(BoxKnownPlugin), Any(ThirdPartyPlugin), } #[serde(tag target, content plugin)] pub enum KnownPlugin { #[serde(rename seelen/fancy-toolbar)] FacyToolbar(BoxToolbarItem), #[serde(rename seelen/window-manager)] WManager(BoxTwmPlugin), #[serde(rename seelen/weg)] Weg(BoxWegPluginItem), } pub struct ThirdPartyPlugin { /// The friendly id of the widget that will use this plugin target: WidgetId, /// The plugin data, this can be anything and depends on the widget using this plugin /// to handle it, parse it and use it. plugin: TsUnknown, }从源码结构看这实现了文档所述的两层行为内置 widget 走类型化解析。当target是seelen/fancy-toolbar、seelen/window-manager或seelen/weg时plugin载荷会被反序列化成对应的强类型结构ToolbarItem/TwmPlugin/WegPluginItem。也就是说这三个 widget 的 schema 在资源加载阶段就会被验证——schema 错配会在加载时暴露出来第三方 widget 走透传。target不匹配时落入ThirdPartyPlugin分支plugin载荷被存为TsUnknown任意值核心层完全不关心其结构等待目标 widget 在运行时自行解析。源码注释与规范文档的表述一致——this can be anything and depends on the widget using this plugin to handle it, parse it and use it。再结合 Plugin 结构体 上#[serde(flatten)]的标注target与plugin在 YAML 中与id、metadata、icon处于同一层级——这正是第 2 节示例中字段平铺的由来。而各 widget 对插件载荷的发现与使用则体现在它们各自的状态模块中例如 weg 插件项定义 与 twm 布局树定义。5. 同一个信封三种行为内置 widget 的插件 API规范文档的一个核心观点是行为由 widget 拥有而非插件拥有。以seelen/fancy-toolbar为例它的 ToolbarItem 载荷 长这样取自仓库自带的 通知中心工具栏插件id: seelen/tb-notifications target: seelen/fancy-toolbar plugin: scopes: - Notifications template: - return [ dndActive ? icon(TbZzz) : null, count 0 ? icon(MdNotificationsActive) : icon(MdOutlineNotifications), ] badge: return count 0 ? count : null tooltip: return [t(placeholder.notifications), : , count] onClickV2: |- trigger(seelen/notifications);要点完整字段与脚本作用域契约见 Toolbar Plugins 指南scopes与template是仅有的两个必填字段其余render、canvasSize、tooltip、badge、onClick/onClickV2、onWheelUp、onWheelDown、style、remoteData均可选除scopes外的脚本字段都是以字符串形式书写的 JS 函数体通常用!include从.js文件引入由工具栏 widget 在 JS 沙箱中编译执行而不是 JSON 或声明式对象onClickV2是onClick的遗留别名仓库中多个内置插件同时使用两种写法二者行为完全一致scopes决定了哪些系统数据被注入脚本作用域Notifications注入count/dndActiveCpu注入cores等等指南中给出了 17 个内置 scope 的完整对照表。Dockseelen/weg的插件形态不同默认render脚本是在256×256 画布上用CanvasRenderingContext2DAPI 画出自定义图标noCanvas: true时则改为返回一个自定义图标键名——详见 Dock Plugins 指南。而平铺窗口管理器seelen/window-manager的插件则完全没有脚本plugin是一棵由Leaf/Stack/Vertical/Horizontal节点构成的声明式布局树由 Rust 代码遍历求值节点支持growFactor、maxStackSize、stackPolicy、布尔condition等字段——详见 Window Manager Layouts 指南。这三种形态恰好印证了规范的第一节同一个Plugin信封在不同 widget 里可以变成沙箱 JS 作用域注入Canvas 绘制或纯声明式布局树而 Seelen UI 核心对这些差异一无所知、也不需要知道。6. 实战仓库内置插件剖析与完整开发工作流6.1 一个最小可用插件的完整形态仓库内置的 CPU 使用率工具栏插件 是一个很好的最小可用插件参考——它演示了!extend多语言元数据 !include引入 JS 的组合id: default/cpu-usage metadata: displayName: !extend i18n/display_name.yml icon: LuCpu target: seelen/fancy-toolbar plugin: scopes: - Cpu template: !include plugin/template.js其中template.js由 Toolbar Plugins 指南 摘录展示了Cpuscope 注入的cores变量的用法const totalUsage cores.reduce((total, core) total core.usage, 0); const used totalUsage / cores.length; return [icon(LuCpu), , used.toFixed(0) %];注意一个容易混淆的细节该文件同时携带target声明这是面向seelen/fancy-toolbar的 Plugin 资源与plugin载荷——这与第 4 节源码中tag target, content plugin的反序列化约定一一对应。6.2 加载、卸载与打包开发过程中无需把文件复制到任何目录用随 Seelen UI 提供的sluCLI 即可从任意目录直接加载要求Seelen UI 正在运行# 加载kind 取 theme/widget/plugin/icon-pack/wallpaper 之一 slu resource load plugin ./my-plugin/mod.yml # 卸载使用与加载时相同的路径 slu resource unload plugin ./my-plugin/mod.yml资源加载后立即注册无需重启应用即可在设置面板中使用。注意CLI 加载的资源只对当前会话有效重启 Seelen UI 后需要重新加载或通过设置面板永久安装。如果要发布到市场需要先打包成自包含文件——打包会解析所有!include/!extend引用并编译 SCSS在与资源目录同级位置生成.yamlslu resource bundle plugin ./my-plugin/mod.yml仅个人使用时不需要打包load/unload工作流即可。多语言文案可以用slu resource translate path/to/file.yml一键补全所有支持语言的翻译非英语源语言需显式传入语言码工作流细节见 Resource Guidelines。6.3 检查清单编写插件前可以对照以下几点自检id是否符合username/name命名规则且全局唯一target是否精确指向目标 widget 的资源 ID路由完全依赖它写错即插件永不生效plugin结构是否符合目标 widget的 schema——对内置 widget 查阅三份专门指南对第三方 widget 查阅该 widget 作者的文档脚本字段是否为字符串形式的 JS 函数体内容脚本需要return渲染值行为脚本返回值被忽略元数据字段displayName、description等用户可见文本是否包含en回退语言。7. 参考路径汇总内容路径插件规范本文主体documentation/plugin-guidelines.md资源通用规范ID、metadata、扩展 YAML、CLIdocumentation/resource-guidelines.md工具栏插件指南documentation/toolbar-plugins.mdDock 插件指南documentation/dock-plugins.md窗口管理器布局指南documentation/wm-layouts.mdPlugin 资源结构体与默认值libs/core/src/state/plugin/mod.rsPluginValue 路由实现内置 vs 第三方libs/core/src/state/plugin/value.rs内置示例CPU 使用率工具栏插件src/static/plugins/tb_cpu_usage/metadata.yml内置示例通知中心工具栏插件src/static/widgets/notifications/toolbar-plugin.yml【免费下载链接】Seelen-UIThe Fully Customizable Desktop Environment for Windows 10/11.项目地址: https://gitcode.com/GitHub_Trending/se/Seelen-UI创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考