- 桌面应用
【免费下载链接】DankMaterialShell
Desktop shell for wayland compositors built with Quickshell & GO, optimized for niri, hyprland, sway, MangoWC, labwc, and MiracleWM.
本指南以 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 插件与宿主交互的全部接口如下表,缺一不可:
| 成员 | 类型 | 说明 |
|---|---|---|
pluginService | property | 宿主注入的 PluginService 引用(声明为null,由框架注入) |
trigger | property | 激活插件的触发词字符串 |
itemsChanged | signal | 条目列表变化时发出,触发启动器 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/!即可。
十四、调试、验证与常见错误
验证清单与运行时调试:
- 用
jq . plugin.json检查清单语法(SKILL.md 建议的排查第一步); - 将插件放入
~/.config/DankMaterialShell/plugins/,在设置中触发 “Scan for Plugins” 扫描; - 启用插件后,打开启动器输入触发词测试条目显示与过滤;
- 运行时可通过 IPC 命令在不重启 shell 的情况下重扫与重载插件:
dms ipc plugin-scan scan(全量重扫)、dms ipc plugin-scan reload <id>(强制重载)、dms ipc plugin-scan status <id>(查看加载状态与错误); - 启动器组件实例化失败时,
PluginService会记录错误(PluginService.qml#L979-L990 的ensureLauncherInstance中对comp.errorString()做了日志输出)。
launcher 插件高频错误清单(提炼自 SKILL.md 的 Common Mistakes 章节):
- 用
PluginComponent而不是普通Item——launcher 基类必须是Item; - 条目缺少
categories字段——条目将无法显示; - 清单缺少
trigger——schema 校验直接失败;composite 插件含launcher表面时同样必填; - 不处理 null pluginService——始终使用
?.可选链或空值检查; - 同时提供
component与components——二者只能取其一; - 误用
globalThis.clipboard——QML 运行时没有浏览器 API,剪贴板用Quickshell.execDetached(["dms", "cl", "copy", text]); - 使用
import QtQuick时调用Quickshell.execDetached失败——Quickshell需要单独的import Quickshell; - 清单类型与表面不匹配——launcher 表面需要
"type": "launcher"或components中带launcher键; - 使用已弃用的
requires字段——应使用dependencies; - 有设置组件却未声明
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.
相关推荐
3步轻松掌握Umi-OCR:免费离线批量文字识别完美解决方案
3步轻松掌握Umi OCR:免费离线批量文字识别完美解决方案 您是否曾为从海量图片中手动提取文字而烦恼?无论是整理会议截图、处理扫描文档,还是收集网页资料,传统
OCR桌面应用SearXNG插件开发入门:创建自定义搜索功能的完整指南
SearXNG插件开发入门:创建自定义搜索功能的完整指南 引言:为什么需要自定义搜索插件? 在信息爆炸的时代,传统的搜索引擎往往无法满足特定场景下的搜索需求。S
后端搜索引擎Autocomplete插件开发终极指南:从零创建自定义搜索体验
Autocomplete插件开发终极指南:从零创建自定义搜索体验 Autocomplete是Algolia开发的一个快速、功能丰富的JavaScript自动补全
前端UI组件搜索引擎
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考