# Plugin System Create widgets for DankBar and Control Center using dynamically-loaded QML components. ## Plugin Registry Browse and discover community plugins at **https://plugins.danklinux.com/** ## Overview Plugins let you add custom widgets to DankBar and Control Center. They're discovered from `~/.config/DankMaterialShell/plugins/` and managed via PluginService. ## Architecture ### Core Components 1. **PluginService** (`Services/PluginService.qml`) - Singleton service managing plugin lifecycle - Discovers plugins from `$CONFIGPATH/DankMaterialShell/plugins/` - Handles loading, unloading, and state management - Provides data persistence for plugin settings 2. **PluginsTab** (`Modules/Settings/PluginsTab.qml`) - UI for managing available plugins - Access plugin settings 3. **PluginsTab Settings** (`Modules/Settings/PluginsTab.qml`) - Accordion-style plugin configuration interface - Dynamically loads plugin settings components inline - Provides consistent settings interface with proper focus handling 4. **DankBar Integration** (`Modules/DankBar/DankBar.qml`) - Renders plugin widgets in the bar - Merges plugin components with built-in widgets - Supports left, center, and right sections - Supports any dankbar position (top/left/right/bottom) ## Plugin Structure Each plugin must be a directory in `$CONFIGPATH/DankMaterialShell/plugins/` containing: ``` $CONFIGPATH/DankMaterialShell/plugins/YourPlugin/ ├── plugin.json # Required: Plugin manifest ├── YourWidget.qml # Required: Widget component ├── YourSettings.qml # Optional: Settings UI └── *.js # Optional: JavaScript utilities ``` ### Plugin Manifest (plugin.json) The manifest file defines plugin metadata and configuration. **JSON Schema:** See `plugin-schema.json` for the complete specification and validation schema. ```json { "id": "yourPlugin", "name": "Your Plugin Name", "description": "Brief description of what your plugin does", "version": "1.0.0", "author": "Your Name", "type": "widget", "capabilities": ["thing-my-plugin-does"], "component": "./YourWidget.qml", "icon": "material_icon_name", "settings": "./YourSettings.qml", "requires_dms": ">=0.1.0", "requires": ["some-system-tool"], "permissions": [ "settings_read", "settings_write" ] } ``` **Required Fields:** - `id`: Unique plugin identifier (camelCase, no spaces) - `name`: Human-readable plugin name - `description`: Short description of plugin functionality (displayed in UI) - `version`: Semantic version string (e.g., "1.0.0") - `author`: Plugin creator name or email - `type`: Plugin type - "widget", "daemon", or "launcher" - `capabilities`: Array of plugin capabilities (e.g., ["dankbar-widget"], ["control-center"], ["monitoring"]) - `component`: Relative path to main QML component file **Required for Launcher Type:** - `trigger`: Trigger string for launcher activation (e.g., "=", "#", "!") **Optional Fields:** - `icon`: Material Design icon name (displayed in UI) - `settings`: Path to settings component (enables settings UI) - `requires_dms`: Minimum DMS version requirement (e.g., ">=0.1.18", ">0.1.0") - `requires`: Array of required system tools/dependencies (e.g., ["wl-copy", "curl"]) - `permissions`: Required DMS permissions (e.g., ["settings_read", "settings_write"]) **Permissions:** The plugin system enforces permissions when settings are accessed: - `settings_read`: Required to read plugin settings (currently not enforced) - `settings_write`: **Required** to use PluginSettings component and save settings If your plugin includes a settings component but doesn't declare `settings_write` permission, users will see an error message instead of the settings UI. ### Widget Component The main widget component uses the **PluginComponent** wrapper which provides automatic property injection and bar integration: ```qml import QtQuick import qs.Common import qs.Widgets import qs.Modules.Plugins PluginComponent { // Define horizontal bar pill, for top and bottom DankBar positions (optional) horizontalBarPill: Component { StyledRect { width: content.implicitWidth + Theme.spacingM * 2 height: parent.widgetThickness radius: Theme.cornerRadius color: Theme.surfaceContainerHigh StyledText { id: content anchors.centerIn: parent text: "Hello World" color: Theme.surfaceText font.pixelSize: Theme.fontSizeMedium } } } // Define vertical bar pill, for left and right DankBar positions (optional) verticalBarPill: Component { // Same as horizontal but optimized for vertical layout } // Define popout content, opens when clicking the bar pill (optional) popoutContent: Component { PopoutComponent { headerText: "My Plugin" detailsText: "Optional description text goes here" showCloseButton: true // Your popout content goes here Column { width: parent.width spacing: Theme.spacingM StyledText { text: "Popout Content" font.pixelSize: Theme.fontSizeLarge color: Theme.surfaceText } } } } // Popout dimensions (required if popoutContent is set) popoutWidth: 400 popoutHeight: 300 } ``` **PluginComponent Properties (automatically injected):** - `axis`: Bar axis information (horizontal/vertical) - `section`: Bar section ("left", "center", "right") - `parentScreen`: Screen reference for multi-monitor support - `widgetThickness`: Recommended widget size perpendicular to bar - `barThickness`: Bar thickness parallel to edge **Component Options:** - `horizontalBarPill`: Component shown in horizontal bars - `verticalBarPill`: Component shown in vertical bars - `popoutContent`: Optional popout window content - `popoutWidth`: Popout window width - `popoutHeight`: Popout window height - `pillClickAction`: Custom click handler function (overrides popout) ### Control Center Integration Add your plugin to Control Center by defining CC properties: ```qml PluginComponent { ccWidgetIcon: "toggle_on" ccWidgetPrimaryText: "My Feature" ccWidgetSecondaryText: isEnabled ? "Active" : "Inactive" ccWidgetIsActive: isEnabled onCcWidgetToggled: { isEnabled = !isEnabled if (pluginService) { pluginService.savePluginData("myPlugin", "isEnabled", isEnabled) } } ccDetailContent: Component { Rectangle { implicitHeight: 200 color: Theme.surfaceContainerHigh radius: Theme.cornerRadius // Your detail UI here } } horizontalBarPill: Component { /* ... */ } } ``` **CC Properties:** - `ccWidgetIcon`: Material icon name - `ccWidgetPrimaryText`: Main label - `ccWidgetSecondaryText`: Subtitle/status - `ccWidgetIsActive`: Active state styling - `ccDetailContent`: Optional dropdown panel (use for CompoundPill) **Signals:** - `ccWidgetToggled()`: Fired when icon clicked - `ccWidgetExpanded()`: Fired when expand area clicked (CompoundPill only) **Widget Sizing:** - 25% width → SmallToggleButton (icon only) - 50% width → ToggleButton (no detail) or CompoundPill (with detail) - Users can resize in edit mode **Custom Click Actions:** Override default popout with `pillClickAction`: ```qml pillClickAction: () => { Process.exec("bash", ["-c", "notify-send 'Clicked!'"]) } // Or with position params: (x, y, width, section, screen) pillClickAction: (x, y, width, section, screen) => { popoutService?.toggleControlCenter(x, y, width, section, screen) } ``` The PluginComponent automatically handles: - Bar orientation detection - Click handlers for popouts - Proper positioning and anchoring - Theme integration ### PopoutComponent PopoutComponent provides a consistent header/content layout for plugin popouts: ```qml import qs.Modules.Plugins PopoutComponent { headerText: "Header Title" // Main header text (bold, large) detailsText: "Description text" // Optional description (smaller, gray) showCloseButton: true // Show X button in top-right // Access to exposed properties for dynamic sizing readonly property int headerHeight // Height of header area readonly property int detailsHeight // Height of description area // Your content here - use parent.width for full width // Calculate available height: root.popoutHeight - headerHeight - detailsHeight - spacing DankGridView { width: parent.width height: parent.height // ... } } ``` **PopoutComponent Properties:** - `headerText`: Main header text (optional, hidden if empty) - `detailsText`: Description text below header (optional, hidden if empty) - `showCloseButton`: Show close button in header (default: false) - `closePopout`: Function to close popout (auto-injected by PluginPopout) - `headerHeight`: Readonly height of header (0 if not visible) - `detailsHeight`: Readonly height of description (0 if not visible) The component automatically handles spacing and layout. Content children are rendered below the description with proper padding. ### Settings Component Optional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components: ```qml import QtQuick import qs.Common import qs.Widgets import qs.Modules.Plugins PluginSettings { id: root pluginId: "yourPlugin" StringSetting { settingKey: "apiKey" label: "API Key" description: "Your API key for accessing the service" placeholder: "Enter API key..." } ToggleSetting { settingKey: "notifications" label: "Enable Notifications" description: "Show desktop notifications for updates" defaultValue: true } SelectionSetting { settingKey: "updateInterval" label: "Update Interval" description: "How often to refresh data" options: [ {label: "1 minute", value: "60"}, {label: "5 minutes", value: "300"}, {label: "15 minutes", value: "900"} ] defaultValue: "300" } ListSetting { id: itemList settingKey: "items" label: "Saved Items" description: "List of configured items" delegate: Component { StyledRect { width: parent.width height: 40 radius: Theme.cornerRadius color: Theme.surfaceContainerHigh StyledText { anchors.left: parent.left anchors.leftMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter text: modelData.name color: Theme.surfaceText } Rectangle { anchors.right: parent.right anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter width: 60 height: 28 color: removeArea.containsMouse ? Theme.errorHover : Theme.error radius: Theme.cornerRadius StyledText { anchors.centerIn: parent text: "Remove" color: Theme.errorText font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium } MouseArea { id: removeArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: itemList.removeItem(index) } } } } } } ``` **Available Setting Components:** All settings automatically save on change and load on component creation. **How Default Values Work:** Each setting component has a `defaultValue` property that is used when no saved value exists. Define sensible defaults in your settings UI: ```qml StringSetting { settingKey: "apiKey" defaultValue: "" // Empty string if no key saved } ToggleSetting { settingKey: "enabled" defaultValue: true // Enabled by default } ListSettingWithInput { settingKey: "locations" defaultValue: [] // Empty array if no locations saved } ``` 1. **PluginSettings** - Root wrapper for all plugin settings - `pluginId`: Your plugin ID (required) - Auto-handles storage and provides saveValue/loadValue to children - Place all other setting components inside this wrapper 2. **StringSetting** - Text input field - `settingKey`: Storage key (required) - `label`: Display label (required) - `description`: Help text (optional) - `placeholder`: Input placeholder (optional) - `defaultValue`: Default value (optional, default: `""`) - Layout: Vertical stack (label, description, input field) 3. **ToggleSetting** - Boolean toggle switch - `settingKey`: Storage key (required) - `label`: Display label (required) - `description`: Help text (optional) - `defaultValue`: Default boolean (optional, default: `false`) - Layout: Horizontal (label/description left, toggle right) 4. **SelectionSetting** - Dropdown menu - `settingKey`: Storage key (required) - `label`: Display label (required) - `description`: Help text (optional) - `options`: Array of `{label, value}` objects or simple strings (required) - `defaultValue`: Default value (optional, default: `""`) - Layout: Horizontal (label/description left, dropdown right) - Stores the `value` field, displays the `label` field 5. **ListSetting** - Manage list of items (manual add/remove) - `settingKey`: Storage key (required) - `label`: Display label (required) - `description`: Help text (optional) - `defaultValue`: Default array (optional, default: `[]`) - `delegate`: Custom item delegate Component (optional) - `addItem(item)`: Add item to list - `removeItem(index)`: Remove item from list - Use when you need custom UI for adding items 6. **ListSettingWithInput** - Complete list management with built-in form - `settingKey`: Storage key (required) - `label`: Display label (required) - `description`: Help text (optional) - `defaultValue`: Default array (optional, default: `[]`) - `fields`: Array of field definitions (required) - `id`: Field ID in saved object (required) - `label`: Column header text (required) - `placeholder`: Input placeholder (optional) - `width`: Column width in pixels (optional, default 200) - `required`: Must have value to add (optional, default false) - `default`: Default value if empty (optional) - Automatically generates: - Column headers from field labels - Input fields with placeholders - Add button with validation - List display showing all field values - Remove buttons for each item - Best for collecting structured data (servers, locations, etc.) **Complete Settings Example:** ```qml import QtQuick import qs.Common import qs.Widgets import qs.Modules.Plugins PluginSettings { pluginId: "myPlugin" StyledText { width: parent.width text: "General Settings" font.pixelSize: Theme.fontSizeLarge font.weight: Font.Bold color: Theme.surfaceText } StringSetting { settingKey: "apiKey" label: "API Key" description: "Your service API key" placeholder: "sk-..." defaultValue: "" } ToggleSetting { settingKey: "enabled" label: "Enable Feature" description: "Turn this feature on or off" defaultValue: true } SelectionSetting { settingKey: "theme" label: "Theme" description: "Choose your preferred theme" options: [ {label: "Dark", value: "dark"}, {label: "Light", value: "light"}, {label: "Auto", value: "auto"} ] defaultValue: "dark" } ListSettingWithInput { settingKey: "locations" label: "Locations" description: "Track multiple locations" defaultValue: [] fields: [ {id: "name", label: "Name", placeholder: "Home", width: 150, required: true}, {id: "timezone", label: "Timezone", placeholder: "America/New_York", width: 200, required: true} ] } } ``` **Key Benefits:** - Zero boilerplate - just define your settings - Automatic persistence to `settings.json` - Clean, consistent UI across all plugins - No manual `pluginService` calls needed - Proper layout and spacing handled automatically ## PluginService API ### Properties ```qml PluginService.pluginDirectory: string // Path to plugins directory ($CONFIGPATH/DankMaterialShell/plugins) PluginService.availablePlugins: object // Map of all discovered plugins {pluginId: pluginInfo} PluginService.loadedPlugins: object // Map of currently loaded plugins {pluginId: pluginInfo} PluginService.pluginWidgetComponents: object // Map of loaded widget components {pluginId: Component} ``` ### Functions ```qml // Plugin Management PluginService.loadPlugin(pluginId: string): bool PluginService.unloadPlugin(pluginId: string): bool PluginService.reloadPlugin(pluginId: string): bool PluginService.enablePlugin(pluginId: string): bool PluginService.disablePlugin(pluginId: string): bool // Plugin Discovery PluginService.scanPlugins(): void PluginService.getAvailablePlugins(): array PluginService.getLoadedPlugins(): array PluginService.isPluginLoaded(pluginId: string): bool PluginService.getWidgetComponents(): object // Data Persistence PluginService.savePluginData(pluginId: string, key: string, value: any): bool PluginService.loadPluginData(pluginId: string, key: string, defaultValue: any): any ``` ### Signals ```qml PluginService.pluginLoaded(pluginId: string) PluginService.pluginUnloaded(pluginId: string) PluginService.pluginLoadFailed(pluginId: string, error: string) ``` ## Creating a Plugin ### Step 1: Create Plugin Directory ```bash mkdir -p $CONFIGPATH/DankMaterialShell/plugins/MyPlugin cd $CONFIGPATH/DankMaterialShell/plugins/MyPlugin ``` ### Step 2: Create Manifest Create `plugin.json`: ```json { "id": "myPlugin", "name": "My Plugin", "description": "A sample plugin", "version": "1.0.0", "author": "Your Name", "type": "widget", "capabilities": ["my-functionality"], "component": "./MyWidget.qml", "icon": "extension", "settings": "./MySettings.qml", "requires_dms": ">=0.1.0", "permissions": ["settings_read", "settings_write"] } ``` ### Step 3: Create Widget Component Create `MyWidget.qml`: ```qml import QtQuick import qs.Common import qs.Widgets import qs.Modules.Plugins PluginComponent { horizontalBarPill: Component { StyledRect { width: textItem.implicitWidth + Theme.spacingM * 2 height: parent.widgetThickness radius: Theme.cornerRadius color: Theme.surfaceContainerHigh StyledText { id: textItem anchors.centerIn: parent text: "Hello World" color: Theme.surfaceText font.pixelSize: Theme.fontSizeMedium } } } verticalBarPill: Component { StyledRect { width: parent.widgetThickness height: textItem.implicitWidth + Theme.spacingM * 2 radius: Theme.cornerRadius color: Theme.surfaceContainerHigh StyledText { id: textItem anchors.centerIn: parent text: "Hello" color: Theme.surfaceText font.pixelSize: Theme.fontSizeSmall rotation: 90 } } } } ``` **Note:** Use `PluginComponent` wrapper for automatic property injection and bar integration. Define separate components for horizontal and vertical orientations. ### Step 4: Create Settings Component (Optional) Create `MySettings.qml`: ```qml import QtQuick import qs.Common import qs.Widgets import qs.Modules.Plugins PluginSettings { pluginId: "myPlugin" StyledText { width: parent.width text: "Configure your plugin settings" font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceVariantText wrapMode: Text.WordWrap } StringSetting { settingKey: "text" label: "Display Text" description: "Text shown in the bar widget" placeholder: "Hello World" defaultValue: "Hello World" } ToggleSetting { settingKey: "showIcon" label: "Show Icon" description: "Display an icon next to the text" defaultValue: true } } ``` ### Step 5: Enable Plugin 1. Run the shell: `qs -p $CONFIGPATH/quickshell/dms/shell.qml` 2. Open Settings (Ctrl+,) 3. Navigate to Plugins tab 4. Click "Scan for Plugins" 5. Enable your plugin with the toggle switch 6. Add the plugin to your DankBar configuration ## Adding Plugin to DankBar After enabling a plugin, add it to the bar: 1. Open Settings → Appearance → DankBar Layout 2. Add a new widget entry with your plugin ID 3. Choose section (left, center, right) 4. Save and reload Or edit `$CONFIGPATH/quickshell/dms/config.json`: ```json { "dankBarLeftWidgets": [ {"widgetId": "myPlugin", "enabled": true} ] } ``` ## Best Practices 1. **Use Existing Widgets**: Leverage `qs.Widgets` components (DankIcon, DankToggle, etc.) for consistency 2. **Follow Theme**: Use `Theme` singleton for colors, spacing, and fonts 3. **Data Persistence**: Use PluginService data APIs instead of manual file operations 4. **Error Handling**: Gracefully handle missing dependencies and invalid data 5. **Performance**: Keep widgets lightweight, avoid long operations that block the UI loop 6. **Responsive Design**: Adapt to `compactMode` and different screen sizes 7. **Documentation**: Include README.md explaining plugin usage 8. **Versioning**: Use semantic versioning for updates 9. **Dependencies**: Document external library requirements ## Clipboard Access Plugins that need to copy text to the clipboard **must** use the Wayland clipboard utility `wl-copy` through Quickshell's `execDetached` function. ### Correct Method Import Quickshell and use `execDetached` with `wl-copy`: ```qml import QtQuick import Quickshell Item { function copyToClipboard(text) { Quickshell.execDetached(["sh", "-c", "echo -n '" + text + "' | wl-copy"]) } } ``` ### Example Usage From the ExampleEmojiPlugin (EmojiWidget.qml:136): ```qml MouseArea { onClicked: { Quickshell.execDetached(["sh", "-c", "echo -n '" + modelData + "' | wl-copy"]) ToastService.showInfo("Copied " + modelData + " to clipboard") popoutColumn.closePopout() } } ``` ### Important Notes 1. **Do NOT** use `globalThis.clipboard` or similar JavaScript APIs - they don't exist in the QML runtime 2. **Always** import `Quickshell` at the top of your QML file 3. **Use** `echo -n` to prevent adding a trailing newline to the clipboard content 4. The `-c` flag for `sh` is required to execute the pipe command properly 5. Consider showing a toast notification to confirm the copy action to users ### Dependencies This method requires `wl-copy` from the `wl-clipboard` package, which is standard on Wayland systems. ## Running External Commands Plugins that need to execute external commands and capture their output should use the `Proc` singleton, which provides debounced command execution with automatic cleanup. ### Correct Method Import the `Proc` singleton from `qs.Common` and use `runCommand`: ```qml import QtQuick import qs.Common Item { function fetchData() { Proc.runCommand( "myPlugin.fetchData", ["curl", "-s", "https://api.example.com/data"], (stdout, exitCode) => { if (exitCode === 0) { console.log("Success:", stdout) processData(stdout) } else { console.error("Command failed with exit code:", exitCode) } }, 100 ) } } ``` ### Function Signature ```qml Proc.runCommand(id, command, callback, debounceMs) ``` **Parameters:** - `id` (string): Unique identifier for this command. Used for debouncing - multiple calls with the same ID within the debounce window will only execute the last one - `command` (array): Command and arguments as an array (e.g., `["sh", "-c", "echo hello"]`) - `callback` (function): Callback function receiving `(stdout, exitCode)` when the command completes - `stdout` (string): Captured standard output from the command - `exitCode` (number): Exit code of the process (0 typically means success) - `debounceMs` (number, optional): Debounce delay in milliseconds. Defaults to 50ms if not specified ### Key Features 1. **Automatic Cleanup**: Process objects are automatically destroyed after completion 2. **Debouncing**: Rapid successive calls with the same ID are debounced, only executing the last one 3. **Output Capture**: Automatically captures stdout for processing 4. **Error Handling**: Exit codes are passed to the callback for error detection ### Example Usage #### Simple Command Execution ```qml import QtQuick import qs.Common Item { function checkNetwork() { Proc.runCommand( "myPlugin.ping", ["ping", "-c", "1", "8.8.8.8"], (output, exitCode) => { if (exitCode === 0) { console.log("Network is up") } else { console.log("Network is down") } } ) } } ``` #### Parsing Command Output ```qml import QtQuick import qs.Common Item { property var diskUsage: ({}) function updateDiskUsage() { Proc.runCommand( "myPlugin.df", ["df", "-h", "/home"], (output, exitCode) => { if (exitCode === 0) { const lines = output.trim().split("\n") if (lines.length > 1) { const parts = lines[1].split(/\s+/) diskUsage = { total: parts[1], used: parts[2], available: parts[3], percent: parts[4] } } } } ) } } ``` #### Shell Commands with Pipes ```qml import QtQuick import qs.Common Item { function getTopProcess() { Proc.runCommand( "myPlugin.topProcess", ["sh", "-c", "ps aux | sort -nrk 3,3 | head -n 1"], (output, exitCode) => { if (exitCode === 0) { console.log("Top process:", output) } } ) } } ``` #### Debouncing Rapid Updates ```qml import QtQuick import qs.Common import qs.Widgets Item { DankTextField { id: searchField placeholderText: "Search files..." onTextChanged: { Proc.runCommand( "myPlugin.search", ["find", "/home", "-name", "*" + text + "*"], (output, exitCode) => { if (exitCode === 0) { updateSearchResults(output) } }, 500 ) } } } ``` ### Important Notes 1. **Unique IDs**: Use descriptive, namespaced IDs (e.g., `"myPlugin.actionName"`) to avoid conflicts 2. **Debouncing**: Use appropriate debounce delays for your use case: - Fast updates (50-100ms): System monitoring, real-time data - User input (300-500ms): Search fields, text input processing - Network requests (500-1000ms): API calls, web scraping 3. **Error Handling**: Always check the exit code in your callback before processing output 4. **Shell Commands**: Use `["sh", "-c", "command"]` for complex shell commands with pipes or redirects 5. **Security**: Sanitize user input before passing to commands to prevent command injection 6. **Performance**: Avoid running expensive commands too frequently - use debouncing wisely ### Comparison with Other Methods **Proc.runCommand** vs **Quickshell.execDetached**: - Use `Proc.runCommand` when you need to capture output or check exit codes - Use `Quickshell.execDetached` for fire-and-forget operations (like clipboard copy) **Proc.runCommand** vs **Process component**: - Use `Proc.runCommand` for simple, one-off command executions with automatic cleanup - Use `Process` component for long-running processes or when you need fine-grained control ## Debugging ### Console Logging View plugin logs: ```bash qs -v -p $CONFIGPATH/quickshell/dms/shell.qml ``` Look for lines prefixed with: - `PluginService:` - Service operations - `PluginsTab:` - UI interactions - `PluginsTab:` - Settings loading and accordion interface ### Common Issues 1. **Plugin Not Detected** - Check plugin.json syntax (use `jq` or JSON validator) - Verify directory is in `$CONFIGPATH/DankMaterialShell/plugins/` - Click "Scan for Plugins" in Settings 2. **Widget Not Displaying** - Ensure plugin is enabled in Settings - Add plugin ID to DankBar widget list - Check widget width/height properties 3. **Settings Not Loading** - Verify `settings` path in plugin.json - Check settings component for errors - Ensure plugin is enabled and loaded - Review PluginsTab console output for injection issues 4. **Data Not Persisting** - Confirm pluginService.savePluginData() calls (with injection) - Check `$CONFIGPATH/DankMaterialShell/settings.json` for pluginSettings data - Verify plugin has settings permissions - Ensure PluginService was properly injected into settings component ## Security Considerations Plugins run with full QML runtime access. Only install plugins from trusted sources. **Permissions System:** - `settings_read`: Read plugin configuration (not currently enforced) - `settings_write`: **Required** to use PluginSettings - write plugin configuration (enforced) - `process`: Execute system commands (not currently enforced) - `network`: Network access (not currently enforced) Currently, only `settings_write` is enforced by the PluginSettings component. ## API Stability The plugin API is currently **experimental**. Breaking changes may occur in minor version updates. Pin to specific DMS versions for production use. **Roadmap:** - Plugin marketplace/repository - Sandboxed plugin execution - Enhanced permission system - Plugin update notifications - Inter-plugin communication ## Launcher Plugins Launcher plugins extend the DMS application launcher by adding custom searchable items with trigger-based filtering. ### Overview Launcher plugins enable you to: - Add custom items to the launcher/app drawer - Use trigger strings for quick filtering (e.g., `!`, `#`, `@`) - Execute custom actions when items are selected - Provide searchable, categorized content - Integrate seamlessly with the existing launcher ### Plugin Type Configuration To create a launcher plugin, set the plugin type in `plugin.json`: ```json { "id": "myLauncher", "name": "My Launcher Plugin", "description": "A custom launcher plugin for quick actions", "version": "1.0.0", "author": "Your Name", "type": "launcher", "capabilities": ["show-thing"], "component": "./MyLauncher.qml", "trigger": "#", "icon": "search", "settings": "./MySettings.qml", "requires_dms": ">=0.1.18", "permissions": ["settings_read", "settings_write"] } ``` ### Launcher Component Contract Create `MyLauncher.qml` with the following interface: ```qml import QtQuick import qs.Services Item { id: root // Required properties property var pluginService: null property string trigger: "#" // Required signals signal itemsChanged() // Required: Return array of launcher items function getItems(query) { return [ { name: "Item Name", icon: "icon_name", comment: "Description", action: "type:data", categories: ["MyLauncher"] } ] } // Required: Execute item action function executeItem(item) { const [type, data] = item.action.split(":", 2) // Handle action based on type } Component.onCompleted: { if (pluginService) { trigger = pluginService.loadPluginData("myLauncher", "trigger", "#") } } } ``` ### Item Structure Each item returned by `getItems()` must include: - `name` (string): Display name shown in launcher - `icon` (string): Material Design icon name - `comment` (string): Description/subtitle text - `action` (string): Action identifier in `type:data` format - `categories` (array): Array containing your plugin name ### Trigger System Triggers control when your plugin's items appear in the launcher: **Empty Trigger Mode** (No trigger): - Items always visible alongside regular apps - Search includes your items automatically - Configure by saving empty trigger: `trigger: ""` **Custom Trigger Mode**: - Items only appear when trigger is typed - Example: Type `#` to show only your plugin's items - Type `# query` to search within your plugin - Configure any string: `#`, `!`, `@`, `!custom`, etc. ### Trigger Configuration in Settings Provide a settings component with trigger configuration: ```qml import QtQuick import QtQuick.Controls import qs.Widgets FocusScope { id: root property var pluginService: null Column { spacing: 12 CheckBox { id: noTriggerToggle text: "No trigger (always show)" checked: loadSettings("noTrigger", false) onCheckedChanged: { saveSettings("noTrigger", checked) if (checked) { saveSettings("trigger", "") } else { saveSettings("trigger", triggerField.text || "#") } } } DankTextField { id: triggerField visible: !noTriggerToggle.checked text: loadSettings("trigger", "#") placeholderText: "#" onTextEdited: { saveSettings("trigger", text || "#") } } } function saveSettings(key, value) { if (pluginService) { pluginService.savePluginData("myLauncher", key, value) } } function loadSettings(key, defaultValue) { if (pluginService) { return pluginService.loadPluginData("myLauncher", key, defaultValue) } return defaultValue } } ``` ### Action Execution Handle different action types in `executeItem()`: ```qml function executeItem(item) { const actionParts = item.action.split(":") const actionType = actionParts[0] const actionData = actionParts.slice(1).join(":") switch (actionType) { case "toast": if (typeof ToastService !== "undefined") { ToastService.showInfo("Plugin", actionData) } break case "copy": // Copy to clipboard break case "script": // Execute command break default: console.warn("Unknown action:", actionType) } } ``` ### Search and Filtering The launcher automatically handles search when: **With empty trigger**: - Your items appear in all searches - No prefix needed **With custom trigger**: - Type trigger alone: Shows all your items - Type trigger + query: Filters your items by query - The query parameter is passed to your `getItems(query)` function Example `getItems()` implementation: ```qml function getItems(query) { const allItems = [ {name: "Item 1", ...}, {name: "Item 2", ...}, {name: "Test Item", ...} ] if (!query || query.length === 0) { return allItems } const lowerQuery = query.toLowerCase() return allItems.filter(item => { return item.name.toLowerCase().includes(lowerQuery) || item.comment.toLowerCase().includes(lowerQuery) }) } ``` ### Integration Flow 1. User opens launcher 2. If empty trigger: Your items appear alongside apps 3. If custom trigger: User types trigger (e.g., `#`) 4. Launcher calls `getItems(query)` on your plugin 5. Your items displayed with your plugin's category 6. User selects item and presses Enter 7. Launcher calls `executeItem(item)` on your plugin ### Best Practices 1. **Unique Triggers**: Choose non-conflicting trigger strings 2. **Fast Response**: Return results quickly from `getItems()` 3. **Clear Names**: Use descriptive item names and comments 4. **Error Handling**: Gracefully handle failures in `executeItem()` 5. **Cleanup**: Destroy temporary objects after use 6. **Empty Trigger Support**: Consider if your plugin benefits from always being visible ### Example Plugin See `PLUGINS/LauncherExample/` for a complete working example demonstrating: - Trigger configuration (including empty trigger mode) - Multiple action types (toast, copy, script) - Search/filtering implementation - Settings integration - Proper error handling ## Resources - **Plugin Schema**: `plugin-schema.json` - JSON Schema for validation - **Example Plugins**: - [Emoji Picker](./ExampleEmojiPlugin/) - [WorldClock](https://github.com/rochacbruno/WorldClock) - [LauncherExample](./LauncherExample/) - [Calculator](https://github.com/rochacbruno/DankCalculator) - **PluginService**: `Services/PluginService.qml` - **Settings UI**: `Modules/Settings/PluginsTab.qml` - **DankBar Integration**: `Modules/DankBar/DankBar.qml` - **Launcher Integration**: `Modules/AppDrawer/AppLauncher.qml` - **Theme Reference**: `Common/Theme.qml` - **Widget Library**: `Widgets/` ## Contributing Share your plugins with the community: 1. Create a public repository with your plugin 2. Validate your `plugin.json` against `plugin-schema.json` 3. Include comprehensive README.md 4. Add example screenshots 5. Document dependencies and permissions For plugin system improvements, submit issues or PRs to the main DMS repository.