news 2026/9/26 9:20:01

DankMaterialShell Launcher 插件开发指南:构建可搜索的自定义启动器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DankMaterialShell Launcher 插件开发指南:构建可搜索的自定义启动器
  • 桌面应用

【免费下载链接】DankMaterialShell

Desktop shell for wayland compositors built with Quickshell & GO, optimized for niri, hyprland, sway, MangoWC, labwc, and MiracleWM.

项目地址:https://gitcode.com/gh_mirrors/da/DankMaterialShell
点击查看免费下载

本指南以 DankMaterialShell(DMS)的 Launcher 插件体系为核心,讲解如何用 QML 为桌面启动器编写带触发词过滤、可搜索、可执行动作的扩展项,并完整覆盖项目结构、清单文件、图标类型、动作解析、状态持久化与设置界面等实战内容。读完本文,你将能够从零编写一个可发布到 DMS 插件目录的 launcher 型插件,并将其接入应用抽屉(app drawer)与启动器搜索结果。

一、Launcher 插件是什么

Launcher 插件是 DankMaterialShell 六种插件类型之一,它的职责是向 DMS 启动器(launcher)提供可搜索的条目(items)与可执行的动作(actions)。与在顶栏显示 pill 的 widget 插件、在后台静默运行的 daemon 插件不同,launcher 插件完全依赖“触发词 + 查询文本”的方式工作:用户在启动器中输入触发词(如#、=、!、img)后,插件条目才会出现,随后输入的内容会被当作查询参数传给插件做过滤。

从 SKILL.md 可知,DMS 插件统一发现自~/.config/DankMaterialShell/plugins/目录,launcher 插件的基本目录结构为:

~/.config/DankMaterialShell/plugins/YourPlugin/ plugin.json # 必需:清单文件(含触发词、组件路径等元数据) YourLauncher.qml # 必需:主 QML 组件(普通 Item) YourSettings.qml # 可选:设置界面 *.js # 可选:JavaScript 工具

与 widget/daemon 使用PluginComponent作为基类不同,launcher 插件使用普通Item——这是本类型最核心的差异点,也是新手最容易犯的错误(详见后文常见错误清单)。

在 PluginService.qml 的实现中可以看到 launcher 插件的注册与实例化链路:插件管理器维护pluginLauncherComponents注册表,ensureLauncherInstance(pluginId)负责按需实例化组件(PluginService.qml#L979-L1003),getLauncherPlugins()则把已加载且含 launcher 表面的插件聚合返回(PluginService.qml#L1192-L1203),并可通过requestLauncherUpdate(pluginId)信号请求刷新启动器内容(PluginService.qml#L57)。

二、最小可运行组件骨架

官方模板位于 .agents/skills/dms-plugin-dev/assets/templates/launcher/Launcher.qml,它给出了一个可直接复制改写的起点。launcher 插件基类是一个普通Item,必需的导入只有QtQuick与qs.Services:

import QtQuick import qs.Services Item { id: root property var pluginService: null property string trigger: "#" signal itemsChanged() function getItems(query) { // Return array of items return [] } function executeItem(item) { // Handle item selection } }

关于模板中的Component.onCompleted片段(trigger = pluginService.loadPluginData("myLauncher", "trigger", "#")),它会在组件加载完成时从插件设置中恢复用户自定义的触发词,保证插件重启后触发词不丢失。

必需接口一览

launcher 插件与宿主交互的全部接口如下表,缺一不可:

成员类型说明
pluginServiceproperty宿主注入的 PluginService 引用(声明为null,由框架注入)
triggerproperty激活插件的触发词字符串
itemsChangedsignal条目列表变化时发出,触发启动器 UI 刷新
getItems(query)function返回匹配查询的条目数组
executeItem(item)function处理条目被选中时的动作

其中pluginService若未声明或类型不符,注入会失败;SKILL.md 的常见错误清单第 2 条专门强调“缺失property var pluginService: null会导致注入失败”。

三、插件清单(plugin.json)与触发词约束

launcher 插件的plugin.json模板见 .agents/skills/dms-plugin-dev/assets/templates/launcher/plugin.json:

{ "id": "myLauncher", "name": "My Launcher", "description": "Custom launcher plugin with searchable items", "version": "1.0.0", "author": "Your Name", "type": "launcher", "capabilities": ["launcher"], "component": "./Launcher.qml", "trigger": "#", "icon": "search", "settings": "./Settings.qml", "permissions": ["settings_read", "settings_write"] }

结合 plugin-schema.json 与 plugin-manifest-reference.md,有几个对 launcher 型插件至关重要的校验规则:

  • trigger是硬性必填:schema 中的条件规则(allOf分支)明确规定,当type为launcher时,trigger字段必须存在;对于components中包含launcher键的 composite 插件同样适用(plugin-schema.json#L211-L236)。
  • id必须为 camelCase,匹配^[a-zA-Z][a-zA-Z0-9]*$;version必须为 semver 格式(如1.0.0)。
  • component路径必须以./开头、以.qml结尾。
  • 若插件带设置界面,permissions中必须声明settings_write,否则设置 UI 会报错。

真实的仓库示例 quickshell/PLUGINS/LauncherImageExample/plugin.json 展示了自定义字段的用法:它使用触发词"img",并额外携带"viewMode": "tile"与"viewModeEnforced": true两个字段(schema 允许additionalProperties,即插件可以携带自定义元数据)。

四、条目(Item)结构

getItems(query)返回的每个条目是一个纯 JavaScript 对象,字段约定如下:

{ name: "Item Display Name", // 必填:启动器中显示的名称 icon: "material:star", // 可选:图标规格 comment: "Description text", // 必填:副标题/描述文本 action: "type:data", // 必填:动作标识符(见“动作执行”一节) categories: ["MyPlugin"], // 必填:包含插件分类名的数组 imageUrl: "https://..." // 可选:tile 视图下显示的图片 }

其中categories在 SKILL.md 的常见错误清单第 8 条被特别点名:“忘记在 launcher 条目中写categories,条目将无法显示”。即使你的插件只服务一个分类,也必须在数组里给出至少一个分类名。

五、四种图标类型

icon字段的取值决定了启动器如何渲染图标,共四种形态:

1. Material Design 图标

{ icon: "material:lightbulb" } { icon: "material:terminal" } { icon: "material:translate" }

以material:为前缀,使用 Material Symbols Rounded 字体渲染,也是模板默认推荐的方式。

2. Unicode / Emoji 图标

{ icon: "unicode:smile_face" }

以unicode:为前缀,按图标尺寸的 70%~80% 渲染并跟随主题配色。

3. 桌面主题图标

{ icon: "firefox" } { icon: "folder" }

不带任何前缀,直接写图标名称,使用用户当前安装的桌面图标主题(icon theme)。

4. 无图标

省略icon字段即可。启动器会隐藏图标区域,把整行宽度让给条目名称,适合纯文本型条目。

六、触发词系统(Trigger System)

触发词决定了插件条目何时出现在启动器中,两种模式对比:

自定义触发词(仅在输入触发词后显示条目):

{ "trigger": "#" }

行为规则:

  • 单独输入#:显示该插件的全部条目;
  • 输入# query:用 query 过滤插件条目;
  • 传给getItems(query)的 query不含触发词前缀——触发词被剥离开后,剩余文本才是查询参数。

无触发词(条目始终与常规应用并列显示):

{ "trigger": "" }

即把触发词设为空字符串,插件条目常驻启动器,与普通应用混排。运行时把空触发词保存到插件数据的写法如下:

Component.onCompleted: { trigger = pluginService?.loadPluginData(pluginId, "trigger", "#") ?? "#" }

注意这里使用了可选链(?.)与空值合并(??)——SKILL.md 常见错误清单第 9 条明确要求“始终使用可选链或空值检查,不要假设 pluginService 一定非空”。加载逻辑的语义是:读取已保存的触发词,若不存在则回退到默认值#。

模板 Launcher.qml 中的写法与之对应:

Component.onCompleted: { if (pluginService) { trigger = pluginService.loadPluginData("myLauncher", "trigger", "#") } }

七、动作执行(Action Execution)

每个条目通过action字段描述“选中后做什么”,格式为type:data。executeItem(item)内解析动作字符串并分发:

function executeItem(item) { const actionParts = item.action.split(":") const actionType = actionParts[0] const actionData = actionParts.slice(1).join(":") switch (actionType) { case "toast": ToastService?.showInfo(actionData) break case "copy": Quickshell.execDetached(["dms", "cl", "copy", actionData]) ToastService?.showInfo("Copied to clipboard") break case "exec": Quickshell.execDetached(actionData.split(" ")) break case "url": Quickshell.execDetached(["xdg-open", actionData]) break default: console.warn("Unknown action type:", actionType) } }

解析要点:

  • action.split(":")以第一个冒号切分类型与数据,actionParts.slice(1).join(":")把剩余部分重新拼接,保证数据段自身包含冒号也不会被破坏(例如copy:https://example.com/a:b.png);
  • toast:经ToastService.showInfo弹出提示(ToastService来自qs.Services,同样使用可选链防御);
  • copy:通过Quickshell.execDetached调用dms cl copy命令写入剪贴板,并附带复制成功提示;
  • exec:把数据按空格拆分成参数数组后分离执行——注意这种拆分方式不支持带空格的单个参数,如需更稳健的参数传递应使用["sh", "-c", ...]形式(见 SKILL.md 的进程执行说明);
  • url:交给xdg-open打开链接;
  • 未知类型回退到console.warn告警。

这里还隐含一个来自 SKILL.md 的关键事实:QML 运行时不存在浏览器 JavaScript API,globalThis.clipboard不可用,剪贴板操作必须走Quickshell.execDetached(["dms", "cl", "copy", text])。

八、搜索与过滤(Search / Filtering)

getItems(query)收到的query是去掉触发词前缀后的用户搜索文本。典型实现是“空查询返回全部,非空查询做大小写不敏感的子串匹配”:

function getItems(query) { const allItems = [ { name: "Calculator", icon: "material:calculate", comment: "Open calculator", action: "exec:gnome-calculator", categories: ["Tools"] }, { name: "Terminal", icon: "material:terminal", comment: "Open terminal", action: "exec:alacritty", categories: ["Tools"] } ] if (!query || query.length === 0) return allItems const q = query.toLowerCase() return allItems.filter(item => item.name.toLowerCase().includes(q) || item.comment.toLowerCase().includes(q) ) }

过滤维度一般覆盖name与comment两个字段。模板 Launcher.qml 采用完全相同的模式(先判空、统一转小写、includes子串匹配)。这也是 SKILL.md 第 3 步给出的 launcher 组件示例的实现方式:items.filter(i => i.name.toLowerCase().includes(q))。

九、右键菜单动作(Context Menu Actions)

launcher 条目支持通过getContextMenuActions(item)提供右键菜单,返回值结构与条目动作一致:

function getContextMenuActions(item) { return [ { name: "Copy", icon: "material:content_copy", action: "copy:" + item.name }, { name: "Open in Browser", icon: "material:open_in_new", action: "url:" + item.url } ] }

要点:右键菜单动作与左键主动作共用同一个executeItem()处理器,所以getContextMenuActions返回的动作也必须遵循type:data格式,并且需要executeItem已支持对应的动作类型。

十、图片磁贴视图(Image Tile View)

对于以图片为核心的启动器(GIF 搜索、贴纸选择器等),可以把视图切换为磁贴网格模式,通过清单中的两个自定义字段控制:

{ "viewMode": "tile", "viewModeEnforced": true }

条目中改用imageUrl提供图片:

{ name: "Image Title", imageUrl: "https://example.com/image.png", comment: "Description", action: "copy:https://example.com/image.png", categories: ["MyPlugin"] }

仓库自带的 LauncherImageExample 就是这一模式的标准示范:触发词为"img",清单同时声明"viewMode": "tile"与"viewModeEnforced": true,把启动器强制锁定为图片网格。viewMode/viewModeEnforced属于 schema 允许的自定义附加字段(plugin-manifest-reference.md 的 “Additional Properties” 一节将其列为生产插件常见字段)。

十一、状态持久化(State Persistence)

对带持久状态的插件(便签、历史记录、收藏等),插件系统提供两套 API:

  • savePluginState(id, key, val)/loadPluginState(id, key, default):运行时数据(便签内容、历史、缓存),写入独立的 state 文件;
  • savePluginData(id, key, val)/loadPluginData(id, key, default):用户偏好与配置,写入 settings.json。

便签型插件的典型模式:

property var notes: [] Component.onCompleted: { const saved = pluginService?.loadPluginState(pluginId, "notes", []) if (saved) notes = saved } function addNote(text) { notes.push({ text: text, timestamp: Date.now() }) pluginService?.savePluginState(pluginId, "notes", notes) itemsChanged() }

这里的关键细节是:修改数据后必须发出itemsChanged()信号,否则启动器 UI 不会感知到条目列表变化。这与前面“必需接口”一节中itemsChanged的语义(触发 UI 刷新)相呼应。SKILL.md 还补充了持久化的第三层:PluginGlobalVar(仅运行时、跨实例共享,用于多显示器场景的同步),以及pluginData作为 PluginComponent 上的响应式属性自动从设置加载。

十二、触发词配置的设置界面

为了让用户自定义触发词(或切换为“始终可见”),插件应提供PluginSettings设置组件。模板 Settings.qml 完整演示了两种设置项:

import QtQuick import qs.Common import qs.Widgets import qs.Modules.Plugins PluginSettings { pluginId: "myLauncher" StringSetting { settingKey: "trigger" label: "Trigger" description: "Type this prefix in the launcher to activate the plugin" placeholder: "#" defaultValue: "#" } ToggleSetting { settingKey: "noTrigger" label: "Always Visible" description: "Show items alongside regular apps without needing a trigger" defaultValue: false } }

要点:

  • PluginSettings必须声明pluginId,与清单中的id一致;
  • 所有设置项自动保存、自动加载,无需手写读写逻辑;
  • StringSetting的settingKey对应loadPluginData(pluginId, "trigger", ...)中的 key;用户在设置界面保存新触发词后,主组件Component.onCompleted里的加载逻辑会读回新值;
  • ToggleSetting的noTrigger与运行时“把 trigger 置空实现常驻显示”的策略互相配合:UI 层给用户开关,运行时层把开关翻译成空触发词;
  • 前置条件:清单permissions必须包含settings_write,否则设置界面直接报错。

十三、完整实战示例:快速命令启动器

下面是一个完整的“快速命令”launcher 插件(与指南原例一致):触发词为!,提供锁定屏幕、截图、打开文件管理器三条命令,支持查询过滤、动作执行与触发词恢复:

import QtQuick import Quickshell import qs.Services Item { id: root property var pluginService: null property string trigger: "!" signal itemsChanged() property var commands: [ { name: "Lock Screen", icon: "material:lock", comment: "Lock the session", action: "exec:loginctl lock-session" }, { name: "Screenshot", icon: "material:screenshot_monitor", comment: "Take a screenshot", action: "exec:grim" }, { name: "File Manager", icon: "material:folder", comment: "Open file manager", action: "exec:nautilus" } ] function getItems(query) { if (!query) return commands const q = query.toLowerCase() return commands.filter(c => c.name.toLowerCase().includes(q) || c.comment.toLowerCase().includes(q) ) } function executeItem(item) { const [type, ...rest] = item.action.split(":") const data = rest.join(":") if (type === "exec") { Quickshell.execDetached(data.split(" ")) } } Component.onCompleted: { if (pluginService) { trigger = pluginService.loadPluginData("quickCommands", "trigger", "!") } } }

这段代码演示了解构式动作解析(const [type, ...rest] = item.action.split(":")),注意本示例只处理了exec类型,其他类型会被静默忽略——生产插件应像第七节那样提供完整的switch分发与default告警分支。

对应的plugin.json只需按第二节的模板把id、component、trigger等字段替换为quickCommands/./Launcher.qml/!即可。

十四、调试、验证与常见错误

验证清单与运行时调试:

  1. 用jq . plugin.json检查清单语法(SKILL.md 建议的排查第一步);
  2. 将插件放入~/.config/DankMaterialShell/plugins/,在设置中触发 “Scan for Plugins” 扫描;
  3. 启用插件后,打开启动器输入触发词测试条目显示与过滤;
  4. 运行时可通过 IPC 命令在不重启 shell 的情况下重扫与重载插件:dms ipc plugin-scan scan(全量重扫)、dms ipc plugin-scan reload <id>(强制重载)、dms ipc plugin-scan status <id>(查看加载状态与错误);
  5. 启动器组件实例化失败时,PluginService会记录错误(PluginService.qml#L979-L990 的ensureLauncherInstance中对comp.errorString()做了日志输出)。

launcher 插件高频错误清单(提炼自 SKILL.md 的 Common Mistakes 章节):

  1. 用PluginComponent而不是普通Item——launcher 基类必须是Item;
  2. 条目缺少categories字段——条目将无法显示;
  3. 清单缺少trigger——schema 校验直接失败;composite 插件含launcher表面时同样必填;
  4. 不处理 null pluginService——始终使用?.可选链或空值检查;
  5. 同时提供component与components——二者只能取其一;
  6. 误用globalThis.clipboard——QML 运行时没有浏览器 API,剪贴板用Quickshell.execDetached(["dms", "cl", "copy", text]);
  7. 使用import QtQuick时调用Quickshell.execDetached失败——Quickshell需要单独的import Quickshell;
  8. 清单类型与表面不匹配——launcher 表面需要"type": "launcher"或components中带launcher键;
  9. 使用已弃用的requires字段——应使用dependencies;
  10. 有设置组件却未声明settings_write权限——设置 UI 会显示错误。

十五、小结

Launcher 插件是 DMS 插件体系中唯一以“普通Item+ 触发词 + 查询过滤”为核心交互模型的类型。本指南覆盖了从最小骨架、清单约束、条目结构、四种图标、触发词语义、动作解析、搜索过滤、右键菜单、图片磁贴、状态持久化到设置界面与调试排错的完整链路。直接复用 launcher 模板目录 的三个文件(Launcher.qml、Settings.qml、plugin.json)即可快速起步,参考 LauncherImageExample 可学习磁贴视图等进阶形态;完整的清单字段与 schema 校验规则见 plugin-manifest-reference.md 与 plugin-schema.json,插件系统的服务端实现可深入阅读 PluginService.qml。

  • 桌面应用

【免费下载链接】DankMaterialShell

Desktop shell for wayland compositors built with Quickshell & GO, optimized for niri, hyprland, sway, MangoWC, labwc, and MiracleWM.

项目地址:https://gitcode.com/gh_mirrors/da/DankMaterialShell
点击查看免费下载

相关推荐

上一篇:ADK Python 模型容错实战:用 FallbackModel 构建跨模型故障转移的可靠 Agent
下一篇:Apache Iceberg Go 0.5.0 发布:V3 表规范、视图支持与删除文件能力全面落地

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

Substrate区块链开发框架实战解析:从Runtime到Pallet

Substrate这个名字&#xff0c;在技术圈里其实撞了非常多的车——生物化学里它是酶作用的底物&#xff0c;材料科学里它是承载薄膜的衬底&#xff0c;但在区块链开发这个语境下&#xff0c;它特指Polkadot生态那套模块化区块链开发框架。简单说&#xff0c;它能让你不写P2P网络…

作者头像 李华
网站建设 2026/9/26 9:19:13

STM32在机器人通信中的实时兜底作用与Linux协同实践

1. 为什么“会聊天的机器人”离不开一颗 STM32&#xff1f;你肯定见过这样的场景&#xff1a;公司群里突然弹出一条消息——“今日温度26℃&#xff0c;湿度65%&#xff0c;建议开窗通风”&#xff0c;底下还跟着一个自动打卡成功的截图&#xff1b;或者深夜调试代码时&#xf…

作者头像 李华
网站建设 2026/9/26 9:19:05

STM32CubeMX 6.14安装配置全指南:USB CDC、LPUART低功耗与TrustZone实战

1. 项目概述&#xff1a;为什么STM32CubeMX 6.14值得你花整整两小时认真走一遍STM32CubeMX 6.14不是一次普通的小版本更新&#xff0c;它是ST官方在2024年中旬释放的一次关键性迭代&#xff0c;直接关系到你后续半年内做STM32项目时的开发效率、外设配置准确率和HAL库兼容性。我…

作者头像 李华
网站建设 2026/9/26 9:17:44

Atlas 300V 24G部署YOLO全攻略:从环境配置到推理调优

我印象里最开始关注到“atlas”这个词&#xff0c;是因为在社区里刷到有人问“Atlas 300V 24G 是运算加速卡吗”&#xff0c;紧接着又看到一串“atlas部署yolo”的讨论。说实话&#xff0c;这块卡在AI推理领域的定位&#xff0c;确实很多人第一眼会当成显卡&#xff0c;国内普通…

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

使用PHP连接MySQL数据库的多种方及错误处理

以下是使用PHP连接MySQL数据库的详细指南&#xff0c;涵盖多种方法、错误处理及最佳实践&#xff0c;供不同需求的开发者参考&#xff1a;一、连接MySQL的常见方法PHP支持多种方式连接MySQL数据库&#xff0c;主要推荐使用 MySQLi扩展&#xff08;面向MySQL优化&#xff09;和 …

作者头像 李华
网站建设 2026/9/26 9:17:05

Atlas 300V 24G推理加速卡部署YOLO:完整实操与踩坑总结

最近后台一直有人问我一个特别具体的问题&#xff1a;"Atlas 300V 24G 是运算加速卡吗"、"Atlas 上能不能部署 YOLO"。说实话&#xff0c;这俩问题问的人太多了&#xff0c;而且我发现不少人对昇腾这个生态还是存在一些误解&#xff0c;总把它跟普通 GPU 卡…

作者头像 李华