1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

switch hto monorepo structure

This commit is contained in:
bbedward
2025-11-12 17:18:45 -05:00
parent 6013c994a6
commit 24e800501a
768 changed files with 76284 additions and 221 deletions

View File

@@ -0,0 +1,32 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Widgets
PluginSettings {
id: root
pluginId: "colorDemo"
StyledText {
width: parent.width
text: "Color Demo Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Choose a custom color to display in the bar widget"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
ColorSetting {
settingKey: "customColor"
label: "Custom Color"
description: "Choose a custom color to display in the widget"
defaultValue: Theme.primary
}
}

View File

@@ -0,0 +1,57 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property color customColor: pluginData.customColor || Theme.primary
horizontalBarPill: Component {
Row {
spacing: Theme.spacingS
Rectangle {
width: 20
height: 20
radius: 4
color: root.customColor
border.color: Theme.outlineStrong
border.width: 1
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.customColor.toString()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
verticalBarPill: Component {
Column {
spacing: Theme.spacingXS
Rectangle {
width: 20
height: 20
radius: 4
color: root.customColor
border.color: Theme.outlineStrong
border.width: 1
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: root.customColor.toString()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}

View File

@@ -0,0 +1,12 @@
{
"id": "colorDemo",
"name": "Color Demo",
"description": "Demonstrates color picker plugin setting",
"version": "1.0.0",
"author": "DMS",
"icon": "palette",
"type": "widget",
"component": "./ColorDemoWidget.qml",
"settings": "./ColorDemoSettings.qml",
"permissions": ["settings_read", "settings_write"]
}

View File

@@ -0,0 +1,138 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property bool isEnabled: pluginData.isEnabled || false
property var options: pluginData.options || ["Option A", "Option B", "Option C"]
ccWidgetIcon: isEnabled ? "settings" : "settings"
ccWidgetPrimaryText: "Detail Example"
ccWidgetSecondaryText: {
if (isEnabled) {
const selected = pluginData.selectedOption || "Option A"
return selected
}
return "Disabled"
}
ccWidgetIsActive: isEnabled
onCcWidgetToggled: {
isEnabled = !isEnabled
if (pluginService) {
pluginService.savePluginData("controlCenterDetailExample", "isEnabled", isEnabled)
}
}
ccDetailContent: Component {
Rectangle {
id: detailRoot
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.width: 0
visible: true
property var options: ["Option A", "Option B", "Option C"]
property string currentSelection: SettingsData.getPluginSetting("controlCenterDetailExample", "selectedOption", "Option A")
Column {
id: detailColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
StyledText {
text: "Detail Example Settings"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Select an option below:"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
Repeater {
model: detailRoot.options
Rectangle {
width: parent.width
height: 40
radius: Theme.cornerRadius
color: optionMouseArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
border.color: detailRoot.currentSelection === modelData ? Theme.primary : "transparent"
border.width: detailRoot.currentSelection === modelData ? 2 : 0
MouseArea {
id: optionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
z: 100
onClicked: {
SettingsData.setPluginSetting("controlCenterDetailExample", "selectedOption", modelData)
detailRoot.currentSelection = modelData
PluginService.pluginDataChanged("controlCenterDetailExample")
ToastService.showInfo("Option Selected", modelData)
}
}
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
enabled: false
DankIcon {
name: detailRoot.currentSelection === modelData ? "radio_button_checked" : "radio_button_unchecked"
color: detailRoot.currentSelection === modelData ? Theme.primary : Theme.surfaceVariantText
size: Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData
color: detailRoot.currentSelection === modelData ? Theme.primary : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
horizontalBarPill: Component {
Row {
spacing: Theme.spacingXS
DankIcon {
name: root.isEnabled ? "settings" : "settings_off"
color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.iconSize - 4
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
const selected = root.pluginData.selectedOption || "Option A"
return selected.substring(0, 1)
}
color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}

View File

@@ -0,0 +1,12 @@
{
"id": "controlCenterDetailExample",
"name": "CC Detail Example",
"description": "Example plugin with Control Center detail dropdown",
"version": "1.0.0",
"author": "DankMaterialShell",
"type": "widget",
"capabilities": ["control-center"],
"component": "./DetailExampleWidget.qml",
"icon": "settings",
"permissions": ["settings_read", "settings_write"]
}

View File

@@ -0,0 +1,68 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property bool isEnabled: pluginData.isEnabled || false
property int clickCount: pluginData.clickCount || 0
ccWidgetIcon: isEnabled ? "toggle_on" : "toggle_off"
ccWidgetPrimaryText: "Example Toggle"
ccWidgetSecondaryText: isEnabled ? `Active ${clickCount} clicks` : "Inactive"
ccWidgetIsActive: isEnabled
onCcWidgetToggled: {
isEnabled = !isEnabled
clickCount += 1
if (pluginService) {
pluginService.savePluginData("controlCenterExample", "isEnabled", isEnabled)
pluginService.savePluginData("controlCenterExample", "clickCount", clickCount)
}
ToastService.showInfo("Example Toggle", isEnabled ? "Activated!" : "Deactivated!")
}
horizontalBarPill: Component {
Row {
spacing: Theme.spacingXS
DankIcon {
name: root.isEnabled ? "toggle_on" : "toggle_off"
color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.iconSize - 4
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: `${root.clickCount}`
color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
anchors.verticalCenter: parent.verticalCenter
}
}
}
verticalBarPill: Component {
Column {
spacing: Theme.spacingXS
DankIcon {
name: root.isEnabled ? "toggle_on" : "toggle_off"
color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.iconSize - 4
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: `${root.clickCount}`
color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}

View File

@@ -0,0 +1,12 @@
{
"id": "controlCenterExample",
"name": "CC Toggle Example",
"description": "Example plugin with Control Center toggle widget",
"version": "1.0.0",
"author": "DankMaterialShell",
"type": "widget",
"capabilities": ["control-center"],
"component": "./ControlCenterExampleWidget.qml",
"icon": "toggle_on",
"permissions": ["settings_read", "settings_write"]
}

View File

@@ -0,0 +1,97 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "exampleEmojiPlugin"
// Header section to explain what this plugin does
StyledText {
width: parent.width
text: "Emoji Cycler Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Configure which emojis appear in your bar, how quickly they cycle, and how many show at once."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
// Dropdown to select which emoji set to use
SelectionSetting {
settingKey: "emojiSet"
label: "Emoji Set"
description: "Choose which collection of emojis to cycle through"
options: [
{label: "Happy & Sad", value: "happySad"},
{label: "Hearts", value: "hearts"},
{label: "Hand Gestures", value: "hands"},
{label: "All Mixed", value: "mixed"}
]
defaultValue: "happySad"
// Update the actual emoji array when selection changes
onValueChanged: {
const sets = {
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
}
const newEmojis = sets[value] || sets["happySad"]
root.saveValue("emojis", newEmojis)
}
Component.onCompleted: {
// Initialize the emojis array on first load
const currentSet = value || defaultValue
const sets = {
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
}
const emojis = sets[currentSet] || sets["happySad"]
root.saveValue("emojis", emojis)
}
}
// Slider to control how fast emojis cycle (in milliseconds)
SliderSetting {
settingKey: "cycleInterval"
label: "Cycle Speed"
description: "How quickly emojis rotate (in seconds)"
defaultValue: 3000
minimum: 500
maximum: 10000
unit: "ms"
leftIcon: "schedule"
}
// Slider to control max emojis shown in the bar
SliderSetting {
settingKey: "maxBarEmojis"
label: "Max Bar Emojis"
description: "Maximum number of emojis to display in the bar at once"
defaultValue: 3
minimum: 1
maximum: 8
unit: ""
rightIcon: "emoji_emotions"
}
StyledText {
width: parent.width
text: "💡 Tip: Click the emoji widget in your bar to open the emoji picker and copy any emoji to your clipboard!"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}

View File

@@ -0,0 +1,149 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property var enabledEmojis: pluginData.emojis || ["😊", "😢", "❤️"]
property int cycleInterval: pluginData.cycleInterval || 3000
property int maxBarEmojis: pluginData.maxBarEmojis || 3
property int currentIndex: 0
property var displayedEmojis: []
Timer {
interval: root.cycleInterval
running: true
repeat: true
onTriggered: {
if (root.enabledEmojis.length > 0) {
root.currentIndex = (root.currentIndex + 1) % root.enabledEmojis.length
root.updateDisplayedEmojis()
}
}
}
function updateDisplayedEmojis() {
const maxToShow = Math.min(root.maxBarEmojis, root.enabledEmojis.length)
let emojis = []
for (let i = 0; i < maxToShow; i++) {
const idx = (root.currentIndex + i) % root.enabledEmojis.length
emojis.push(root.enabledEmojis[idx])
}
root.displayedEmojis = emojis
}
Component.onCompleted: {
updateDisplayedEmojis()
}
onEnabledEmojisChanged: updateDisplayedEmojis()
onMaxBarEmojisChanged: updateDisplayedEmojis()
horizontalBarPill: Component {
Row {
id: emojiRow
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
}
}
}
}
verticalBarPill: Component {
Column {
id: emojiColumn
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeMedium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
popoutContent: Component {
PopoutComponent {
id: popoutColumn
headerText: "Emoji Picker"
detailsText: "Click an emoji to copy it to clipboard"
showCloseButton: true
property var allEmojis: [
"😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃",
"😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙",
"😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔",
"🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥",
"😌", "😔", "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮",
"🤧", "🥵", "🥶", "😶‍🌫️", "😵", "😵‍💫", "🤯", "🤠", "🥳", "😎",
"🤓", "🧐", "😕", "😟", "🙁", "☹️", "😮", "😯", "😲", "😳",
"🥺", "😦", "😧", "😨", "😰", "😥", "😢", "😭", "😱", "😖",
"😣", "😞", "😓", "😩", "😫", "🥱", "😤", "😡", "😠", "🤬",
"❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔",
"❤️‍🔥", "❤️‍🩹", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "💟",
"👍", "👎", "👊", "✊", "🤛", "🤜", "🤞", "✌️", "🤟", "🤘",
"👌", "🤌", "🤏", "👈", "👉", "👆", "👇", "☝️", "✋", "🤚"
]
Item {
width: parent.width
implicitHeight: root.popoutHeight - popoutColumn.headerHeight - popoutColumn.detailsHeight - Theme.spacingXL
DankGridView {
id: emojiGrid
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(parent.width / 50) * 50
height: parent.height
clip: true
cellWidth: 50
cellHeight: 50
model: popoutColumn.allEmojis
delegate: StyledRect {
width: 45
height: 45
radius: Theme.cornerRadius
color: emojiMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh
border.width: 0
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeXLarge
}
MouseArea {
id: emojiMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["sh", "-c", "echo -n '" + modelData + "' | wl-copy"])
ToastService.showInfo("Copied " + modelData + " to clipboard")
popoutColumn.closePopout()
}
}
}
}
}
}
}
popoutWidth: 400
popoutHeight: 500
}

View File

@@ -0,0 +1,56 @@
# Emoji Cycler Plugin
An example dms plugin that displays cycling emojis in your bar with an emoji picker popout.
## Features
- **Cycling Emojis**: Automatically rotates through your selected emoji set in the bar
- **Emoji Picker**: Click the widget to open a grid of 120+ emojis
- **Copy to Clipboard**: Click any emoji in the picker to copy it to clipboard (uses `wl-copy`)
- **Customizable**: Choose emoji sets, cycle speed, and max emojis shown
## Installation
1. Copy this directory to `~/.config/DankMaterialShell/plugins/ExampleEmojiPlugin`
2. Open DMS Settings → Plugins
3. Click "Scan for Plugins"
4. Enable "Emoji Cycler"
5. Add `exampleEmojiPlugin` to your DankBar widget list
## Settings
### Emoji Set
Choose from different emoji collections:
- **Happy & Sad**: Mix of emotional faces
- **Hearts**: Various colored hearts
- **Hand Gestures**: Thumbs up, peace signs, etc.
- **All Mixed**: A bit of everything
### Cycle Speed
Control how fast emojis rotate (500ms - 10000ms)
### Max Bar Emojis
How many emojis to display at once (1-8)
## Usage
**In the bar**: Watch emojis cycle through automatically
**Click the widget**: Opens emoji picker with 120+ emojis
**Click any emoji**: Copies it to clipboard and shows toast
## Requirements
- `wl-copy` (for clipboard support on Wayland)
## Example Code Highlights
This plugin demonstrates:
- Using `PluginComponent` for bar integration
- `SelectionSetting`, `SliderSetting` for configuration
- Timer-based animation
- Popout content with grid layout
- External command execution (`Quickshell.execDetached`)
- Toast notifications (`ToastService.show`)
- Dynamic settings loading/saving
Perfect template for creating your own DMS plugins!

View File

@@ -0,0 +1,16 @@
{
"id": "exampleEmojiPlugin",
"name": "Emoji Cycler",
"description": "Display cycling emojis in your bar with a handy emoji picker popout",
"version": "1.0.0",
"author": "AvengeMedia",
"type": "widget",
"capabilities": ["emoji-search", "clipboard", "dankbar-widget"],
"component": "./EmojiWidget.qml",
"icon": "mood",
"settings": "./EmojiSettings.qml",
"permissions": [
"settings_read",
"settings_write"
]
}

View File

@@ -0,0 +1,116 @@
# Example with Variants Plugin
This plugin demonstrates the dynamic variant system for DankMaterialShell plugins.
## What are Variants?
Variants allow a single plugin to create multiple widget instances with different configurations. Each variant appears as a separate widget option in the Bar Settings "Add Widget" menu.
## How It Works
### Plugin Architecture
1. **Single Component**: The plugin defines one widget component (`VariantWidget.qml`)
2. **Variant Data**: Each variant stores its configuration in plugin settings
3. **Dynamic Creation**: Variants are created through the plugin's settings UI
4. **Widget ID Format**: Variants use the format `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)
### Widget Properties
Each variant widget receives:
- `pluginService`: Reference to PluginService
- `pluginId`: The base plugin ID (e.g., "exampleVariants")
- `variantId`: The specific variant ID (e.g., "variant_1234567890")
- `variantData`: The variant's configuration object
### Variant Configuration
This example stores:
```javascript
{
id: "variant_1234567890",
name: "My Variant",
text: "Display Text",
icon: "star",
color: "#FF5722"
}
```
## Creating Your Own Variant Plugin
### 1. Widget Component
Create a widget that accepts variant data:
```qml
Rectangle {
property var pluginService: null
property string pluginId: ""
property string variantId: ""
property var variantData: null
// Use variantData for configuration
property string displayText: variantData?.text || "Default"
}
```
### 2. Settings Component
Provide UI to manage variants:
```qml
FocusScope {
property var pluginService: null
// Create variant
pluginService.createPluginVariant(pluginId, variantName, variantConfig)
// Remove variant
pluginService.removePluginVariant(pluginId, variantId)
// Update variant
pluginService.updatePluginVariant(pluginId, variantId, variantConfig)
// Get all variants
var variants = pluginService.getPluginVariants(pluginId)
}
```
### 3. Plugin Manifest
Standard plugin manifest - no special configuration needed:
```json
{
"id": "yourPlugin",
"name": "Your Plugin",
"component": "./Widget.qml",
"settings": "./Settings.qml"
}
```
## Use Cases
- **Script Runner**: Multiple variants running different scripts
- **System Monitors**: Different monitoring targets (CPU, GPU, Network)
- **Quick Launchers**: Different apps or commands per variant
- **Custom Indicators**: Different data sources or APIs
- **Time Zones**: Multiple clocks for different time zones
## API Reference
### PluginService Functions
- `getPluginVariants(pluginId)`: Get all variants for a plugin
- `getAllPluginVariants()`: Get all variants across all plugins
- `createPluginVariant(pluginId, name, config)`: Create new variant
- `removePluginVariant(pluginId, variantId)`: Delete variant
- `updatePluginVariant(pluginId, variantId, config)`: Update variant
- `getPluginVariantData(pluginId, variantId)`: Get specific variant data
### Widget Properties
- `pluginId`: Base plugin identifier
- `variantId`: Variant identifier (null if no variant)
- `variantData`: Variant configuration object
- `pluginService`: PluginService reference

View File

@@ -0,0 +1,303 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "exampleVariants"
onVariantsChanged: {
variantsModel.clear()
for (var i = 0; i < variants.length; i++) {
variantsModel.append(variants[i])
}
}
StyledText {
width: parent.width
text: "Variant Manager"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Create multiple widget variants with different text, icons, and colors"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
StyledRect {
width: parent.width
height: addVariantColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: addVariantColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
StyledText {
text: "Add New Variant"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Row {
width: parent.width
spacing: Theme.spacingM
Column {
width: (parent.width - Theme.spacingM * 2) / 3
spacing: Theme.spacingXS
StyledText {
text: "Name"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: nameField
width: parent.width
placeholderText: "Variant Name"
}
}
Column {
width: (parent.width - Theme.spacingM * 2) / 3
spacing: Theme.spacingXS
StyledText {
text: "Icon"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: iconField
width: parent.width
placeholderText: "star"
}
}
Column {
width: (parent.width - Theme.spacingM * 2) / 3
spacing: Theme.spacingXS
StyledText {
text: "Text"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: textField
width: parent.width
placeholderText: "Display Text"
}
}
}
DankButton {
text: "Create Variant"
iconName: "add"
onClicked: {
if (!nameField.text) {
ToastService.showError("Please enter a variant name")
return
}
var variantConfig = {
text: textField.text || nameField.text,
icon: iconField.text || "widgets"
}
createVariant(nameField.text, variantConfig)
ToastService.showInfo("Variant created: " + nameField.text)
nameField.text = ""
iconField.text = ""
textField.text = ""
}
}
}
}
StyledRect {
width: parent.width
height: Math.max(200, variantsColumn.implicitHeight + Theme.spacingL * 2)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: variantsColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
StyledText {
text: "Existing Variants"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
ListView {
width: parent.width
height: Math.max(100, contentHeight)
clip: true
spacing: Theme.spacingXS
model: ListModel {
id: variantsModel
}
delegate: StyledRect {
required property var model
width: ListView.view.width
height: variantRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: variantMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainer
Row {
id: variantRow
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Item {
width: Theme.iconSize
height: Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: model.icon || "widgets"
size: Theme.iconSize
color: Theme.surfaceText
width: Theme.iconSize
height: Theme.iconSize
clip: true
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
width: parent.width - Theme.iconSize - deleteButton.width - Theme.spacingM * 4
StyledText {
text: model.name || "Unnamed"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: "Text: " + (model.text || "") + " | Icon: " + (model.icon || "")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
elide: Text.ElideRight
}
}
Rectangle {
id: deleteButton
width: 32
height: 32
radius: 16
color: deleteArea.containsMouse ? Theme.error : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "delete"
size: 16
color: deleteArea.containsMouse ? Theme.onError : Theme.surfaceVariantText
}
MouseArea {
id: deleteArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
removeVariant(model.id)
ToastService.showInfo("Variant removed: " + model.name)
}
}
}
}
MouseArea {
id: variantMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
}
StyledText {
anchors.centerIn: parent
text: "No variants created yet"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: variantsModel.count === 0
}
}
}
}
StyledRect {
width: parent.width
height: instructionsColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surface
Column {
id: instructionsColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
spacing: Theme.spacingM
DankIcon {
name: "info"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "How to Use Variants"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "1. Create variants with different names, icons, and text\n2. Go to Bar Settings and click 'Add Widget'\n3. Each variant will appear as a separate widget option\n4. Add variants to your bar just like any other widget"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
lineHeight: 1.4
}
}
}
}

View File

@@ -0,0 +1,57 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property string variantId: ""
property var variantData: null
property string displayText: variantData?.text || "Default Text"
property string displayIcon: variantData?.icon || "widgets"
horizontalBarPill: Component {
Row {
spacing: 3
DankIcon {
name: root.displayIcon
size: Theme.iconSize - 8
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.displayText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
verticalBarPill: Component {
Column {
spacing: 1
DankIcon {
name: root.displayIcon
size: Theme.iconSize - 8
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: root.displayText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"id": "exampleVariants",
"name": "Example with Variants",
"description": "Demonstrates dynamic variant creation for plugins",
"version": "1.0.0",
"author": "DMS",
"type": "widget",
"capabilities": ["multiple-usecases", "variants"],
"component": "./VariantWidget.qml",
"icon": "widgets",
"settings": "./VariantSettings.qml",
"permissions": [
"settings_read",
"settings_write"
]
}

View File

@@ -0,0 +1,140 @@
import QtQuick
import Quickshell
import qs.Services
Item {
id: root
// Plugin properties
property var pluginService: null
property string trigger: "#"
// Plugin interface signals
signal itemsChanged()
Component.onCompleted: {
console.info("LauncherExample: Plugin loaded")
// Load custom trigger from settings
if (pluginService) {
trigger = pluginService.loadPluginData("launcherExample", "trigger", "#")
}
}
// Required function: Get items for launcher
function getItems(query) {
const baseItems = [
{
name: "Test Item 1",
icon: "material:lightbulb",
comment: "This is a test item that shows a toast notification",
action: "toast:Test Item 1 executed!",
categories: ["LauncherExample"]
},
{
name: "Test Item 2",
icon: "material:star",
comment: "Another test item with different action",
action: "toast:Test Item 2 clicked!",
categories: ["LauncherExample"]
},
{
name: "Test Item 3",
icon: "material:favorite",
comment: "Third test item for demonstration",
action: "toast:Test Item 3 activated!",
categories: ["LauncherExample"]
},
{
name: "Unicode Icon Example",
icon: "unicode:🚀",
comment: "Demonstrates unicode/emoji icon support",
action: "toast:Unicode icons work great!",
categories: ["LauncherExample"]
},
{
name: "Example Copy Action",
icon: "material:content_copy",
comment: "Demonstrates copying text to clipboard",
action: "copy:This text was copied by the launcher plugin!",
categories: ["LauncherExample"]
},
{
name: "Example Script Action",
icon: "material:terminal",
comment: "Demonstrates running a simple command",
action: "script:echo 'Hello from launcher plugin!'",
categories: ["LauncherExample"]
}
]
if (!query || query.length === 0) {
return baseItems
}
// Filter items based on query
const lowerQuery = query.toLowerCase()
return baseItems.filter(item => {
return item.name.toLowerCase().includes(lowerQuery) ||
item.comment.toLowerCase().includes(lowerQuery)
})
}
// Required function: Execute item action
function executeItem(item) {
if (!item || !item.action) {
console.warn("LauncherExample: Invalid item or action")
return
}
console.log("LauncherExample: Executing item:", item.name, "with action:", item.action)
const actionParts = item.action.split(":")
const actionType = actionParts[0]
const actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "toast":
showToast(actionData)
break
case "copy":
copyToClipboard(actionData)
break
case "script":
runScript(actionData)
break
default:
console.warn("LauncherExample: Unknown action type:", actionType)
showToast("Unknown action: " + actionType)
}
}
// Helper functions for different action types
function showToast(message) {
if (typeof ToastService !== "undefined") {
ToastService.showInfo("LauncherExample", message)
} else {
console.log("LauncherExample Toast:", message)
}
}
function copyToClipboard(text) {
Quickshell.execDetached(["sh", "-c", "echo -n '" + text + "' | wl-copy"])
showToast("Copied to clipboard: " + text)
}
function runScript(command) {
console.log("LauncherExample: Would run script:", command)
showToast("Script executed: " + command)
// In a real plugin, you might create a Process component here
// For demo purposes, we just show what would happen
}
// Watch for trigger changes
onTriggerChanged: {
if (pluginService) {
pluginService.savePluginData("launcherExample", "trigger", trigger)
}
}
}

View File

@@ -0,0 +1,244 @@
import QtQuick
import QtQuick.Controls
import qs.Widgets
FocusScope {
id: root
property var pluginService: null
implicitHeight: settingsColumn.implicitHeight
height: implicitHeight
Column {
id: settingsColumn
anchors.fill: parent
anchors.margins: 16
spacing: 16
Text {
text: "Launcher Example Plugin Settings"
font.pixelSize: 18
font.weight: Font.Bold
color: "#FFFFFF"
}
Text {
text: "This plugin demonstrates the launcher plugin system with example items and actions."
font.pixelSize: 14
color: "#CCFFFFFF"
wrapMode: Text.WordWrap
width: parent.width - 32
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 12
width: parent.width - 32
Text {
text: "Trigger Configuration"
font.pixelSize: 16
font.weight: Font.Medium
color: "#FFFFFF"
}
Text {
text: noTriggerToggle.checked ? "Items will always show in the launcher (no trigger needed)." : "Set the trigger text to activate this plugin. Type the trigger in the launcher to filter to this plugin's items."
font.pixelSize: 12
color: "#CCFFFFFF"
wrapMode: Text.WordWrap
width: parent.width
}
Row {
spacing: 12
CheckBox {
id: noTriggerToggle
text: "No trigger (always show)"
checked: loadSettings("noTrigger", false)
contentItem: Text {
text: noTriggerToggle.text
font.pixelSize: 14
color: "#FFFFFF"
leftPadding: noTriggerToggle.indicator.width + 8
verticalAlignment: Text.AlignVCenter
}
indicator: Rectangle {
implicitWidth: 20
implicitHeight: 20
radius: 4
border.color: noTriggerToggle.checked ? "#4CAF50" : "#60FFFFFF"
border.width: 2
color: noTriggerToggle.checked ? "#4CAF50" : "transparent"
Rectangle {
width: 12
height: 12
anchors.centerIn: parent
radius: 2
color: "#FFFFFF"
visible: noTriggerToggle.checked
}
}
onCheckedChanged: {
saveSettings("noTrigger", checked)
if (checked) {
saveSettings("trigger", "")
} else {
saveSettings("trigger", triggerField.text || "#")
}
}
}
}
Row {
spacing: 12
anchors.left: parent.left
anchors.right: parent.right
visible: !noTriggerToggle.checked
Text {
text: "Trigger:"
font.pixelSize: 14
color: "#FFFFFF"
anchors.verticalCenter: parent.verticalCenter
}
DankTextField {
id: triggerField
width: 100
height: 40
text: loadSettings("trigger", "#")
placeholderText: "#"
backgroundColor: "#30FFFFFF"
textColor: "#FFFFFF"
onTextEdited: {
const newTrigger = text.trim()
saveSettings("trigger", newTrigger || "#")
saveSettings("noTrigger", newTrigger === "")
}
}
Text {
text: "Examples: #, !, @, !ex, etc."
font.pixelSize: 12
color: "#AAFFFFFF"
anchors.verticalCenter: parent.verticalCenter
}
}
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 8
width: parent.width - 32
Text {
text: "Example Items:"
font.pixelSize: 14
font.weight: Font.Medium
color: "#FFFFFF"
}
Column {
spacing: 4
leftPadding: 16
Text {
text: "• Test Item 1, 2, 3 - Show toast notifications"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: "• Example Copy Action - Copy text to clipboard"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: "• Example Script Action - Demonstrate script execution"
font.pixelSize: 12
color: "#CCFFFFFF"
}
}
}
Rectangle {
width: parent.width - 32
height: 1
color: "#30FFFFFF"
}
Column {
spacing: 8
width: parent.width - 32
Text {
text: "Usage:"
font.pixelSize: 14
font.weight: Font.Medium
color: "#FFFFFF"
}
Column {
spacing: 4
leftPadding: 16
bottomPadding: 24
Text {
text: "1. Open Launcher (Ctrl+Space or click launcher button)"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: noTriggerToggle.checked ? "2. Items are always visible in the launcher" : "2. Type your trigger (default: #) to filter to this plugin"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: noTriggerToggle.checked ? "3. Search works normally with plugin items included" : "3. Optionally add search terms: '# test' to find test items"
font.pixelSize: 12
color: "#CCFFFFFF"
}
Text {
text: "4. Select an item and press Enter to execute its action"
font.pixelSize: 12
color: "#CCFFFFFF"
}
}
}
}
function saveSettings(key, value) {
if (pluginService) {
pluginService.savePluginData("launcherExample", key, value)
}
}
function loadSettings(key, defaultValue) {
if (pluginService) {
return pluginService.loadPluginData("launcherExample", key, defaultValue)
}
return defaultValue
}
}

View File

@@ -0,0 +1,240 @@
# LauncherExample Plugin
A demonstration plugin that showcases the DMS launcher plugin system capabilities.
## Purpose
This plugin serves as a comprehensive example for developers creating launcher plugins for DMS. It demonstrates:
- **Plugin Structure**: Proper manifest, launcher, and settings components
- **Trigger System**: Customizable trigger strings for plugin activation (including empty triggers)
- **Item Management**: Providing searchable items to the launcher
- **Action Execution**: Handling different types of actions (toast, copy, script)
- **Settings Integration**: Configurable plugin settings with persistence
## Features
### Example Items
- **Test Items 1-3**: Demonstrate toast notifications
- **Copy Action**: Shows clipboard integration
- **Script Action**: Demonstrates command execution
### Trigger System
- **Default Trigger**: `#` (configurable in settings)
- **Empty Trigger Option**: Items can always be visible without needing a trigger
- **Usage**: Type `#` in launcher to filter to this plugin (when trigger is set)
- **Search**: Type `# test` to search within plugin items
### Action Types
- `toast:message` - Shows toast notification
- `copy:text` - Copies text to clipboard
- `script:command` - Executes shell command (demo only)
## File Structure
```
PLUGINS/LauncherExample/
├── plugin.json # Plugin manifest
├── LauncherExampleLauncher.qml # Main launcher component
├── LauncherExampleSettings.qml # Settings interface
└── README.md # This documentation
```
## Installation
1. **Plugin Directory**: Copy to `~/.config/DankMaterialShell/plugins/LauncherExample`
2. **Enable Plugin**: Settings → Plugins → Enable "LauncherExample"
3. **Configure**: Set custom trigger in plugin settings if desired
## Usage
### With Trigger (Default)
1. Open launcher (Ctrl+Space or launcher button)
2. Type `#` to activate plugin trigger
3. Browse available items or add search terms
4. Press Enter to execute selected item
### Without Trigger (Empty Trigger Mode)
1. Enable "No trigger (always show)" in plugin settings
2. Open launcher - plugin items are always visible
3. Search works normally with plugin items included
4. Press Enter to execute selected item
### Search Examples
- `#` - Show all plugin items (with trigger enabled)
- `# test` - Show items matching "test"
- `# copy` - Show items matching "copy"
- `test` - Show all items matching "test" (with empty trigger enabled)
## Developer Guide
### Plugin Contract
**Launcher Component Requirements**:
```qml
// Required properties
property var pluginService: null
property string trigger: "#"
// Required signals
signal itemsChanged()
// Required functions
function getItems(query): array
function executeItem(item): void
```
**Item Structure**:
```javascript
{
name: "Item Name", // Display name
icon: "icon_name", // Icon (optional, see Icon Types below)
comment: "Description", // Subtitle text
action: "type:data", // Action to execute
categories: ["PluginName"] // Category array
}
```
**Icon Types**:
The `icon` field supports four formats:
1. **Material Design Icons** - Use `material:` prefix:
```javascript
icon: "material:lightbulb" // Material Symbols Rounded font
```
Examples: `material:star`, `material:favorite`, `material:settings`
2. **Unicode/Emoji Icons** - Use `unicode:` prefix:
```javascript
icon: "unicode:🚀" // Unicode character or emoji
```
Display any Unicode character or emoji as the icon. Examples: `unicode:😀`, `unicode:⚡`, `unicode:🎨`
3. **Desktop Theme Icons** - Use icon name directly:
```javascript
icon: "firefox" // Uses system icon theme
```
Examples: `firefox`, `chrome`, `folder`, `text-editor`
4. **No Icon** - Omit the `icon` field entirely:
```javascript
{
name: "😀 Grinning Face",
// No icon field
comment: "Copy emoji",
action: "copy:😀",
categories: ["MyPlugin"]
}
```
When icon is omitted, the launcher hides the icon area and displays only text
**Action Format**: `type:data` where:
- `type` - Action handler (toast, copy, script, etc.)
- `data` - Action-specific data
### Settings Integration
```qml
// Save setting
pluginService.savePluginData("pluginId", "key", value)
// Load setting
pluginService.loadPluginData("pluginId", "key", defaultValue)
```
### Trigger Configuration
The trigger can be configured in two ways:
1. **Empty Trigger** (No Trigger Mode):
- Check "No trigger (always show)" in settings
- Saves `trigger: ""` and `noTrigger: true`
- Items always appear in launcher alongside regular apps
2. **Custom Trigger**:
- Enter any string (e.g., `#`, `!`, `@`, `!ex`)
- Uncheck "No trigger" checkbox
- Items only appear when trigger is typed
### Manifest Structure
```json
{
"id": "launcherExample",
"name": "LauncherExample",
"type": "launcher",
"capabilities": ["launcher"],
"component": "./LauncherExampleLauncher.qml",
"settings": "./LauncherExampleSettings.qml",
"permissions": ["settings_read", "settings_write"]
}
```
Note: The `trigger` field in the manifest is optional and serves as the default trigger value.
## Extending This Plugin
### Adding New Items
```qml
function getItems(query) {
return [
{
name: "My Item",
icon: "custom_icon",
comment: "Does something cool",
action: "custom:action_data",
categories: ["LauncherExample"]
}
]
}
```
### Adding New Actions
```qml
function executeItem(item) {
const actionParts = item.action.split(":")
const actionType = actionParts[0]
const actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "custom":
handleCustomAction(actionData)
break
}
}
```
### Custom Trigger Logic
```qml
Component.onCompleted: {
if (pluginService) {
trigger = pluginService.loadPluginData("launcherExample", "trigger", "#")
}
}
onTriggerChanged: {
if (pluginService) {
pluginService.savePluginData("launcherExample", "trigger", trigger)
}
}
```
## Best Practices
1. **Unique Triggers**: Choose triggers that don't conflict with other plugins
2. **Clear Descriptions**: Write helpful item comments
3. **Error Handling**: Gracefully handle action failures
4. **Performance**: Return results quickly in getItems()
5. **Cleanup**: Destroy temporary objects in executeItem()
6. **Empty Trigger Support**: Consider if your plugin should support empty trigger mode
## Testing
Test the plugin by:
1. Installing and enabling in DMS
2. Testing with trigger enabled
3. Testing with empty trigger (no trigger mode)
4. Trying each action type
5. Testing search functionality
6. Verifying settings persistence
This plugin provides a solid foundation for building more sophisticated launcher plugins with custom functionality!

View File

@@ -0,0 +1,17 @@
{
"id": "launcherExample",
"name": "LauncherExample",
"description": "Example launcher plugin demonstrating the launcher plugin system",
"version": "1.0.0",
"author": "DMS Team",
"icon": "extension",
"type": "launcher",
"capabilities": ["clipboard", "command-execution"],
"component": "./LauncherExampleLauncher.qml",
"settings": "./LauncherExampleSettings.qml",
"trigger": "#",
"permissions": [
"settings_read",
"settings_write"
]
}

View File

@@ -0,0 +1,285 @@
# PopoutService for Plugins
## Overview
The `PopoutService` singleton provides plugins with access to all DankMaterialShell popouts and modals. It's automatically injected into plugin widgets and daemons, enabling them to control shell UI elements.
## Automatic Injection
The `popoutService` property is automatically injected into:
- Widget plugins (loaded in DankBar)
- Daemon plugins (background services)
- Plugin settings components
**Required**: Declare the property in your plugin component:
```qml
property var popoutService: null
```
**Note**: Without this declaration, the service cannot be injected and you'll see errors like `Cannot assign to non-existent property "popoutService"`
## API Reference
### Popouts (DankPopout-based)
| Component | Open | Close | Toggle |
|-----------|------|-------|--------|
| Control Center | `openControlCenter()` | `closeControlCenter()` | `toggleControlCenter()` |
| Notification Center | `openNotificationCenter()` | `closeNotificationCenter()` | `toggleNotificationCenter()` |
| App Drawer | `openAppDrawer()` | `closeAppDrawer()` | `toggleAppDrawer()` |
| Process List | `openProcessList()` | `closeProcessList()` | `toggleProcessList()` |
| DankDash | `openDankDash(tab)` | `closeDankDash()` | `toggleDankDash(tab)` |
| Battery | `openBattery()` | `closeBattery()` | `toggleBattery()` |
| VPN | `openVpn()` | `closeVpn()` | `toggleVpn()` |
| System Update | `openSystemUpdate()` | `closeSystemUpdate()` | `toggleSystemUpdate()` |
### Modals (DankModal-based)
| Modal | Show | Hide | Notes |
|-------|------|------|-------|
| Settings | `openSettings()` | `closeSettings()` | Full settings interface |
| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Cliphist integration |
| Spotlight | `openSpotlight()` | `closeSpotlight()` | Command launcher |
| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |
| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Fullscreen version, has `toggleProcessListModal()` |
| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |
| Notification | `showNotificationModal()` | `hideNotificationModal()` | Notification details |
| WiFi Password | `showWifiPasswordModal()` | `hideWifiPasswordModal()` | Network authentication |
| Network Info | `showNetworkInfoModal()` | `hideNetworkInfoModal()` | Network details |
### Slideouts
| Component | Open | Close | Toggle |
|-----------|------|-------|--------|
| Notepad | `openNotepad()` | `closeNotepad()` | `toggleNotepad()` |
## Usage Examples
### Simple Widget with Popout Control
```qml
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property var popoutService: null
width: 100
height: 30
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
MouseArea {
anchors.fill: parent
onClicked: popoutService?.toggleControlCenter()
}
StyledText {
anchors.centerIn: parent
text: "Settings"
}
}
```
### Daemon with Event-Driven Popouts
```qml
import QtQuick
import qs.Services
Item {
id: root
property var popoutService: null
Connections {
target: NotificationService
function onNotificationReceived(notification) {
if (notification.urgency === "critical") {
popoutService?.openNotificationCenter()
}
}
}
Connections {
target: BatteryService
function onPercentageChanged() {
if (BatteryService.percentage < 10 && !BatteryService.isCharging) {
popoutService?.openBattery()
}
}
}
}
```
### Widget with Multiple Popout Options
```qml
import QtQuick
import QtQuick.Controls
import qs.Common
Rectangle {
property var popoutService: null
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
contextMenu.popup()
} else {
popoutService?.toggleControlCenter()
}
}
}
Menu {
id: contextMenu
MenuItem {
text: "Settings"
onClicked: popoutService?.openSettings()
}
MenuItem {
text: "Notifications"
onClicked: popoutService?.toggleNotificationCenter()
}
MenuItem {
text: "Power Menu"
onClicked: popoutService?.openPowerMenu()
}
}
}
```
## Implementation Details
### Service Architecture
`PopoutService` is a singleton that holds references to popout instances:
```qml
// Services/PopoutService.qml
Singleton {
id: root
property var controlCenterPopout: null
property var notificationCenterPopout: null
// ... other popout references
function toggleControlCenter() {
controlCenterPopout?.toggle()
}
// ... other control functions
}
```
### Reference Assignment
References are assigned in `DMSShell.qml` when popouts are loaded:
```qml
LazyLoader {
ControlCenterPopout {
id: controlCenterPopout
Component.onCompleted: {
PopoutService.controlCenterPopout = controlCenterPopout
}
}
}
```
### Plugin Injection
The service is injected in three locations:
1. **DMSShell.qml** (daemon plugins):
```qml
Instantiator {
delegate: Loader {
onLoaded: {
if (item) {
item.popoutService = PopoutService
}
}
}
}
```
2. **WidgetHost.qml** (widget plugins):
```qml
onLoaded: {
if (item.popoutService !== undefined) {
item.popoutService = PopoutService
}
}
```
3. **CenterSection.qml** (center widgets):
```qml
onLoaded: {
if (item.popoutService !== undefined) {
item.popoutService = PopoutService
}
}
```
4. **PluginsTab.qml** (settings):
```qml
onLoaded: {
if (item && typeof PopoutService !== "undefined") {
item.popoutService = PopoutService
}
}
```
## Best Practices
1. **Use Optional Chaining**: Always use `?.` to handle null cases
```qml
popoutService?.toggleControlCenter()
```
2. **Check Availability**: Some popouts may not be available
```qml
if (popoutService && popoutService.controlCenterPopout) {
popoutService.toggleControlCenter()
}
```
3. **Lazy Loading**: First access may activate lazy loaders - this is normal
4. **Feature Detection**: Some popouts require specific features
```qml
if (BatteryService.batteryAvailable) {
popoutService?.openBattery()
}
```
5. **User Intent**: Only trigger popouts based on user actions or critical events
## Example Plugin
See `PLUGINS/PopoutControlExample/` for a complete working example that demonstrates:
- Widget creation with popout controls
- Menu-based popout selection
- Proper service usage
- Error handling
## Limitations
- Popouts are shared across all plugins - avoid conflicts
- Some popouts may be compositor or feature-dependent
- Lazy-loaded popouts need activation before use
- Multi-monitor considerations apply to positioned popouts

View File

@@ -0,0 +1,56 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "popoutControlExample"
StyledText {
width: parent.width
text: "Popout Control Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Choose which popout/modal will open when clicking the widget"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
SelectionSetting {
settingKey: "selectedPopout"
label: "Popout to Open"
description: "Select which popout or modal opens when you click the widget"
options: [
{label: "Control Center", value: "controlCenter"},
{label: "Notification Center", value: "notificationCenter"},
{label: "App Drawer", value: "appDrawer"},
{label: "Process List", value: "processList"},
{label: "DankDash", value: "dankDash"},
{label: "Battery Info", value: "battery"},
{label: "VPN", value: "vpn"},
{label: "System Update", value: "systemUpdate"},
{label: "Settings", value: "settings"},
{label: "Clipboard History", value: "clipboardHistory"},
{label: "Spotlight", value: "spotlight"},
{label: "Power Menu", value: "powerMenu"},
{label: "Color Picker", value: "colorPicker"},
{label: "Notepad", value: "notepad"}
]
defaultValue: "controlCenter"
}
StyledText {
width: parent.width
text: "💡 Tip: The widget displays the name of the selected popout and opens it when clicked!"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}

View File

@@ -0,0 +1,95 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property var popoutService: null
property string selectedPopout: pluginData.selectedPopout || "controlCenter"
property var popoutActions: ({
"controlCenter": (x, y, w, s, scr) => popoutService?.toggleControlCenter(x, y, w, s, scr),
"notificationCenter": (x, y, w, s, scr) => popoutService?.toggleNotificationCenter(x, y, w, s, scr),
"appDrawer": (x, y, w, s, scr) => popoutService?.toggleAppDrawer(x, y, w, s, scr),
"processList": (x, y, w, s, scr) => popoutService?.toggleProcessList(x, y, w, s, scr),
"dankDash": (x, y, w, s, scr) => popoutService?.toggleDankDash(0, x, y, w, s, scr),
"battery": (x, y, w, s, scr) => popoutService?.toggleBattery(x, y, w, s, scr),
"vpn": (x, y, w, s, scr) => popoutService?.toggleVpn(x, y, w, s, scr),
"systemUpdate": (x, y, w, s, scr) => popoutService?.toggleSystemUpdate(x, y, w, s, scr),
"settings": () => popoutService?.openSettings(),
"clipboardHistory": () => popoutService?.openClipboardHistory(),
"spotlight": () => popoutService?.openSpotlight(),
"powerMenu": () => popoutService?.togglePowerMenu(),
"colorPicker": () => popoutService?.showColorPicker(),
"notepad": () => popoutService?.toggleNotepad()
})
property var popoutNames: ({
"controlCenter": "Control Center",
"notificationCenter": "Notification Center",
"appDrawer": "App Drawer",
"processList": "Process List",
"dankDash": "DankDash",
"battery": "Battery Info",
"vpn": "VPN",
"systemUpdate": "System Update",
"settings": "Settings",
"clipboardHistory": "Clipboard",
"spotlight": "Spotlight",
"powerMenu": "Power Menu",
"colorPicker": "Color Picker",
"notepad": "Notepad"
})
pillClickAction: (x, y, width, section, screen) => {
if (popoutActions[selectedPopout]) {
popoutActions[selectedPopout](x, y, width, section, screen)
}
}
horizontalBarPill: Component {
Row {
spacing: Theme.spacingXS
DankIcon {
name: "widgets"
color: Theme.primary
font.pixelSize: Theme.iconSize - 6
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: popoutNames[selectedPopout] || "Popouts"
color: Theme.primary
font.pixelSize: Theme.fontSizeMedium
anchors.verticalCenter: parent.verticalCenter
}
}
}
verticalBarPill: Component {
Column {
spacing: Theme.spacingXS
DankIcon {
name: "widgets"
color: Theme.primary
font.pixelSize: Theme.iconSize - 6
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: popoutNames[selectedPopout] || "Popouts"
color: Theme.primary
font.pixelSize: Theme.fontSizeSmall
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
}
}
}
}

View File

@@ -0,0 +1,207 @@
# Popout Control Example Plugin
This example plugin demonstrates:
- Using `PopoutService` to trigger positioned popouts and modals
- Using `pillClickAction` with position parameters
- Using `PluginSettings` with dropdown selection
- Dynamic widget text based on settings
The `pillClickAction` receives position parameters `(x, y, width, section, screen)` which are passed to PopoutService functions to properly position popouts relative to the widget.
## PopoutService API
The `PopoutService` is automatically injected into plugin widgets and daemons as `popoutService`. It provides access to all shell popouts and modals.
### Available Popouts
#### Control Center
```qml
popoutService.openControlCenter()
popoutService.closeControlCenter()
popoutService.toggleControlCenter()
```
#### Notification Center
```qml
popoutService.openNotificationCenter()
popoutService.closeNotificationCenter()
popoutService.toggleNotificationCenter()
```
#### App Drawer
```qml
popoutService.openAppDrawer()
popoutService.closeAppDrawer()
popoutService.toggleAppDrawer()
```
#### Process List (Popout)
```qml
popoutService.openProcessList()
popoutService.closeProcessList()
popoutService.toggleProcessList()
```
#### DankDash
```qml
popoutService.openDankDash(tabIndex) // tabIndex: 0=Calendar, 1=Media, 2=Weather
popoutService.closeDankDash()
popoutService.toggleDankDash(tabIndex)
```
#### Battery Popout
```qml
popoutService.openBattery()
popoutService.closeBattery()
popoutService.toggleBattery()
```
#### VPN Popout
```qml
popoutService.openVpn()
popoutService.closeVpn()
popoutService.toggleVpn()
```
#### System Update Popout
```qml
popoutService.openSystemUpdate()
popoutService.closeSystemUpdate()
popoutService.toggleSystemUpdate()
```
### Available Modals
#### Settings Modal
```qml
popoutService.openSettings()
popoutService.closeSettings()
```
#### Clipboard History Modal
```qml
popoutService.openClipboardHistory()
popoutService.closeClipboardHistory()
```
#### Spotlight Modal
```qml
popoutService.openSpotlight()
popoutService.closeSpotlight()
```
#### Power Menu Modal
```qml
popoutService.openPowerMenu()
popoutService.closePowerMenu()
popoutService.togglePowerMenu()
```
#### Process List Modal (fullscreen)
```qml
popoutService.showProcessListModal()
popoutService.hideProcessListModal()
popoutService.toggleProcessListModal()
```
#### Color Picker Modal
```qml
popoutService.showColorPicker()
popoutService.hideColorPicker()
```
#### Notification Modal
```qml
popoutService.showNotificationModal()
popoutService.hideNotificationModal()
```
#### WiFi Password Modal
```qml
popoutService.showWifiPasswordModal()
popoutService.hideWifiPasswordModal()
```
#### Network Info Modal
```qml
popoutService.showNetworkInfoModal()
popoutService.hideNetworkInfoModal()
```
#### Notepad Slideout
```qml
popoutService.openNotepad()
popoutService.closeNotepad()
popoutService.toggleNotepad()
```
## Usage in Plugins
### Widget Plugins
```qml
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property var popoutService: null // REQUIRED: Must declare for injection
MouseArea {
anchors.fill: parent
onClicked: {
popoutService?.toggleControlCenter()
}
}
}
```
### Daemon Plugins
```qml
import QtQuick
import qs.Services
Item {
id: root
property var popoutService: null // REQUIRED: Must declare for injection
Connections {
target: NotificationService
function onNotificationReceived() {
popoutService?.openNotificationCenter()
}
}
}
```
**Important**: The `popoutService` property **must** be declared in your plugin component. Without it, you'll get errors like:
```
Error: Cannot assign to non-existent property "popoutService"
```
## Example Use Cases
1. **Custom Launcher**: Create a widget that opens the app drawer
2. **Quick Settings**: Toggle control center from a custom button
3. **Notification Manager**: Open notification center on new notifications
4. **System Monitor**: Open process list on high CPU usage
5. **Power Management**: Trigger power menu from a custom widget
## Installation
1. Copy the plugin directory to `~/.config/DankMaterialShell/plugins/`
2. Open Settings → Plugins
3. Click "Scan for Plugins"
4. Enable "Popout Control Example"
5. Add `popoutControlExample` to your DankBar widget list
## Notes
- The `popoutService` property is automatically injected - no manual setup required
- Always use optional chaining (`?.`) when calling methods to handle null cases
- Popouts are lazily loaded - first access may activate the loader
- Some popouts require specific system features to be available

View File

@@ -0,0 +1,13 @@
{
"id": "popoutControlExample",
"name": "Popout Control Example",
"description": "Example widget demonstrating PopoutService usage with pillClickAction",
"version": "1.0.0",
"author": "DankMaterialShell",
"type": "widget",
"capabilities": ["dankbar-widget"],
"component": "./PopoutControlWidget.qml",
"icon": "widgets",
"settings": "./PopoutControlSettings.qml",
"permissions": ["settings_read", "settings_write"]
}

1480
quickshell/PLUGINS/README.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,132 @@
# Theme Property Reference for Plugins
Quick reference for commonly used Theme properties in plugin development.
## Font Sizes
```qml
Theme.fontSizeSmall // 12px (scaled)
Theme.fontSizeMedium // 14px (scaled)
Theme.fontSizeLarge // 16px (scaled)
Theme.fontSizeXLarge // 20px (scaled)
```
**Note**: These are scaled by `SettingsData.fontScale`
## Icon Sizes
```qml
Theme.iconSizeSmall // 16px
Theme.iconSize // 24px (default)
Theme.iconSizeLarge // 32px
```
## Spacing
```qml
Theme.spacingXS // Extra small
Theme.spacingS // Small
Theme.spacingM // Medium
Theme.spacingL // Large
Theme.spacingXL // Extra large
```
## Border Radius
```qml
Theme.cornerRadius // Standard corner radius
Theme.cornerRadiusSmall // Smaller radius
Theme.cornerRadiusLarge // Larger radius
```
## Colors
### Surface Colors
```qml
Theme.surface
Theme.surfaceContainerLow
Theme.surfaceContainer
Theme.surfaceContainerHigh
Theme.surfaceContainerHighest
```
### Text Colors
```qml
Theme.onSurface // Primary text on surface
Theme.onSurfaceVariant // Secondary text on surface
Theme.outline // Border/divider color
```
### Semantic Colors
```qml
Theme.primary
Theme.onPrimary
Theme.secondary
Theme.onSecondary
Theme.error
Theme.warning
Theme.success
```
### Special Functions
```qml
Theme.popupBackground() // Popup background with opacity
```
## Common Patterns
### Icon with Text
```qml
DankIcon {
name: "icon_name"
color: Theme.onSurface
font.pixelSize: Theme.iconSize
}
StyledText {
text: "Label"
color: Theme.onSurface
font.pixelSize: Theme.fontSizeMedium
}
```
### Container with Border
```qml
Rectangle {
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
}
```
### Hover Effect
```qml
MouseArea {
hoverEnabled: true
onEntered: parent.color = Qt.lighter(Theme.surfaceContainerHigh, 1.1)
onExited: parent.color = Theme.surfaceContainerHigh
}
```
## Common Mistakes
**Wrong**:
```qml
font.pixelSize: Theme.fontSizeS // Property doesn't exist
font.pixelSize: Theme.iconSizeS // Property doesn't exist
```
**Correct**:
```qml
font.pixelSize: Theme.fontSizeSmall // Use full name
font.pixelSize: Theme.iconSizeSmall // Use full name
```
## Checking Available Properties
To see all available Theme properties, check `Common/Theme.qml` or use:
```bash
grep "property" Common/Theme.qml
```

View File

@@ -0,0 +1,37 @@
# Wallpaper Watcher Daemon
Run a script whenever your wallpaper changes.
## What it does
This daemon monitors wallpaper changes and executes a script you specify. The new wallpaper path gets passed as the first argument to your script.
## Setup
1. Enable the plugin in Settings → Plugins
2. Configure the script path in the plugin settings
3. Make sure your script is executable (`chmod +x /path/to/script.sh`)
## Example script
```bash
#!/bin/bash
echo "New wallpaper: $1"
# Do something with the wallpaper path
```
Save this to a file, make it executable, and point the plugin to it.
## Use cases
- Generate color schemes from the new wallpaper
- Update theme files based on wallpaper colors
- Send notifications when wallpaper changes
- Sync wallpaper info to other devices
- Log wallpaper history
## Notes
- Script errors show up as toast notifications
- Script output goes to console logs
- The daemon runs invisibly in the background

View File

@@ -0,0 +1,66 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Modules.Plugins
PluginComponent {
id: root
property string scriptPath: pluginData.scriptPath || ""
property var popoutService: null
Connections {
target: SessionData
function onWallpaperPathChanged() {
if (scriptPath) {
var scriptProcess = scriptProcessComponent.createObject(root, {
wallpaperPath: SessionData.wallpaperPath
})
scriptProcess.running = true
}
}
}
Component {
id: scriptProcessComponent
Process {
property string wallpaperPath: ""
command: [scriptPath, wallpaperPath]
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
console.log("WallpaperWatcherDaemon script output:", text.trim())
}
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
ToastService.showError("Wallpaper Change Script Error", text.trim())
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
ToastService.showError("Wallpaper Change Script Error", "Script exited with code: " + exitCode)
}
destroy()
}
}
}
Component.onCompleted: {
console.info("WallpaperWatcherDaemon: Started monitoring wallpaper changes")
}
Component.onDestruction: {
console.info("WallpaperWatcherDaemon: Stopped monitoring wallpaper changes")
}
}

View File

@@ -0,0 +1,24 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "wallpaperWatcherDaemon"
StyledText {
text: "Wallpaper Change Hook"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StringSetting {
settingKey: "scriptPath"
label: "Script Path"
description: "Path to a script that will be executed when the wallpaper changes. The new wallpaper path will be passed as the first argument."
placeholder: "/path/to/your/script.sh"
defaultValue: ""
}
}

View File

@@ -0,0 +1,16 @@
{
"id": "wallpaperWatcherDaemon",
"name": "Wallpaper Watcher Daemon",
"description": "Background daemon that monitors wallpaper changes and logs them",
"version": "1.0.0",
"author": "DankMaterialShell",
"type": "daemon",
"capabilities": ["wallpaper", "monitoring"],
"component": "./WallpaperWatcherDaemon.qml",
"icon": "wallpaper",
"settings": "./WallpaperWatcherSettings.qml",
"permissions": [
"settings_read",
"settings_write"
]
}

View File

@@ -0,0 +1,115 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://danklinux.com/schemas/plugin.json",
"title": "DankMaterialShell Plugin Manifest",
"description": "Schema for DankMaterialShell plugin.json manifest files",
"type": "object",
"required": [
"id",
"name",
"description",
"version",
"author",
"type",
"capabilities",
"component"
],
"properties": {
"id": {
"type": "string",
"description": "Unique plugin identifier (camelCase, no spaces)",
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$"
},
"name": {
"type": "string",
"description": "Human-readable plugin name",
"minLength": 1
},
"description": {
"type": "string",
"description": "Short description of plugin functionality",
"minLength": 1
},
"version": {
"type": "string",
"description": "Semantic version string (e.g., '1.0.0')",
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$"
},
"author": {
"type": "string",
"description": "Plugin creator name or email",
"minLength": 1
},
"type": {
"type": "string",
"description": "Plugin type",
"enum": ["widget", "daemon", "launcher"]
},
"capabilities": {
"type": "array",
"description": "Array of plugin capabilities",
"items": {
"type": "string"
},
"minItems": 1
},
"component": {
"type": "string",
"description": "Relative path to main QML component file",
"pattern": "^\\./.*\\.qml$"
},
"trigger": {
"type": "string",
"description": "Trigger string for launcher activation (required for launcher type)"
},
"icon": {
"type": "string",
"description": "Material Design icon name"
},
"settings": {
"type": "string",
"description": "Path to settings component QML file",
"pattern": "^\\./.*\\.qml$"
},
"requires_dms": {
"type": "string",
"description": "Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')",
"pattern": "^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"
},
"requires": {
"type": "array",
"description": "Array of required system tools/dependencies",
"items": {
"type": "string"
}
},
"permissions": {
"type": "array",
"description": "Required capabilities",
"items": {
"type": "string",
"enum": [
"settings_read",
"settings_write",
"process",
"network"
]
}
}
},
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "launcher"
}
}
},
"then": {
"required": ["trigger"]
}
}
],
"additionalProperties": true
}