Remove .config/DankMaterialShell/plugins/dankDesktopWeather.meta

Remove .config/DankMaterialShell/plugins/dankHooks.meta
Remove .config/DankMaterialShell/plugins/desktopCommand/LICENSE
Remove .config/DankMaterialShell/plugins/desktopCommand/README.md
Remove .config/DankMaterialShell/plugins/desktopCommand/Settings.qml
Remove .config/DankMaterialShell/plugins/desktopCommand/Widget.qml
Remove .config/DankMaterialShell/plugins/desktopCommand/assets/screenshot.jpg
Remove .config/DankMaterialShell/plugins/desktopCommand/.git/HEAD
Remove .config/DankMaterialShell/plugins/desktopCommand/.git/config
Remove .config/DankMaterialShell/plugins/desktopCommand/.git/index
Remove .config/DankMaterialShell/plugins/desktopCommand/.git/objects/info/.keep
Remove .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.idx
Remove .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.rev
Remove .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.pack
Remove .config/DankMaterialShell/plugins/desktopCommand/.git/refs/heads/main
Remove .config/DankMaterialShell/plugins/desktopCommand/.git/refs/remotes/origin/main
Remove .config/DankMaterialShell/plugins/desktopCommand/.git/refs/tags/.keep
Remove .config/DankMaterialShell/plugins/desktopCommand/.gitignore
Remove .config/DankMaterialShell/plugins/desktopCommand/wrapCommand
Remove .config/DankMaterialShell/plugins/desktopCommand/plugin.json
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/DankActionsSettings.qml
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/DankActionsWidget.qml
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/plugin.json
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/DankBatteryAlerts.qml
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/DankBatteryAlertsSettings.qml
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/plugin.json
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/DankDesktopWeather.qml
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/DankDesktopWeatherSettings.qml
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/plugin.json
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/DankHooks.qml
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/DankHooksSettings.qml
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/README.md
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/plugin.json
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/DankPomodoroSettings.qml
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/DankPomodoroWidget.qml
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/plugin.json
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/LICENSE
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/README.md
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/HEAD
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/config
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/index
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/info/.keep
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.idx
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.rev
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.idx
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.rev
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.idx
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.rev
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.pack
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.pack
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.pack
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/heads/master
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/remotes/origin/master
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/tags/.keep
Remove .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.gitignore
Remove .config/DankMaterialShell/plugins/emojiLauncher/EmojiLauncher.qml
Remove .config/DankMaterialShell/plugins/emojiLauncher/EmojiLauncherSettings.qml
Remove .config/DankMaterialShell/plugins/emojiLauncher/LICENSE
Remove .config/DankMaterialShell/plugins/emojiLauncher/README.md
Remove .config/DankMaterialShell/plugins/emojiLauncher/catalog.js
Remove .config/DankMaterialShell/plugins/emojiLauncher/data/emojis.txt
Remove .config/DankMaterialShell/plugins/emojiLauncher/data/math.txt
Remove .config/DankMaterialShell/plugins/emojiLauncher/data/nerdfont.txt
Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/HEAD
Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/config
Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/index
Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/info/.keep
Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.idx
Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.rev
Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.pack
Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/heads/main
Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/remotes/origin/main
Remove .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/tags/.keep
Remove .config/DankMaterialShell/plugins/emojiLauncher/plugin.json
Remove .config/DankMaterialShell/plugins/emojiLauncher/screenshot.png
Remove .config/DankMaterialShell/plugins/emojiLauncher/scripts/generate_catalog.py
Remove .config/DankMaterialShell/plugins/mediaPlayer/MediaPlayerSettings.qml
Remove .config/DankMaterialShell/plugins/mediaPlayer/MediaPlayerTab.qml
Remove .config/DankMaterialShell/plugins/mediaPlayer/README.md
Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/HEAD
Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/config
Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/index
Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/info/.keep
Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.idx
Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.rev
Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.pack
Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/heads/main
Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/remotes/origin/main
Remove .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/tags/.keep
Remove .config/DankMaterialShell/plugins/mediaPlayer/plugin.json
Remove .config/DankMaterialShell/plugins/mediaPlayer/screenshot_8.png
Remove .config/DankMaterialShell/plugins/dankDesktopWeather
Remove .config/DankMaterialShell/plugins/dankHooks
This commit is contained in:
Lewis Wynne 2026-02-13 13:22:44 +00:00
parent 4ec9bad108
commit 72efdad217
93 changed files with 0 additions and 23485 deletions

View file

@ -1,3 +0,0 @@
repo=https://github.com/AvengeMedia/dms-plugins
path=DankDesktopWeather
repodir=0026f1eba8dedaec

View file

@ -1,3 +0,0 @@
repo=https://github.com/AvengeMedia/dms-plugins
path=DankHooks
repodir=0026f1eba8dedaec

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 yayuuu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,3 +0,0 @@
# desktopCommand
Desktop Command - DMS plugin that allows running shell command and displaying its output on the desktop
![Screenshot](assets/screenshot.jpg)

View file

@ -1,250 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "desktopCommand"
property string defaultCommand: "fastfetch --logo-type builtin"
property int defaultFontSize: Theme.fontSizeSmall
property string command: root.loadValue("command", defaultCommand)
property bool autoRefresh: root.loadValue("autoRefresh", false)
property bool useDank16: root.loadValue("useDank16", true)
property string commandTimeout: String(root.loadValue("commandTimeout", "1"))
property string refreshInterval: String(root.loadValue("refreshInterval", "5"))
property int fontSize: String(root.loadValue("fontSize", defaultFontSize))
property int backgroundOpacity: root.loadValue("backgroundOpacity", 50)
function sanitizeIntInput(textValue, fallback) {
const cleaned = String(textValue ?? "").replace(/[^0-9]/g, "")
return cleaned.length > 0 ? cleaned : String(fallback)
}
function sanitizeDecimalInput(textValue, fallback) {
let cleaned = String(textValue ?? "").replace(/[^0-9.]/g, "")
const dot = cleaned.indexOf(".")
if (dot !== -1) {
cleaned = cleaned.slice(0, dot + 1) + cleaned.slice(dot + 1).replace(/\./g, "")
}
return cleaned.length > 0 ? cleaned : String(fallback)
}
StyledText {
text: I18n.tr("Command Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
Column {
id: content
spacing: Theme.spacingM
anchors.left: parent.left
anchors.right: parent.right
Column {
spacing: Theme.spacingXS
width: parent.width
StyledText {
text: I18n.tr("Shell command")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Shell command to run and display.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
DankTextField {
id: commandField
width: parent.width
height: 40
text: command
placeholderText: defaultCommand
backgroundColor: Theme.surfaceContainer
textColor: Theme.surfaceText
}
}
Column {
spacing: Theme.spacingXS
width: parent.width
StyledText {
text: I18n.tr("Command Timeout (seconds)")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Maximum amount of time to run the command.<br />Important when running commands taht never exit, like TUI apps.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
DankTextField {
id: timeoutField
width: parent.width
height: 40
text: commandTimeout
placeholderText: "1"
backgroundColor: Theme.surfaceContainer
textColor: Theme.surfaceText
onEditingFinished: {
commandTimeout = sanitizeDecimalInput(text, "1")
text = commandTimeout
}
}
}
Column {
spacing: Theme.spacingS
width: parent.width
CheckBox {
id: autoRefreshToggle
checked: autoRefresh
contentItem: StyledText {
text: I18n.tr("Auto Refresh")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
leftPadding: autoRefreshToggle.indicator.width + 8
verticalAlignment: Text.AlignVCenter
}
}
StyledText {
text: I18n.tr("Automatically rerun the command on the chosen interval.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - autoRefreshToggle.width - Theme.spacingS
}
}
Column {
spacing: Theme.spacingXS
width: parent.width
StyledText {
text: I18n.tr("Refresh Interval (seconds)")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
opacity: autoRefreshToggle.checked ? 1.0 : 0.3
}
StyledText {
text: I18n.tr("How often to rerun the command (supports decimals).")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
opacity: autoRefreshToggle.checked ? 1.0 : 0.3
}
DankTextField {
id: intervalField
width: parent.width
height: 40
text: refreshInterval
placeholderText: "5"
backgroundColor: Theme.surfaceContainer
textColor: Theme.surfaceText
enabled: autoRefreshToggle.checked
opacity: autoRefreshToggle.checked ? 1.0 : 0.3
onEditingFinished: {
refreshInterval = sanitizeDecimalInput(text, "5")
text = refreshInterval
}
}
}
DankButton {
text: I18n.tr("Save command settings")
width: parent.width
onClicked: {
command = commandField.text.trim()
root.saveValue("command", command)
root.saveValue("autoRefresh", autoRefreshToggle.checked)
commandTimeout = sanitizeDecimalInput(timeoutField.text, "1")
root.saveValue("commandTimeout", commandTimeout)
refreshInterval = sanitizeDecimalInput(intervalField.text, "5")
root.saveValue("refreshInterval", refreshInterval)
commandField.text = command
timeoutField.text = commandTimeout
intervalField.text = refreshInterval
}
}
Column {
topPadding: Theme.spacingL*2
spacing: Theme.spacingXS
width: parent.width
StyledText {
text: I18n.tr("Appearance Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
Item {
width: parent.width
height: Theme.spacingM
}
SliderSetting {
settingKey: "fontSize"
label: I18n.tr("Font size (px)")
description: I18n.tr("Default monospace font is being used,<br />but you can set a custom size.")
defaultValue: fontSize
minimum: 8
maximum: 100
unit: "px"
}
Item {
width: parent.width
height: Theme.spacingM
}
SliderSetting {
settingKey: "backgroundOpacity"
label: I18n.tr("Background Opacity")
defaultValue: backgroundOpacity
minimum: 0
maximum: 100
unit: "%"
}
Item {
width: parent.width
height: Theme.spacingM
}
ToggleSetting {
settingKey: "useDank16"
label: I18n.tr("Use Dank16 Colorscheme")
description: I18n.tr("Will be applied after the next refresh.")
defaultValue: useDank16
}
}
}
}

View file

@ -1,229 +0,0 @@
import QtQuick
import QtQml
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modules.Plugins
DesktopPluginComponent {
id: root
property string command: pluginData.command ?? ""
property real refreshInterval: normalizeRefreshInterval(pluginData.refreshInterval)
property bool autoRefresh: pluginData.autoRefresh ?? false
property real commandTimeout: normalizeCommandTimeout(pluginData.commandTimeout) // seconds
property bool hasRunInitial: false
property string output: ""
property int rows: 0
property int cols: 0
property var windowRef: null
property int fontSizePx: normalizeFontSize(pluginData.fontSize)
property bool useDank16: (pluginData.useDank16 ?? true) && Theme.dank16 !== null
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 50) / 100
property string pluginUrl: ""
property string pluginDir: ""
property string wrapCommandPath: ""
property var dank16: Theme.isLightMode ? Theme.dank16.light : Theme.dank16.dark
FontMetrics {
id: fontMetrics
font.pixelSize: root.fontSizePx
font.family: Theme.monoFontFamily
}
Timer {
id: timer
interval: root.refreshInterval
repeat: true
running: false
onTriggered: runCommand()
}
// workaround for widget being spawned with weird size initially
Timer {
id: initialRunTimer
interval: 1000
repeat: false
running: false
onTriggered: root.handleVisibilityChange("timer")
}
Component.onCompleted: {
root.windowRef = Window.window ?? null
root.handleVisibilityChange("completed")
const url = Qt.resolvedUrl("Widget.qml") || (typeof __qmlfile__ !== "undefined" ? __qmlfile__ : "")
const cleanedUrl = String(url ?? "")
const cleanedPath = cleanedUrl.startsWith("file://") ? cleanedUrl.slice("file://".length) : cleanedUrl
const lastSlash = cleanedPath.lastIndexOf("/")
root.pluginUrl = cleanedUrl
root.pluginDir = lastSlash !== -1 ? cleanedPath.slice(0, lastSlash) : ""
const resolvedWrapUrl = Qt.resolvedUrl("wrapCommand")
const resolvedWrap = String(resolvedWrapUrl ?? "")
root.wrapCommandPath = resolvedWrap
? resolvedWrap.replace(/^file:\/\//, "")
: (root.pluginDir ? `${root.pluginDir}/wrapCommand` : "wrapCommand")
}
onVisibleChanged: {
root.handleVisibilityChange("root.visible")
}
onWidgetWidthChanged: root.handleVisibilityChange("sizeChanged")
onWidgetHeightChanged: root.handleVisibilityChange("sizeChanged")
Component.onDestruction: {
root.stopAllActivity("destruction")
}
onCommandChanged: {
handleVisibilityChange("commandChanged")
}
onAutoRefreshChanged: {
timer.running = root.autoRefresh && root.isRunnable()
if (root.autoRefresh && root.hasRunInitial && root.isRunnable()) {
timer.restart()
}
}
onRefreshIntervalChanged: {
if (timer.running) {
timer.restart()
}
}
function normalizeRefreshInterval(value) {
const parsed = Number(value)
if (!isFinite(parsed) || parsed <= 0) {
return 60000
}
return parsed * 1000
}
function normalizeCommandTimeout(value) {
const parsed = Number(value)
if (!isFinite(parsed) || parsed <= 0) {
return 5
}
return parsed
}
function normalizeFontSize(value) {
const parsed = parseInt(value, 10)
if (!isFinite(parsed) || parsed <= 0) {
return Theme.fontSizeSmall
}
return parsed
}
function isRunnable() {
const win = root.windowRef
const winVisible = win === null ? true : !!win.visible
// in other weird cases, it will just start on timer with 2s delay
return root.visible && winVisible && root.widgetWidth > 0 && root.widgetHeight > 0
}
function isStartingEdgeCase(){
if (root.widgetWidth == 500 || root.widgetWidth == 200) {
initialRunTimer.start()
return true
}
if (root.widgetHeight == 500 || root.widgetHeight == 200) {
initialRunTimer.start()
return true
}
return false
}
function handleVisibilityChange(source) {
if(source == "commandChanged" || source == "root.visible"){
root.hasRunInitial = false
}
if (!root.isRunnable()) {
root.stopAllActivity(source)
return
}
if (!root.hasRunInitial) {
if(root.isStartingEdgeCase() && source != "timer"){
return
}
root.hasRunInitial = true
initialRunTimer.stop()
runCommand()
}
if (root.autoRefresh) {
timer.start()
}
}
function runCommand() {
if (!root.isRunnable()) {
console.warn(`[desktopCommand] runCommand skipped; not runnable (visible=${root.visible} winVisible=${root.windowRef ? root.windowRef.visible : "n/a"}`)
return
}
if (process.running) {
console.warn(`[desktopCommand] runCommand skipped; process already running; command="${root.command}"`)
return
}
root.updateTerminalSize()
let command = `"${root.wrapCommandPath}" --width=${root.cols} --height=${root.rows} --timeout=${root.commandTimeout} `
if (root.useDank16) {
command += `--colors='${JSON.stringify(root.dank16)}' `
}
command += `-- ${JSON.stringify(root.command)}`
process.command = ["sh", "-c", command]
process.running = true
}
function updateTerminalSize() {
const horizontalMargin = 0
const verticalMargin = 8
const availableWidth = Math.max(200, (root.widgetWidth ?? root.width) - horizontalMargin)
const availableHeight = Math.max(200, (root.widgetHeight ?? root.height) - verticalMargin)
root.cols = Math.max(1, Math.floor(availableWidth / Math.max(1, fontMetrics.averageCharacterWidth)))
root.rows = Math.max(1, Math.floor(availableHeight / Math.max(1, fontMetrics.lineSpacing)))
}
function stopAllActivity(reason) {
timer.stop()
process.running = false
root.output = ""
}
Process {
id: process
stdout: StdioCollector {
onStreamFinished: {
root.output = this.text
}
}
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, root.backgroundOpacity)
visible: root.visible
Text {
anchors.fill: parent
anchors.margins: 8
text: root.output
textFormat: Text.RichText
wrapMode: Text.NoWrap
color: useDank16? Theme.surfaceText : "#c0c0c0"
font.pixelSize: root.fontSizePx
font.family: Theme.monoFontFamily
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignTop
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

View file

@ -1 +0,0 @@
ref: refs/heads/main

View file

@ -1,9 +0,0 @@
[core]
bare = false
filemode = true
[remote "origin"]
url = https://github.com/yayuuu/desktopCommand
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main

View file

@ -1 +0,0 @@
0ff6dad312fa8532e4e152eb37507a11f2fd4663

View file

@ -1 +0,0 @@
0ff6dad312fa8532e4e152eb37507a11f2fd4663

View file

@ -1,493 +0,0 @@
#!/usr/bin/env python3
"""
Runs a command inside a pseudo-terminal with a fixed size and timeout, then
converts its ANSI output to HTML.
"""
import argparse
import fcntl
import json
import os
import pty
import re
import select
import signal
import shlex
import struct
import subprocess
import termios
import time
import sys
from typing import List, Dict, Any, Optional
DEFAULT_BASIC_COLORS = (
"#000000",
"#800000",
"#008000",
"#808000",
"#000080",
"#800080",
"#008080",
"#c0c0c0",
)
DEFAULT_BRIGHT_COLORS = (
"#808080",
"#ff0000",
"#00ff00",
"#ffff00",
"#0000ff",
"#ff00ff",
"#00ffff",
"#ffffff",
)
BASIC_COLORS = DEFAULT_BASIC_COLORS
BRIGHT_COLORS = DEFAULT_BRIGHT_COLORS
ALL_COLORS = BASIC_COLORS + BRIGHT_COLORS
def set_color_palette(color_map: Dict[str, Any]) -> None:
values = []
for i in range(16):
val = color_map.get(f"color{i}")
if not (isinstance(val, str) and re.match(r"^#[0-9a-fA-F]{6}$", val)):
return
values.append(val)
global BASIC_COLORS, BRIGHT_COLORS, ALL_COLORS
BASIC_COLORS = tuple(values[:8])
BRIGHT_COLORS = tuple(values[8:])
ALL_COLORS = BASIC_COLORS + BRIGHT_COLORS
def create_state() -> Dict[str, Any]:
return {
"row": 0,
"col": 0,
"lines": [[]],
"fg": None,
"bg": None,
"bold": False,
"ignoreClearsAfterAltExit": False,
"savedRow": 0,
"savedCol": 0,
}
def escape_html(text: str) -> str:
return (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
)
def ansi256_to_hex(code: str):
try:
c = int(code, 10)
except ValueError:
return None
if 0 <= c < 16:
return ALL_COLORS[c]
if 16 <= c <= 231:
idx = c - 16
r = idx // 36
g = (idx % 36) // 6
b = idx % 6
values = [r, g, b]
comps = [0 if n == 0 else 55 + n * 40 for n in values]
return "#" + "".join(f"{v:02x}" for v in comps)
if 232 <= c <= 255:
level = 8 + (c - 232) * 10
return f"#{level:02x}{level:02x}{level:02x}"
return None
def basic_color(code: str):
try:
c = int(code, 10)
except ValueError:
return None
if 30 <= c <= 37:
return BASIC_COLORS[c - 30]
if 90 <= c <= 97:
return BRIGHT_COLORS[c - 90]
if 40 <= c <= 47:
return BASIC_COLORS[c - 40]
if 100 <= c <= 107:
return BRIGHT_COLORS[c - 100]
return None
def rgb_to_hex(r: str, g: str, b: str):
try:
values = [int(r), int(g), int(b)]
except ValueError:
return None
if any(v < 0 or v > 255 for v in values):
return None
return "#" + "".join(f"{v:02x}" for v in values)
def current_style(state: Dict[str, Any]) -> str:
parts: List[str] = []
if state["fg"]:
parts.append(f"color:{state['fg']}")
if state["bg"]:
parts.append(f"background-color:{state['bg']}")
if state["bold"]:
parts.append("font-weight:bold")
return ";".join(parts)
def ensure_line(state: Dict[str, Any], row: int) -> None:
while len(state["lines"]) <= row:
state["lines"].append([])
def set_cursor(state: Dict[str, Any], row: int, col: int) -> None:
state["row"] = max(0, row)
state["col"] = max(0, col)
ensure_line(state, state["row"])
def apply_sgr(state: Dict[str, Any], code_str: str) -> None:
codes = ["0"] if code_str == "" else [c for c in code_str.split(";") if c != ""]
i = 0
while i < len(codes):
code_num = codes[i]
if code_num == "?25":
i += 1
continue
num = int(code_num) if code_num.isdigit() else None
if num == 0:
state["fg"] = None
state["bg"] = None
state["bold"] = False
elif num == 1:
state["bold"] = True
elif num == 22:
state["bold"] = False
elif num == 39:
state["fg"] = None
elif num == 49:
state["bg"] = None
elif num == 38 and i + 1 < len(codes) and codes[i + 1] == "5":
state["fg"] = ansi256_to_hex(codes[i + 2]) if i + 2 < len(codes) else None
i += 2
elif num == 48 and i + 1 < len(codes) and codes[i + 1] == "5":
state["bg"] = ansi256_to_hex(codes[i + 2]) if i + 2 < len(codes) else None
i += 2
elif num == 38 and i + 3 < len(codes) and codes[i + 1] == "2":
state["fg"] = rgb_to_hex(codes[i + 2], codes[i + 3], codes[i + 4]) if i + 4 < len(codes) else None
i += 4
elif num == 48 and i + 3 < len(codes) and codes[i + 1] == "2":
state["bg"] = rgb_to_hex(codes[i + 2], codes[i + 3], codes[i + 4]) if i + 4 < len(codes) else None
i += 4
elif num is not None and ((30 <= num <= 37) or (90 <= num <= 97)):
state["fg"] = basic_color(str(num))
elif num is not None and ((40 <= num <= 47) or (100 <= num <= 107)):
state["bg"] = basic_color(str(num))
i += 1
def apply_csi(state: Dict[str, Any], params: Optional[str], code: str) -> None:
if params is None:
cleaned = ""
parts: List[str] = []
else:
cleaned = re.sub(r"\?", "", params)
parts = [] if cleaned == "" else [p for p in cleaned.split(";") if p != ""]
first = int(parts[0]) if parts and parts[0].isdigit() else None
mode = parts[0] if parts else ""
is_private = params is not None and "?" in params
if code == "m":
apply_sgr(state, cleaned)
elif code == "d":
set_cursor(state, max(0, (first or 1) - 1), state["col"])
elif code == "a":
set_cursor(state, state["row"], state["col"] + (first or 1))
elif code == "e":
set_cursor(state, state["row"] + (first or 1), state["col"])
elif code == "`":
set_cursor(state, state["row"], max(0, (first or 1) - 1))
elif code in ("h", "l") and mode == "1049":
if code == "h":
state["lines"] = [[]]
set_cursor(state, 0, 0)
state["ignoreClearsAfterAltExit"] = False
else:
set_cursor(state, 0, 0)
state["ignoreClearsAfterAltExit"] = True
elif code in ("h", "l") and is_private:
pass
elif code in ("h", "l"):
pass
elif code == "s":
state["savedRow"] = state["row"]
state["savedCol"] = state["col"]
elif code == "u":
set_cursor(state, state.get("savedRow", 0), state.get("savedCol", 0))
elif code == "A":
set_cursor(state, state["row"] - (first or 1), state["col"])
elif code == "B":
set_cursor(state, state["row"] + (first or 1), state["col"])
elif code == "C":
set_cursor(state, state["row"], state["col"] + (first or 1))
elif code == "D":
if params is None:
set_cursor(state, state["row"] + 1, state["col"])
else:
set_cursor(state, state["row"], max(0, state["col"] - (first or 1)))
elif code == "G":
set_cursor(state, state["row"], max(0, (first or 1) - 1))
elif code in ("H", "f"):
row_val = first or 1
col_val = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 1
set_cursor(state, row_val - 1, max(0, col_val - 1))
elif code == "E":
if params is None:
set_cursor(state, state["row"] + 1, 0)
else:
set_cursor(state, state["row"] + (first or 1), 0)
elif code == "F":
set_cursor(state, max(0, state["row"] - (first or 1)), 0)
elif code == "M":
if params is None:
set_cursor(state, max(0, state["row"] - 1), state["col"])
else:
delete_count = first or 1
for _ in range(delete_count):
if state["row"] < len(state["lines"]):
state["lines"].pop(state["row"])
state["lines"].append([])
ensure_line(state, state["row"])
elif code == "K":
ensure_line(state, state["row"])
line = state["lines"][state["row"]]
start = state["col"]
for i in range(start, len(line)):
line[i] = {"ch": " ", "style": ""}
elif code == "J":
ensure_line(state, state["row"])
mode_num = 0 if first is None else first
if not state["ignoreClearsAfterAltExit"]:
if mode_num == 2:
state["lines"] = [[]]
set_cursor(state, 0, 0)
else:
for r in range(state["row"], len(state["lines"])):
line = state["lines"][r]
start = state["col"] if r == state["row"] else 0
for i in range(start, len(line)):
line[i] = {"ch": " ", "style": ""}
elif code == "r":
set_cursor(state, 0, 0)
elif code == "t":
pass
def write_char(state: Dict[str, Any], ch: str) -> None:
ensure_line(state, state["row"])
line = state["lines"][state["row"]]
while len(line) <= state["col"]:
line.append({"ch": " ", "style": ""})
line[state["col"]] = {"ch": ch, "style": current_style(state)}
state["col"] += 1
def append_text(state: Dict[str, Any], text: str) -> None:
for ch in text:
if ch == "\n":
set_cursor(state, state["row"] + 1, 0)
elif ch == "\r":
set_cursor(state, state["row"], 0)
elif ch == "\b":
set_cursor(state, state["row"], max(0, state["col"] - 1))
elif ch == "\t":
next_stop = ((state["col"] // 8) + 1) * 8
set_cursor(state, state["row"], next_stop)
else:
write_char(state, ch)
def parse_ansi(state: Dict[str, Any], text: str) -> None:
normalized = text.replace("\r\n", "\n").replace("\r", "\n")
index = 0
while index < len(normalized):
esc_index = normalized.find("\x1b", index)
chunk = normalized[index:] if esc_index == -1 else normalized[index:esc_index]
append_text(state, chunk)
if esc_index == -1:
break
slice_text = normalized[esc_index:]
csi_match = re.match(r"^\x1b\[([0-9?;]*)([A-Za-z])", slice_text)
if csi_match:
apply_csi(state, csi_match.group(1), csi_match.group(2))
index = esc_index + len(csi_match.group(0))
continue
esc_move = re.match(r"^\x1b([DEM])", slice_text)
if esc_move:
apply_csi(state, None, esc_move.group(1))
index = esc_index + len(esc_move.group(0))
continue
simple_esc = re.match(r"^\x1b[\(\)][A-Za-z0-9]", slice_text)
if simple_esc:
index = esc_index + len(simple_esc.group(0))
continue
one_char_esc = re.match(r"^\x1b[][><=]", slice_text)
if one_char_esc:
index = esc_index + len(one_char_esc.group(0))
continue
index = esc_index + 1
def lines_to_html(lines: List[List[Dict[str, str]]]) -> str:
def render_line(line: List[Dict[str, str]]) -> str:
if not line:
return "&nbsp;"
html_parts: List[str] = []
open_style = ""
for cell in line:
cell = cell or {"ch": " ", "style": ""}
safe_char = "&nbsp;" if cell["ch"] == " " else escape_html(cell["ch"])
if cell["style"] != open_style:
if open_style:
html_parts.append("</span>")
if cell["style"]:
html_parts.append(f'<span style="{cell["style"]}">')
open_style = cell["style"]
html_parts.append(safe_char)
if open_style:
html_parts.append("</span>")
return "".join(html_parts)
return "<br/>".join(render_line(line) for line in lines)
def strip_unhandled_control_codes(raw_text: str) -> str:
# Remove control characters we don't explicitly handle (e.g. SI/SO 0x0e/0x0f) to avoid stray glyphs
# Keep controls we do handle like backspace (0x08), tab/newline/carriage return.
return re.sub(r"[\x00-\x07\x0b-\x0c\x0e-\x0f\x10-\x1a\x1c-\x1f\x7f]", "", raw_text)
def ansi_to_html(raw_text: str) -> str:
state = create_state()
parse_ansi(state, strip_unhandled_control_codes(raw_text))
if len(state["lines"]) > 1 and state["lines"][1]:
first_cell = state["lines"][1][0]
if first_cell.get("ch") == ">":
state["lines"][1][0] = {"ch": " ", "style": first_cell.get("style", "")}
return lines_to_html(state["lines"])
def parse_args():
parser = argparse.ArgumentParser(description="Wrap a command and convert ANSI output to HTML.")
parser.add_argument("--width", type=int, required=True, help="Pseudo-terminal width (columns).")
parser.add_argument("--height", type=int, required=True, help="Pseudo-terminal height (rows).")
parser.add_argument("--timeout", type=float, required=True, help="Timeout in seconds.")
parser.add_argument("--raw", type=int, default=0, help="Return raw command output when set to 1.")
parser.add_argument(
"--colors",
type=str,
help="JSON map of color0-color15 to hex values for the 16-color palette.",
)
parser.add_argument("cmd", nargs=argparse.REMAINDER, help="Command to run after '--'.")
args = parser.parse_args()
command = args.cmd
if command and command[0] == "--":
command = command[1:]
if not command:
parser.error("No command specified. Usage: wrapCommand --width=80 --height=40 --timeout=3 -- cmd")
if len(command) == 1:
single = command[0]
if re.search(r"[|&;<>()$`!{}\n]", single):
command = ["sh", "-c", single]
else:
try:
split_cmd = shlex.split(single)
if split_cmd:
command = split_cmd
except ValueError:
pass
return args.width, args.height, args.timeout, bool(args.raw), command, args.colors
def set_winsize(fd: int, rows: int, cols: int) -> None:
packed = struct.pack("HHHH", rows, cols, 0, 0)
if hasattr(termios, "TIOCSWINSZ"):
fcntl.ioctl(fd, termios.TIOCSWINSZ, packed)
def run_command(cols: int, rows: int, timeout_sec: float, command: List[str]) -> str:
master_fd, slave_fd = pty.openpty()
try:
set_winsize(slave_fd, rows, cols)
proc = subprocess.Popen(
command,
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
close_fds=True,
env={**os.environ, "TERM": "xterm-256color"},
)
finally:
os.close(slave_fd)
output = bytearray()
deadline = time.time() + timeout_sec
killed = False
while True:
if not killed and time.time() >= deadline and proc.poll() is None:
os.kill(proc.pid, signal.SIGKILL)
killed = True
rlist, _, _ = select.select([master_fd], [], [], 0.1)
if rlist:
try:
chunk = os.read(master_fd, 4096)
except OSError:
break
if chunk:
output.extend(chunk)
continue
break
if proc.poll() is not None:
drain, _, _ = select.select([master_fd], [], [], 0)
if not drain:
break
try:
proc.wait(timeout=0)
except subprocess.TimeoutExpired:
pass
finally:
os.close(master_fd)
return output.decode(errors="replace")
def main():
width, height, timeout_sec, raw_output, command, colors_json = parse_args()
if colors_json:
try:
parsed_colors = json.loads(colors_json)
if isinstance(parsed_colors, dict):
set_color_palette(parsed_colors)
except json.JSONDecodeError:
pass
raw_text = run_command(width, height, timeout_sec, command)
if raw_output:
sys.stdout.write(raw_text)
else:
html = ansi_to_html(raw_text)
print(html)
if __name__ == "__main__":
main()

View file

@ -1,14 +0,0 @@
{
"id": "desktopCommand",
"name": "Desktop Command",
"description": "A widget that displays a command output on your desktop",
"version": "1.2.5",
"author": "yayuuu",
"type": "desktop",
"capabilities": ["desktop-widget"],
"component": "./Widget.qml",
"icon": "terminal",
"settings": "./Settings.qml",
"requires_dms": ">=1.2.0",
"permissions": ["settings_read", "settings_write"]
}

View file

@ -1,734 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "dankActions"
property string editingVariantId: ""
function loadVariantForEditing(variantData) {
editingVariantId = variantData.id || "";
nameField.text = variantData.name || "";
iconField.text = variantData.icon || "";
displayTextField.text = variantData.displayText || "";
displayCommandField.text = variantData.displayCommand || "";
clickCommandField.text = variantData.clickCommand || "";
middleClickCommandField.text = variantData.middleClickCommand || "";
rightClickCommandField.text = variantData.rightClickCommand || "";
updateIntervalField.text = (variantData.updateInterval || 0).toString();
showIconToggle.checked = variantData.showIcon !== undefined ? variantData.showIcon : true;
showTextToggle.checked = variantData.showText !== undefined ? variantData.showText : true;
visibilityCommandField.text = variantData.visibilityCommand || "";
visibilityIntervalField.text = (variantData.visibilityInterval || 0).toString();
}
function clearForm() {
editingVariantId = "";
nameField.text = "";
iconField.text = "";
displayTextField.text = "";
displayCommandField.text = "";
clickCommandField.text = "";
middleClickCommandField.text = "";
rightClickCommandField.text = "";
updateIntervalField.text = "0";
showIconToggle.checked = true;
showTextToggle.checked = true;
visibilityCommandField.text = "";
visibilityIntervalField.text = "0";
}
StyledText {
width: parent.width
text: "Custom Action Manager"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Create custom widgets that execute commands and display dynamic output"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
StyledRect {
width: parent.width
height: addActionColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: addActionColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: root.editingVariantId ? "Edit Action" : "Create New Action"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
DankButton {
text: "Cancel"
iconName: "close"
visible: root.editingVariantId !== ""
onClicked: root.clearForm()
}
}
Row {
width: parent.width
spacing: Theme.spacingM
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: "Variant Name *"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: nameField
width: parent.width
placeholderText: "e.g., Power Profile"
keyNavigationTab: iconField
onFocusStateChanged: hasFocus => {
if (hasFocus)
root.ensureItemVisible(nameField);
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: "Icon Name"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: iconField
width: parent.width
placeholderText: "e.g., power_settings_new"
keyNavigationBacktab: nameField
keyNavigationTab: displayTextField
onFocusStateChanged: hasFocus => {
if (hasFocus)
root.ensureItemVisible(iconField);
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Static Display Text (optional)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: displayTextField
width: parent.width
placeholderText: "Text to show (or leave empty if using command output)"
keyNavigationBacktab: iconField
keyNavigationTab: displayCommandField
onFocusStateChanged: hasFocus => {
if (hasFocus)
root.ensureItemVisible(displayTextField);
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Display Command (optional)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: displayCommandField
width: parent.width
placeholderText: 'e.g., echo "Hello World" or powerprofilesctl get'
keyNavigationBacktab: displayTextField
keyNavigationTab: clickCommandField
onFocusStateChanged: hasFocus => {
if (hasFocus)
root.ensureItemVisible(displayCommandField);
}
}
StyledText {
text: "Command output will replace static text. Runs on widget load."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Left Click Command (optional)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: clickCommandField
width: parent.width
placeholderText: "e.g., notify-send 'Clicked!' or cycle-power-profile.sh"
keyNavigationBacktab: displayCommandField
keyNavigationTab: middleClickCommandField
onFocusStateChanged: hasFocus => {
if (hasFocus)
root.ensureItemVisible(clickCommandField);
}
}
StyledText {
text: "Command to run on left click. After completion, display command refreshes."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Middle Click Command (optional)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: middleClickCommandField
width: parent.width
placeholderText: "e.g., notify-send 'Middle clicked!'"
keyNavigationBacktab: clickCommandField
keyNavigationTab: rightClickCommandField
onFocusStateChanged: hasFocus => {
if (hasFocus)
root.ensureItemVisible(middleClickCommandField);
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Right Click Command (optional)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: rightClickCommandField
width: parent.width
placeholderText: "e.g., notify-send 'Right clicked!'"
keyNavigationBacktab: middleClickCommandField
keyNavigationTab: updateIntervalField
onFocusStateChanged: hasFocus => {
if (hasFocus)
root.ensureItemVisible(rightClickCommandField);
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Update Interval (seconds, 0 = disabled)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: updateIntervalField
width: parent.width
placeholderText: "0"
text: "0"
keyNavigationBacktab: rightClickCommandField
keyNavigationTab: visibilityCommandField
onFocusStateChanged: hasFocus => {
if (hasFocus)
root.ensureItemVisible(updateIntervalField);
}
}
StyledText {
text: "Automatically re-run display command every N seconds. Set to 0 to disable."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Visibility Condition (optional)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: visibilityCommandField
width: parent.width
placeholderText: "e.g., pgrep -x firefox (exit 0 = show, non-zero = hide)"
keyNavigationBacktab: updateIntervalField
keyNavigationTab: visibilityIntervalField
onFocusStateChanged: hasFocus => {
if (hasFocus)
root.ensureItemVisible(visibilityCommandField);
}
}
StyledText {
text: "Widget shows when command exits 0, hides when non-zero."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Visibility Check Interval (seconds, 0 = once on load)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: visibilityIntervalField
width: parent.width
placeholderText: "0"
text: "0"
keyNavigationBacktab: visibilityCommandField
onFocusStateChanged: hasFocus => {
if (hasFocus)
root.ensureItemVisible(visibilityIntervalField);
}
}
StyledText {
text: "How often to re-check visibility condition. Set to 0 to check only once."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
Row {
width: parent.width
spacing: Theme.spacingL
Column {
spacing: Theme.spacingXS
StyledText {
text: "Show Icon"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankToggle {
id: showIconToggle
checked: true
onToggled: isChecked => {
checked = isChecked;
}
}
}
Column {
spacing: Theme.spacingXS
StyledText {
text: "Show Text"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankToggle {
id: showTextToggle
checked: true
onToggled: isChecked => {
checked = isChecked;
}
}
}
}
DankButton {
text: root.editingVariantId ? "Update Action" : "Create Action"
iconName: root.editingVariantId ? "check" : "add"
onClicked: {
if (!nameField.text) {
ToastService.showError("Please enter a variant name");
return;
}
var interval = parseInt(updateIntervalField.text) || 0;
if (interval < 0) {
ToastService.showError("Update interval must be 0 or greater");
return;
}
var visInterval = parseInt(visibilityIntervalField.text) || 0;
if (visInterval < 0) {
ToastService.showError("Visibility interval must be 0 or greater");
return;
}
var variantConfig = {
icon: iconField.text || "terminal",
displayText: displayTextField.text || "",
displayCommand: displayCommandField.text || "",
clickCommand: clickCommandField.text || "",
middleClickCommand: middleClickCommandField.text || "",
rightClickCommand: rightClickCommandField.text || "",
updateInterval: interval,
showIcon: showIconToggle.checked,
showText: showTextToggle.checked,
visibilityCommand: visibilityCommandField.text || "",
visibilityInterval: visInterval
};
if (root.editingVariantId) {
variantConfig.name = nameField.text;
updateVariant(root.editingVariantId, variantConfig);
} else {
createVariant(nameField.text, variantConfig);
}
root.clearForm();
}
}
}
}
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 Actions"
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: root.variantsModel
delegate: StyledRect {
required property var model
width: ListView.view.width
height: variantColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: variantMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainer
Column {
id: variantColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingM
Item {
width: Theme.iconSize
height: Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: model.icon || "terminal"
size: Theme.iconSize
color: Theme.surfaceText
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: parent.width - Theme.iconSize - (editButton.width + deleteButton.width + Theme.spacingXS) - Theme.spacingM * 3
StyledText {
text: model.name || "Unnamed"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: {
var parts = [];
if (model.displayText)
parts.push("Text: " + model.displayText);
if (model.displayCommand)
parts.push("Cmd: " + model.displayCommand);
if (model.updateInterval && model.updateInterval > 0)
parts.push("Update: " + model.updateInterval + "s");
if (model.visibilityCommand)
parts.push("Vis: " + model.visibilityCommand);
return parts.join(" | ") || "No display config";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: {
var actions = [];
if (model.clickCommand)
actions.push("L: " + model.clickCommand);
if (model.middleClickCommand)
actions.push("M: " + model.middleClickCommand);
if (model.rightClickCommand)
actions.push("R: " + model.rightClickCommand);
return actions.join(" | ") || "No click actions";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
elide: Text.ElideRight
}
}
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Rectangle {
id: editButton
width: 32
height: 32
radius: 16
color: editArea.containsMouse ? Theme.primary : "transparent"
DankIcon {
anchors.centerIn: parent
name: "edit"
size: 16
color: editArea.containsMouse ? Theme.onPrimary : Theme.surfaceVariantText
}
MouseArea {
id: editArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.loadVariantForEditing(model);
}
}
}
Rectangle {
id: deleteButton
width: 32
height: 32
radius: 16
color: deleteArea.containsMouse ? Theme.error : "transparent"
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);
}
}
}
}
}
}
MouseArea {
id: variantMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
}
StyledText {
anchors.centerIn: parent
text: "No actions created yet"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: variantsModel.count === 0
}
}
}
}
StyledRect {
width: parent.width
height: examplesColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surface
Column {
id: examplesColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
spacing: Theme.spacingM
DankIcon {
name: "lightbulb"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Usage Examples"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "• Power Profile Cycler"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Display: powerprofilesctl get\nClick: powerprofilesctl set $(powerprofilesctl list | grep -v \"$(powerprofilesctl get)\" | head -1)"
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
width: parent.width
leftPadding: Theme.spacingM
}
StyledText {
text: "• System Uptime"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Display: uptime -p | sed 's/up //'\nClick: notify-send \"Uptime\" \"$(uptime -p)\""
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
width: parent.width
leftPadding: Theme.spacingM
}
StyledText {
text: "• Custom Greeting"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Display: echo \"Hello $(whoami)!\"\nClick: notify-send \"Hi!\" \"Welcome back!\""
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
width: parent.width
leftPadding: Theme.spacingM
}
}
StyledText {
text: "After creating actions, add them to your bar from Bar Settings → Add Widget"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}

View file

@ -1,265 +0,0 @@
import QtQuick
import Quickshell.Io
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 displayIcon: "terminal"
property string displayText: ""
property string displayCommand: ""
property string clickCommand: ""
property string middleClickCommand: ""
property string rightClickCommand: ""
property int updateInterval: 0
property bool showIcon: true
property bool showText: true
property string currentOutput: ""
property bool isLoading: false
visibilityCommand: variantData?.visibilityCommand || ""
visibilityInterval: variantData?.visibilityInterval || 0
onVariantDataChanged: {
updatePropertiesFromVariantData();
}
Connections {
target: PluginService
function onPluginDataChanged(changedPluginId) {
if (changedPluginId === "dankActions" && variantId) {
const newData = PluginService.getPluginVariantData("dankActions", variantId);
if (newData) {
variantData = newData;
}
}
}
}
function updatePropertiesFromVariantData() {
if (!variantData) {
displayIcon = "terminal";
displayText = "";
displayCommand = "";
clickCommand = "";
middleClickCommand = "";
rightClickCommand = "";
updateInterval = 0;
showIcon = true;
showText = true;
currentOutput = "";
return;
}
displayIcon = variantData.icon || "terminal";
displayText = variantData.displayText || "";
displayCommand = variantData.displayCommand || "";
clickCommand = variantData.clickCommand || "";
middleClickCommand = variantData.middleClickCommand || "";
rightClickCommand = variantData.rightClickCommand || "";
updateInterval = variantData.updateInterval || 0;
showIcon = variantData.showIcon !== undefined ? variantData.showIcon : true;
showText = variantData.showText !== undefined ? variantData.showText : true;
if (displayCommand) {
Qt.callLater(refreshOutput);
} else {
currentOutput = displayText;
}
if (updateInterval > 0) {
updateTimer.restart();
}
}
onDisplayCommandChanged: {
if (displayCommand) {
Qt.callLater(refreshOutput);
} else {
currentOutput = displayText;
}
}
onDisplayTextChanged: {
if (!displayCommand) {
currentOutput = displayText;
}
}
onUpdateIntervalChanged: {
if (updateInterval > 0) {
updateTimer.restart();
} else {
updateTimer.stop();
}
}
Component.onCompleted: {
if (displayCommand) {
Qt.callLater(refreshOutput);
} else {
currentOutput = displayText;
}
if (updateInterval > 0) {
updateTimer.start();
}
}
Timer {
id: updateTimer
interval: root.updateInterval * 1000
repeat: true
running: false
onTriggered: {
if (root.displayCommand) {
root.refreshOutput();
}
}
}
function refreshOutput() {
if (!displayCommand) {
currentOutput = displayText;
return;
}
isLoading = true;
displayProcess.running = true;
}
function executeCommand(command) {
if (!command)
return;
isLoading = true;
actionProcess.command = ["sh", "-c", command];
actionProcess.running = true;
}
Process {
id: displayProcess
command: ["sh", "-c", root.displayCommand]
running: false
stdout: SplitParser {
onRead: data => {
root.currentOutput = data.trim();
}
}
onExited: (exitCode, exitStatus) => {
root.isLoading = false;
if (exitCode !== 0) {
console.warn("CustomActions: Display command failed with code", exitCode);
}
}
}
Process {
id: actionProcess
command: ["sh", "-c", ""]
running: false
onExited: (exitCode, exitStatus) => {
root.isLoading = false;
if (exitCode === 0) {
if (root.displayCommand) {
root.refreshOutput();
}
} else {
console.warn("CustomActions: Action command failed with code", exitCode);
}
}
}
pillClickAction: () => {
if (root.clickCommand) {
root.executeCommand(root.clickCommand);
}
}
horizontalBarPill: Component {
MouseArea {
implicitWidth: contentRow.implicitWidth
implicitHeight: contentRow.implicitHeight
acceptedButtons: Qt.MiddleButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (mouse.button === Qt.MiddleButton && root.middleClickCommand) {
root.executeCommand(root.middleClickCommand);
} else if (mouse.button === Qt.RightButton && root.rightClickCommand) {
root.executeCommand(root.rightClickCommand);
}
}
Row {
id: contentRow
spacing: Theme.spacingXS
DankIcon {
name: root.displayIcon
size: Theme.iconSize - 6
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showIcon
}
StyledText {
text: root.currentOutput || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showText && root.currentOutput
}
}
}
}
verticalBarPill: Component {
MouseArea {
implicitWidth: contentColumn.implicitWidth
implicitHeight: contentColumn.implicitHeight
acceptedButtons: Qt.MiddleButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (mouse.button === Qt.MiddleButton && root.middleClickCommand) {
root.executeCommand(root.middleClickCommand);
} else if (mouse.button === Qt.RightButton && root.rightClickCommand) {
root.executeCommand(root.rightClickCommand);
}
}
Column {
id: contentColumn
spacing: Theme.spacingXS
DankIcon {
name: root.displayIcon
size: Theme.iconSize - 6
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showIcon
}
StyledText {
text: root.currentOutput || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showText && root.currentOutput
}
}
}
}
}

View file

@ -1,16 +0,0 @@
{
"id": "dankActions",
"name": "Dank Actions",
"description": "Execute custom commands with dynamic output display and configurable icons",
"version": "1.0.5",
"license": "MIT",
"author": "Avenge Media",
"icon": "terminal",
"firstParty": true,
"component": "./DankActionsWidget.qml",
"settings": "./DankActionsSettings.qml",
"permissions": [
"settings_read",
"settings_write"
]
}

View file

@ -1,110 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Modules.Plugins
PluginComponent {
id: root
Item {}
property bool enableCriticalAlert: pluginData.enableCriticalAlert ?? true
property int criticalThreshold: pluginData.criticalThreshold ?? 10
property string criticalTitle: pluginData.criticalTitle || "Critical Battery Level"
property string criticalMessage: pluginData.criticalMessage || "Battery at ${level}% - Connect charger immediately!"
property bool enableWarningAlert: pluginData.enableWarningAlert ?? true
property int warningThreshold: pluginData.warningThreshold ?? 20
property string warningTitle: pluginData.warningTitle || "Low Battery"
property string warningMessage: pluginData.warningMessage || "Battery at ${level}% - Consider charging soon"
property bool criticalAlertSent: false
property bool warningAlertSent: false
Component.onCompleted: {
console.log("DankBatteryAlerts: Started monitoring battery level")
console.log("DankBatteryAlerts: Critical alerts:", enableCriticalAlert, "at", criticalThreshold + "%")
console.log("DankBatteryAlerts: Warning alerts:", enableWarningAlert, "at", warningThreshold + "%")
}
Connections {
target: BatteryService.batteryAvailable ? BatteryService : null
function onBatteryLevelChanged() {
const level = BatteryService.batteryLevel
const isCharging = BatteryService.isCharging
if (isCharging) {
criticalAlertSent = false
warningAlertSent = false
return
}
if (enableCriticalAlert && level <= criticalThreshold && !criticalAlertSent) {
sendNotification(
criticalTitle,
criticalMessage.replace("${level}", level),
"critical",
"battery_alert"
)
criticalAlertSent = true
} else if (enableWarningAlert && level <= warningThreshold && !warningAlertSent && !criticalAlertSent) {
sendNotification(
warningTitle,
warningMessage.replace("${level}", level),
"normal",
"battery_std"
)
warningAlertSent = true
}
if (level > warningThreshold) {
warningAlertSent = false
}
if (level > criticalThreshold) {
criticalAlertSent = false
}
}
}
function sendNotification(title, message, urgency, icon) {
const process = notifyProcessComponent.createObject(root, {
notifyTitle: title,
notifyMessage: message,
notifyUrgency: urgency,
notifyIcon: icon
})
process.running = true
}
Component {
id: notifyProcessComponent
Process {
property string notifyTitle: ""
property string notifyMessage: ""
property string notifyUrgency: "normal"
property string notifyIcon: "battery_alert"
command: [
"notify-send",
"-a", "DankMaterialShell",
"-i", notifyIcon,
"-u", notifyUrgency,
notifyTitle,
notifyMessage
]
onExited: (exitCode) => {
if (exitCode !== 0) {
console.error("DankBatteryAlerts: notify-send failed with code:", exitCode)
}
destroy()
}
}
}
Component.onDestruction: {
console.log("DankBatteryAlerts: Stopped monitoring battery level")
}
}

View file

@ -1,139 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "dankBatteryAlerts"
StyledText {
text: "Battery Alerts"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: "Get notified when battery reaches critical or warning levels while on battery power"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
StyledRect {
width: parent.width
height: 1
color: Theme.surfaceVariant
}
StyledText {
text: "Critical Alert"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
color: Theme.surfaceText
}
ToggleSetting {
settingKey: "enableCriticalAlert"
label: "Enable Critical Alert"
description: "Show urgent notification when battery reaches critical level"
defaultValue: true
}
SliderSetting {
settingKey: "criticalThreshold"
label: "Critical Threshold"
description: "Battery percentage to trigger critical alert"
defaultValue: 10
minimum: 1
maximum: 30
unit: "%"
rightIcon: "battery_alert"
}
StringSetting {
settingKey: "criticalTitle"
label: "Critical Title"
description: "Notification title for critical alerts"
placeholder: "Critical Battery Level"
defaultValue: "Critical Battery Level"
}
StringSetting {
settingKey: "criticalMessage"
label: "Critical Message"
description: "Use ${level} for battery percentage"
placeholder: "Battery at ${level}% - Connect charger immediately!"
defaultValue: "Battery at ${level}% - Connect charger immediately!"
}
StyledRect {
width: parent.width
height: 1
color: Theme.surfaceVariant
}
StyledText {
text: "Warning Alert"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
color: Theme.surfaceText
}
ToggleSetting {
settingKey: "enableWarningAlert"
label: "Enable Warning Alert"
description: "Show notification when battery reaches warning level"
defaultValue: true
}
SliderSetting {
settingKey: "warningThreshold"
label: "Warning Threshold"
description: "Battery percentage to trigger warning alert"
defaultValue: 20
minimum: 5
maximum: 50
unit: "%"
rightIcon: "battery_std"
}
StringSetting {
settingKey: "warningTitle"
label: "Warning Title"
description: "Notification title for warning alerts"
placeholder: "Low Battery"
defaultValue: "Low Battery"
}
StringSetting {
settingKey: "warningMessage"
label: "Warning Message"
description: "Use ${level} for battery percentage"
placeholder: "Battery at ${level}% - Consider charging soon"
defaultValue: "Battery at ${level}% - Consider charging soon"
}
StyledRect {
width: parent.width
height: 1
color: Theme.surfaceVariant
}
StyledText {
text: "Alert Behavior"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
color: Theme.surfaceText
}
StyledText {
text: "• Critical alerts use urgent priority and persist longer\n• Alerts reset when battery is charging or rises above threshold\n• Only one alert per threshold per battery discharge cycle"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
}

View file

@ -1,13 +0,0 @@
{
"id": "dankBatteryAlerts",
"name": "Dank Battery Alerts",
"description": "Receive notifications when battery level reaches critical or warning thresholds",
"version": "1.1.1",
"license": "MIT",
"author": "Avenge Media",
"icon": "battery_alert",
"type": "daemon",
"component": "./DankBatteryAlerts.qml",
"settings": "./DankBatteryAlertsSettings.qml",
"permissions": ["settings_read", "settings_write"]
}

View file

@ -1,767 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
DesktopPluginComponent {
id: root
minWidth: {
switch (viewMode) {
case "compact":
return 80;
case "standard":
return 140;
case "detailed":
return 200;
case "forecast":
return 280;
default:
return 160;
}
}
minHeight: {
switch (viewMode) {
case "compact":
return 80;
case "standard":
return 100;
case "detailed":
return 200;
case "forecast":
return 320;
default:
return 140;
}
}
property string viewMode: pluginData.viewMode ?? "standard"
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 80) / 100
property string colorMode: pluginData.colorMode ?? "primary"
property color customColor: pluginData.customColor ?? "#ffffff"
property bool showLocation: pluginData.showLocation ?? true
property bool showCondition: pluginData.showCondition ?? true
property bool showFeelsLike: pluginData.showFeelsLike ?? true
property bool showHumidity: pluginData.showHumidity ?? true
property bool showWind: pluginData.showWind ?? true
property bool showPressure: pluginData.showPressure ?? false
property bool showPrecipitation: pluginData.showPrecipitation ?? true
property bool showSunTimes: pluginData.showSunTimes ?? true
property bool showForecast: pluginData.showForecast ?? true
property int forecastDays: pluginData.forecastDays ?? 5
property bool showHourlyForecast: pluginData.showHourlyForecast ?? false
property int hourlyCount: pluginData.hourlyCount ?? 6
readonly property color accentColor: {
switch (colorMode) {
case "secondary":
return Theme.secondary;
case "custom":
return customColor;
default:
return Theme.primary;
}
}
readonly property color bgColor: Theme.withAlpha(Theme.surface, backgroundOpacity)
readonly property color tileBg: Theme.withAlpha(Theme.surfaceContainerHigh, backgroundOpacity)
readonly property color textColor: Theme.surfaceText
readonly property color dimColor: Theme.surfaceVariantText
readonly property bool available: WeatherService.weather.available
readonly property var weather: WeatherService.weather
readonly property real scaleFactor: Math.min(width, height) / 200
readonly property int scaledMargin: {
switch (viewMode) {
case "compact":
return 0;
case "standard":
return 2;
default:
return Math.round(Math.max(4, Theme.spacingS * scaleFactor));
}
}
readonly property int scaledSpacing: Math.round(Math.max(1, Theme.spacingXS * scaleFactor))
Ref {
service: WeatherService
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: root.bgColor
border.width: 0
ColumnLayout {
anchors.fill: parent
anchors.margins: root.scaledMargin
spacing: root.scaledSpacing
Loader {
id: headerLoader
Layout.fillWidth: true
Layout.fillHeight: {
switch (root.viewMode) {
case "compact":
case "standard":
return true;
case "detailed":
return !root.showForecast;
case "forecast":
return false;
default:
return true;
}
}
Layout.preferredHeight: {
if (root.viewMode === "forecast")
return 50;
if (root.viewMode === "detailed" && root.showForecast)
return 140;
return -1;
}
sourceComponent: {
switch (root.viewMode) {
case "compact":
return compactView;
case "standard":
return standardView;
case "detailed":
return detailedView;
case "forecast":
return forecastHeaderView;
default:
return standardView;
}
}
}
Loader {
Layout.fillWidth: true
Layout.fillHeight: true
visible: root.viewMode === "forecast" || (root.viewMode === "detailed" && root.showForecast)
active: visible
sourceComponent: forecastSection
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !root.available
DankIcon {
name: "cloud_off"
size: Theme.iconSize * 1.5
color: root.dimColor
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No Weather Data")
font.pixelSize: Theme.fontSizeSmall
color: root.dimColor
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Component {
id: compactView
Item {
id: compactRoot
visible: root.available
readonly property int baseSize: Math.min(width, height)
readonly property int iconSize: Math.round(baseSize * 0.55)
readonly property int tempFontSize: Math.round(baseSize * 0.22)
Column {
anchors.centerIn: parent
spacing: 0
DankIcon {
name: WeatherService.getWeatherIcon(root.weather.wCode)
size: compactRoot.iconSize
color: root.accentColor
anchors.horizontalCenter: parent.horizontalCenter
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.6
shadowColor: Theme.shadowMedium
shadowOpacity: 0.2
}
}
StyledText {
text: WeatherService.formatTemp(root.weather.temp, true, false)
font.pixelSize: compactRoot.tempFontSize
font.weight: Font.Light
color: root.textColor
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Component {
id: standardView
Item {
id: standardRoot
visible: root.available
readonly property int baseSize: Math.min(width, height)
readonly property int iconSize: Math.round(baseSize * 0.55)
readonly property int tempFontSize: Math.round(baseSize * 0.28)
readonly property int labelFontSize: Math.max(9, Math.round(baseSize * 0.12))
RowLayout {
anchors.centerIn: parent
width: parent.width
spacing: Math.round(baseSize * 0.04)
DankIcon {
name: WeatherService.getWeatherIcon(root.weather.wCode)
size: standardRoot.iconSize
color: root.accentColor
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 3
shadowBlur: 0.7
shadowColor: Theme.shadowMedium
shadowOpacity: 0.2
}
}
Column {
Layout.fillWidth: true
spacing: 0
StyledText {
text: WeatherService.formatTemp(root.weather.temp, true, false)
font.pixelSize: standardRoot.tempFontSize
font.weight: Font.Light
color: root.textColor
}
StyledText {
visible: root.showCondition
text: WeatherService.getWeatherCondition(root.weather.wCode)
font.pixelSize: standardRoot.labelFontSize
color: root.dimColor
elide: Text.ElideRight
width: parent.width
}
StyledText {
visible: root.showLocation && root.weather.city
text: root.weather.city
font.pixelSize: standardRoot.labelFontSize
color: root.dimColor
elide: Text.ElideRight
width: parent.width
}
}
}
}
}
Component {
id: detailedView
Item {
id: detailedRoot
visible: root.available
readonly property int baseSize: Math.min(width, height)
readonly property int iconSize: Math.round(Math.max(28, Math.min(56, baseSize * 0.28)))
readonly property int tempFontSize: Math.round(Math.max(16, Math.min(32, baseSize * 0.16)))
readonly property int labelFontSize: Math.round(Math.max(10, Math.min(14, baseSize * 0.07)))
readonly property int smallIconSize: Math.round(Math.max(12, Math.min(16, baseSize * 0.07)))
readonly property int itemSpacing: Math.round(Math.max(2, Math.min(8, baseSize * 0.04)))
ColumnLayout {
anchors.fill: parent
spacing: detailedRoot.itemSpacing
RowLayout {
Layout.fillWidth: true
spacing: detailedRoot.itemSpacing * 2
DankIcon {
name: WeatherService.getWeatherIcon(root.weather.wCode)
size: detailedRoot.iconSize
color: root.accentColor
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 3
shadowBlur: 0.7
shadowColor: Theme.shadowMedium
shadowOpacity: 0.2
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
text: WeatherService.formatTemp(root.weather.temp, true, false)
font.pixelSize: detailedRoot.tempFontSize
font.weight: Font.Light
color: root.textColor
}
StyledText {
visible: root.showCondition
text: WeatherService.getWeatherCondition(root.weather.wCode)
font.pixelSize: detailedRoot.labelFontSize
color: root.dimColor
}
StyledText {
visible: root.showLocation && root.weather.city
text: root.weather.city
font.pixelSize: detailedRoot.labelFontSize
color: root.dimColor
elide: Text.ElideRight
Layout.fillWidth: true
}
}
Item {
Layout.fillWidth: true
}
ColumnLayout {
visible: root.showSunTimes
spacing: 1
Layout.alignment: Qt.AlignRight
RowLayout {
spacing: 2
DankIcon {
name: "wb_twilight"
size: detailedRoot.smallIconSize
color: root.dimColor
}
StyledText {
text: root.weather.sunrise || "--"
font.pixelSize: detailedRoot.labelFontSize
color: root.dimColor
}
}
RowLayout {
spacing: 2
DankIcon {
name: "bedtime"
size: detailedRoot.smallIconSize
color: root.dimColor
}
StyledText {
text: root.weather.sunset || "--"
font.pixelSize: detailedRoot.labelFontSize
color: root.dimColor
}
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.withAlpha(Theme.outline, 0.15)
}
Flow {
Layout.fillWidth: true
spacing: detailedRoot.itemSpacing
WeatherMetric {
visible: root.showFeelsLike
icon: "device_thermostat"
label: I18n.tr("Feels")
value: WeatherService.formatTemp(root.weather.feelsLike, true, true)
accentColor: root.accentColor
textColor: root.textColor
dimColor: root.dimColor
iconSize: detailedRoot.smallIconSize
fontSize: detailedRoot.labelFontSize
}
WeatherMetric {
visible: root.showHumidity
icon: "humidity_percentage"
label: I18n.tr("Humidity")
value: WeatherService.formatPercent(root.weather.humidity)
accentColor: root.accentColor
textColor: root.textColor
dimColor: root.dimColor
iconSize: detailedRoot.smallIconSize
fontSize: detailedRoot.labelFontSize
}
WeatherMetric {
visible: root.showWind
icon: "air"
label: I18n.tr("Wind")
value: root.weather.wind || "--"
accentColor: root.accentColor
textColor: root.textColor
dimColor: root.dimColor
iconSize: detailedRoot.smallIconSize
fontSize: detailedRoot.labelFontSize
}
WeatherMetric {
visible: root.showPrecipitation
icon: "rainy"
label: I18n.tr("Precip")
value: WeatherService.formatPercent(root.weather.precipitationProbability)
accentColor: root.accentColor
textColor: root.textColor
dimColor: root.dimColor
iconSize: detailedRoot.smallIconSize
fontSize: detailedRoot.labelFontSize
}
WeatherMetric {
visible: root.showPressure
icon: "speed"
label: I18n.tr("Pressure")
value: WeatherService.formatPressure(root.weather.pressure)
accentColor: root.accentColor
textColor: root.textColor
dimColor: root.dimColor
iconSize: detailedRoot.smallIconSize
fontSize: detailedRoot.labelFontSize
}
}
Item {
Layout.fillHeight: true
visible: !root.showForecast
}
}
}
}
Component {
id: forecastHeaderView
Item {
id: forecastHeaderRoot
visible: root.available
readonly property int baseSize: Math.min(width, height)
readonly property int iconSize: Math.round(Math.max(20, baseSize * 0.7))
readonly property int tempFontSize: Math.round(Math.max(12, baseSize * 0.4))
readonly property int labelFontSize: Math.round(Math.max(9, baseSize * 0.22))
RowLayout {
anchors.fill: parent
spacing: Math.round(baseSize * 0.15)
DankIcon {
name: WeatherService.getWeatherIcon(root.weather.wCode)
size: forecastHeaderRoot.iconSize
color: root.accentColor
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.6
shadowColor: Theme.shadowMedium
shadowOpacity: 0.2
}
}
ColumnLayout {
spacing: 0
StyledText {
text: WeatherService.formatTemp(root.weather.temp, true, false)
font.pixelSize: forecastHeaderRoot.tempFontSize
font.weight: Font.Light
color: root.textColor
}
StyledText {
visible: root.showLocation && root.weather.city
text: root.weather.city
font.pixelSize: forecastHeaderRoot.labelFontSize
color: root.dimColor
}
}
Item {
Layout.fillWidth: true
}
GridLayout {
columns: 2
rowSpacing: 1
columnSpacing: Math.round(forecastHeaderRoot.baseSize * 0.1)
visible: root.width > 300
WeatherMetric {
visible: root.showHumidity
icon: "humidity_percentage"
value: WeatherService.formatPercent(root.weather.humidity)
accentColor: root.accentColor
textColor: root.textColor
dimColor: root.dimColor
compact: true
iconSize: forecastHeaderRoot.labelFontSize
fontSize: forecastHeaderRoot.labelFontSize
}
WeatherMetric {
visible: root.showWind
icon: "air"
value: root.weather.wind || "--"
accentColor: root.accentColor
textColor: root.textColor
dimColor: root.dimColor
compact: true
iconSize: forecastHeaderRoot.labelFontSize
fontSize: forecastHeaderRoot.labelFontSize
}
}
}
}
}
Component {
id: forecastSection
ColumnLayout {
id: forecastRoot
spacing: root.scaledSpacing
visible: root.available && root.showForecast
readonly property int itemFontSize: Math.round(Math.max(10, Math.min(14, root.height * 0.035)))
readonly property int itemIconSize: Math.round(Math.max(12, Math.min(18, root.height * 0.04)))
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.withAlpha(Theme.outline, 0.15)
visible: root.viewMode === "forecast"
}
DankListView {
id: hourlyList
Layout.fillWidth: true
Layout.preferredHeight: root.showHourlyForecast ? Math.round(Math.max(50, Math.min(80, root.height * 0.18))) : 0
visible: root.showHourlyForecast && root.weather.hourlyForecast?.length > 0
orientation: ListView.Horizontal
flickableDirection: Flickable.HorizontalFlick
spacing: root.scaledSpacing
clip: true
model: Math.min(root.hourlyCount, root.weather.hourlyForecast?.length ?? 0)
delegate: Rectangle {
required property int index
width: Math.round(Math.max(36, hourlyList.height * 0.8))
height: hourlyList.height
radius: Theme.cornerRadius - 2
color: root.tileBg
property var forecast: root.weather.hourlyForecast?.[index] ?? {}
ColumnLayout {
anchors.fill: parent
anchors.margins: 2
spacing: 0
Item {
Layout.fillHeight: true
}
StyledText {
text: forecast.time || "--"
font.pixelSize: forecastRoot.itemFontSize
color: root.dimColor
Layout.alignment: Qt.AlignHCenter
}
DankIcon {
name: WeatherService.getWeatherIcon(forecast.wCode, forecast.isDay)
size: forecastRoot.itemIconSize
color: root.accentColor
Layout.alignment: Qt.AlignHCenter
}
StyledText {
text: WeatherService.formatTemp(forecast.temp, false)
font.pixelSize: forecastRoot.itemFontSize
font.weight: Font.Medium
color: root.textColor
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.withAlpha(Theme.outline, 0.1)
visible: root.showHourlyForecast && root.weather.hourlyForecast?.length > 0
}
DankListView {
id: dailyList
Layout.fillWidth: true
Layout.fillHeight: true
spacing: root.scaledSpacing
clip: true
readonly property int itemCount: Math.min(root.forecastDays, root.weather.forecast?.length ?? 0)
readonly property int dynamicItemHeight: itemCount > 0 ? Math.round((height - (itemCount - 1) * spacing) / itemCount) : 24
model: itemCount
delegate: Rectangle {
required property int index
width: dailyList.width
height: dailyList.dynamicItemHeight
radius: Theme.cornerRadius - 2
color: index === 0 ? Theme.withAlpha(root.accentColor, 0.1) : root.tileBg
property var forecast: root.weather.forecast?.[index] ?? {}
RowLayout {
anchors.fill: parent
anchors.leftMargin: root.scaledMargin
anchors.rightMargin: root.scaledMargin
spacing: root.scaledSpacing
StyledText {
text: forecast.day || "--"
font.pixelSize: forecastRoot.itemFontSize
font.weight: index === 0 ? Font.Medium : Font.Normal
color: root.textColor
Layout.preferredWidth: forecastRoot.itemFontSize * 5
}
DankIcon {
name: WeatherService.getWeatherIcon(forecast.wCode, true)
size: forecastRoot.itemIconSize + 2
color: root.accentColor
}
Item {
Layout.fillWidth: true
}
RowLayout {
spacing: 1
visible: forecast.precipitationProbability > 0
DankIcon {
name: "water_drop"
size: forecastRoot.itemIconSize - 2
color: Theme.primary
}
StyledText {
text: forecast.precipitationProbability + "%"
font.pixelSize: forecastRoot.itemFontSize - 1
color: Theme.primary
}
}
StyledText {
text: WeatherService.formatTemp(forecast.tempMax, false)
font.pixelSize: forecastRoot.itemFontSize
font.weight: Font.Medium
color: root.textColor
horizontalAlignment: Text.AlignRight
Layout.preferredWidth: forecastRoot.itemFontSize * 2.5
}
StyledText {
text: WeatherService.formatTemp(forecast.tempMin, false)
font.pixelSize: forecastRoot.itemFontSize
color: root.dimColor
horizontalAlignment: Text.AlignRight
Layout.preferredWidth: forecastRoot.itemFontSize * 2.5
}
}
}
}
}
}
component WeatherMetric: RowLayout {
property string icon: ""
property string label: ""
property string value: ""
property color accentColor: Theme.primary
property color textColor: Theme.surfaceText
property color dimColor: Theme.surfaceVariantText
property bool compact: false
property real iconSize: Theme.iconSizeSmall
property real fontSize: Theme.fontSizeSmall
spacing: 2
DankIcon {
name: parent.icon
size: compact ? parent.iconSize - 2 : parent.iconSize
color: parent.accentColor
}
ColumnLayout {
spacing: 0
visible: !compact
StyledText {
visible: parent.parent.label.length > 0
text: parent.parent.label
font.pixelSize: parent.parent.fontSize - 2
color: parent.parent.dimColor
}
StyledText {
text: parent.parent.value
font.pixelSize: parent.parent.fontSize
color: parent.parent.textColor
}
}
StyledText {
visible: compact
text: parent.value
font.pixelSize: parent.fontSize
color: parent.textColor
}
}
}

View file

@ -1,147 +0,0 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "dankDesktopWeather"
SelectionSetting {
settingKey: "viewMode"
label: I18n.tr("View Mode")
description: I18n.tr("Choose how the weather widget is displayed")
options: [
{
label: I18n.tr("Compact"),
value: "compact"
},
{
label: I18n.tr("Standard"),
value: "standard"
},
{
label: I18n.tr("Detailed"),
value: "detailed"
},
{
label: I18n.tr("Forecast"),
value: "forecast"
}
]
defaultValue: "standard"
}
SelectionSetting {
settingKey: "colorMode"
label: I18n.tr("Accent Color")
options: [
{
label: I18n.tr("Primary"),
value: "primary"
},
{
label: I18n.tr("Secondary"),
value: "secondary"
},
{
label: I18n.tr("Custom"),
value: "custom"
}
]
defaultValue: "primary"
}
ColorSetting {
settingKey: "customColor"
label: I18n.tr("Custom Color")
description: I18n.tr("Used when accent color is set to Custom")
defaultValue: "#4fc3f7"
}
SliderSetting {
settingKey: "backgroundOpacity"
label: I18n.tr("Background Opacity")
defaultValue: 80
minimum: 0
maximum: 100
unit: "%"
}
ToggleSetting {
settingKey: "showLocation"
label: I18n.tr("Show Location")
defaultValue: true
}
ToggleSetting {
settingKey: "showCondition"
label: I18n.tr("Show Weather Condition")
defaultValue: true
}
ToggleSetting {
settingKey: "showFeelsLike"
label: I18n.tr("Show Feels Like Temperature")
defaultValue: true
}
ToggleSetting {
settingKey: "showHumidity"
label: I18n.tr("Show Humidity")
defaultValue: true
}
ToggleSetting {
settingKey: "showWind"
label: I18n.tr("Show Wind Speed")
defaultValue: true
}
ToggleSetting {
settingKey: "showPressure"
label: I18n.tr("Show Pressure")
defaultValue: false
}
ToggleSetting {
settingKey: "showPrecipitation"
label: I18n.tr("Show Precipitation Probability")
defaultValue: true
}
ToggleSetting {
settingKey: "showSunTimes"
label: I18n.tr("Show Sunrise/Sunset")
defaultValue: true
}
ToggleSetting {
settingKey: "showForecast"
label: I18n.tr("Show Forecast")
description: I18n.tr("Available in Detailed and Forecast view modes")
defaultValue: true
}
SliderSetting {
settingKey: "forecastDays"
label: I18n.tr("Forecast Days")
defaultValue: 5
minimum: 1
maximum: 7
}
ToggleSetting {
settingKey: "showHourlyForecast"
label: I18n.tr("Show Hourly Forecast")
description: I18n.tr("Display hourly weather predictions")
defaultValue: false
}
SliderSetting {
settingKey: "hourlyCount"
label: I18n.tr("Hourly Forecast Count")
defaultValue: 6
minimum: 3
maximum: 12
}
}

View file

@ -1,19 +0,0 @@
{
"id": "dankDesktopWeather",
"name": "Dank Desktop Weather",
"description": "Feature-rich weather widget with current conditions, forecasts, and multiple view modes",
"version": "1.0.1",
"license": "MIT",
"author": "Avenge Media",
"icon": "partly_cloudy_day",
"firstParty": true,
"type": "desktop",
"capabilities": ["desktop-widget", "weather"],
"component": "./DankDesktopWeather.qml",
"settings": "./DankDesktopWeatherSettings.qml",
"requires_dms": ">=1.2.0",
"permissions": [
"settings_read",
"settings_write"
]
}

View file

@ -1,277 +0,0 @@
import QtQuick
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Modules.Plugins
PluginComponent {
id: root
property bool preparingForSleep: false
property string hookWallpaperPath: pluginData.wallpaperPath || ""
property string hookLightMode: pluginData.lightMode || ""
property string hookTheme: pluginData.theme || ""
property string hookMatugenCompleted: pluginData.matugenCompleted || ""
property string hookBatteryLevel: pluginData.batteryLevel || ""
property string hookBatteryCharging: pluginData.batteryCharging || ""
property string hookBatteryPluggedIn: pluginData.batteryPluggedIn || ""
property string hookPowerRequestLock: pluginData.hookPowerRequestLock || ""
property string hookPowerMonitorOff: pluginData.hookPowerMonitorOff || ""
property string hookPowerMonitorOn: pluginData.hookPowerMonitorOn || ""
property string hookPowerSuspend: pluginData.hookPowerSuspend || ""
property string hookResumeFromSleep: pluginData.hookResumeFromSleep || ""
property string hookWifiConnected: pluginData.wifiConnected || ""
property string hookWifiSSID: pluginData.wifiSSID || ""
property string hookEthernetConnected: pluginData.ethernetConnected || ""
property string hookAudioVolume: pluginData.audioVolume || ""
property string hookAudioMute: pluginData.audioMute || ""
property string hookMicMute: pluginData.micMute || ""
property string hookBrightness: pluginData.brightness || ""
property string hookNightMode: pluginData.nightMode || ""
property string hookDoNotDisturb: pluginData.doNotDisturb || ""
property string hookMediaPlaying: pluginData.mediaPlaying || ""
property string hookIdleStateActive: pluginData.idleStateActive || ""
property string hookMonitorWallpaper: pluginData.monitorWallpaper || ""
Connections {
target: SessionData
function onWallpaperPathChanged() {
if (hookWallpaperPath) {
executeHook(hookWallpaperPath, "onWallpaperChanged", SessionData.wallpaperPath);
}
}
function onMonitorWallpapersChanged() {
if (hookMonitorWallpaper) {
const wallpapersJson = JSON.stringify(SessionData.monitorWallpapers);
executeHook(hookMonitorWallpaper, "onMonitorWallpapersChanged", wallpapersJson);
}
}
function onIsLightModeChanged() {
if (hookLightMode) {
executeHook(hookLightMode, "onLightModeChanged", SessionData.isLightMode ? "light" : "dark");
}
}
function onNightModeEnabledChanged() {
if (hookNightMode) {
executeHook(hookNightMode, "onNightModeChanged", SessionData.nightModeEnabled ? "enabled" : "disabled");
}
}
function onDoNotDisturbChanged() {
if (hookDoNotDisturb) {
executeHook(hookDoNotDisturb, "onDoNotDisturbChanged", SessionData.doNotDisturb ? "enabled" : "disabled");
}
}
}
Connections {
target: typeof Theme !== "undefined" ? Theme : null
function onCurrentThemeChanged() {
if (!hookTheme)
return;
executeHook(hookTheme, "onThemeChanged", Theme.currentTheme);
}
function onMatugenCompleted(mode, result) {
if (!hookMatugenCompleted)
return;
executeHook(hookMatugenCompleted, "onMatugenCompleted", mode + ":" + result);
}
}
Connections {
target: BatteryService.batteryAvailable ? BatteryService : null
function onBatteryLevelChanged() {
if (hookBatteryLevel) {
executeHook(hookBatteryLevel, "onBatteryLevelChanged", String(BatteryService.batteryLevel));
}
}
function onIsChargingChanged() {
if (hookBatteryCharging) {
executeHook(hookBatteryCharging, "onBatteryChargingChanged", BatteryService.isCharging ? "charging" : "not-charging");
}
}
function onIsPluggedInChanged() {
if (hookBatteryPluggedIn) {
executeHook(hookBatteryPluggedIn, "onBatteryPluggedInChanged", BatteryService.isPluggedIn ? "plugged-in" : "on-battery");
}
}
}
Connections {
target: IdleService
function onLockRequested() {
if (hookPowerRequestLock) {
executeHook(hookPowerRequestLock, "onLockRequested", "");
}
}
function onRequestMonitorOff() {
if (hookPowerMonitorOff) {
executeHook(hookPowerMonitorOff, "onRequestMonitorOff", "");
}
}
function onRequestMonitorOn() {
if (hookPowerMonitorOn) {
executeHook(hookPowerMonitorOn, "onRequestMonitorOn", "");
}
}
function onRequestSuspend() {
if (hookPowerSuspend) {
executeHook(hookPowerSuspend, "onRequestSuspend", "");
}
}
}
Connections {
target: DMSService
function onLoginctlStateUpdate(data) {
var lastState = root.preparingForSleep;
root.preparingForSleep = data.preparingForSleep;
if (lastState && !root.preparingForSleep) {
executeHook(hookResumeFromSleep, "onResumeFromSleep", "");
}
}
}
Connections {
target: NetworkService
function onWifiConnectedChanged() {
if (hookWifiConnected) {
executeHook(hookWifiConnected, "onWifiConnectedChanged", NetworkService.wifiConnected ? "connected" : "disconnected");
}
}
function onCurrentWifiSSIDChanged() {
if (hookWifiSSID) {
executeHook(hookWifiSSID, "onWifiSSIDChanged", NetworkService.currentWifiSSID || "none");
}
}
function onEthernetConnectedChanged() {
if (hookEthernetConnected) {
executeHook(hookEthernetConnected, "onEthernetConnectedChanged", NetworkService.ethernetConnected ? "connected" : "disconnected");
}
}
}
Connections {
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
function onVolumeChanged() {
if (hookAudioVolume && AudioService.sink && AudioService.sink.audio) {
executeHook(hookAudioVolume, "onAudioVolumeChanged", String(Math.round(AudioService.sink.audio.volume * 100)));
}
}
function onMutedChanged() {
if (hookAudioMute && AudioService.sink && AudioService.sink.audio) {
executeHook(hookAudioMute, "onAudioMuteChanged", AudioService.sink.audio.muted ? "muted" : "unmuted");
}
}
}
Connections {
target: AudioService.source && AudioService.source.audio ? AudioService.source.audio : null
function onMutedChanged() {
if (hookMicMute && AudioService.source && AudioService.source.audio) {
executeHook(hookMicMute, "onMicMuteChanged", AudioService.source.audio.muted ? "muted" : "unmuted");
}
}
}
Connections {
target: DisplayService
function onBrightnessLevelChanged() {
if (hookBrightness && DisplayService.brightnessAvailable) {
executeHook(hookBrightness, "onBrightnessChanged", String(DisplayService.brightnessLevel));
}
}
}
Connections {
target: MprisController.activePlayer
function onIsPlayingChanged() {
if (hookMediaPlaying && MprisController.activePlayer) {
executeHook(hookMediaPlaying, "onMediaPlayingChanged", MprisController.activePlayer.isPlaying ? "playing" : "paused");
}
}
}
function executeHook(scriptPath, hookName, hookValue) {
if (!scriptPath || scriptPath.trim() === "") {
return;
}
const process = hookProcessComponent.createObject(root, {
hookScript: scriptPath,
hookName: hookName,
hookValue: hookValue
});
if (!process) {
console.error("DankHooks: Failed to create process object");
return;
}
process.running = true;
}
Component {
id: hookProcessComponent
Process {
property string hookScript: ""
property string hookName: ""
property string hookValue: ""
command: ["sh", "-c", "$HOOK_SCRIPT \"$HOOK_NAME\" \"$HOOK_VALUE\""]
environment: {
"HOOK_SCRIPT": hookScript,
"HOOK_NAME": hookName,
"HOOK_VALUE": hookValue
}
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
console.log("DankHooks output:", text.trim());
}
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
ToastService.showError("Hook Script Error", text.trim());
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
ToastService.showError("Hook Script Error", `Script '${hookScript}' exited with code: ${exitCode}`);
}
destroy();
}
}
}
Component.onDestruction: {
console.log("DankHooks: Stopped monitoring system events");
}
}

View file

@ -1,332 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "dankHooks"
StyledText {
text: "System Event Hooks"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: "Execute custom scripts when system events occur. Scripts receive two arguments: hook name (e.g., 'onBatteryLevelChanged') and event value."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
StyledRect {
width: parent.width
height: 1
color: Theme.surfaceVariant
}
StyledText {
text: "Appearance & Theme"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
color: Theme.surfaceText
}
StringSetting {
settingKey: "wallpaperPath"
label: "Wallpaper Changed"
description: "Hook: onWallpaperChanged | Value: wallpaper file path"
placeholder: "/path/to/wallpaper-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "monitorWallpaper"
label: "Per-Monitor Wallpapers Changed"
description: "Hook: onMonitorWallpapersChanged | Value: JSON object with all monitors (e.g., '{\"eDP-1\":\"/path1.jpg\",\"DP-2\":\"/path2.jpg\"}')"
placeholder: "/path/to/monitor-wallpaper-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "lightMode"
label: "Light/Dark Mode Changed"
description: "Hook: onLightModeChanged | Value: 'light' or 'dark'"
placeholder: "/path/to/mode-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "theme"
label: "Theme Changed"
description: "Hook: onThemeChanged | Value: theme name (e.g., 'blue', 'red', 'dynamic')"
placeholder: "/path/to/theme-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "matugenCompleted"
label: "Matugen Generation Completed"
description: "Hook: onMatugenCompleted | Value: '<mode>:<result>' (e.g., 'dark:success', 'light:no-changes')"
placeholder: "/path/to/matugen-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "nightMode"
label: "Night Mode Changed"
description: "Hook: onNightModeChanged | Value: 'enabled' or 'disabled'"
placeholder: "/path/to/nightmode-hook.sh"
defaultValue: ""
}
StyledRect {
width: parent.width
height: 1
color: Theme.surfaceVariant
}
StyledText {
text: "Power & Battery"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
color: Theme.surfaceText
}
StringSetting {
settingKey: "batteryLevel"
label: "Battery Level Changed"
description: "Hook: onBatteryLevelChanged | Value: percentage (0-100)"
placeholder: "/path/to/battery-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "batteryCharging"
label: "Battery Charging State Changed"
description: "Hook: onBatteryChargingChanged | Value: 'charging' or 'not-charging'"
placeholder: "/path/to/charging-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "batteryPluggedIn"
label: "Power Adapter Changed"
description: "Hook: onBatteryPluggedInChanged | Value: 'plugged-in' or 'on-battery'"
placeholder: "/path/to/power-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "hookPowerRequestLock"
label: "Lock Screen Event Triggered"
description: "Hook: onLockRequested | Value: empty"
placeholder: "/path/to/sessionlock-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "hookPowerMonitorOff"
label: "Monitor Off Event Triggered"
description: "Hook: onRequestMonitorOff | Value: empty"
placeholder: "/path/to/monitoroff-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "hookPowerMonitorOn"
label: "Monitor On Event Triggered"
description: "Hook: onRequestMonitorOn | Value: empty"
placeholder: "/path/to/monitoron-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "hookPowerSuspend"
label: "Suspend Event Triggered"
description: "Hook: onRequestSuspend | Value: empty"
placeholder: "/path/to/suspend-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "hookResumeFromSleep"
label: "Resume From Sleep Event Triggered"
description: "Hook: onResumeFromSleep | Value: empty"
placeholder: "/path/to/resumeFromSleep-hook.sh"
defaultValue: ""
}
StyledRect {
width: parent.width
height: 1
color: Theme.surfaceVariant
}
StyledText {
text: "Network"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
color: Theme.surfaceText
}
StringSetting {
settingKey: "wifiConnected"
label: "WiFi Connection Changed"
description: "Hook: onWifiConnectedChanged | Value: 'connected' or 'disconnected'"
placeholder: "/path/to/wifi-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "wifiSSID"
label: "WiFi Network Changed"
description: "Hook: onWifiSSIDChanged | Value: SSID name or 'none'"
placeholder: "/path/to/ssid-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "ethernetConnected"
label: "Ethernet Connection Changed"
description: "Hook: onEthernetConnectedChanged | Value: 'connected' or 'disconnected'"
placeholder: "/path/to/ethernet-hook.sh"
defaultValue: ""
}
StyledRect {
width: parent.width
height: 1
color: Theme.surfaceVariant
}
StyledText {
text: "Audio"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
color: Theme.surfaceText
}
StringSetting {
settingKey: "audioVolume"
label: "Audio Volume Changed"
description: "Hook: onAudioVolumeChanged | Value: percentage (0-100)"
placeholder: "/path/to/volume-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "audioMute"
label: "Audio Mute Changed"
description: "Hook: onAudioMuteChanged | Value: 'muted' or 'unmuted'"
placeholder: "/path/to/mute-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "micMute"
label: "Microphone Mute Changed"
description: "Hook: onMicMuteChanged | Value: 'muted' or 'unmuted'"
placeholder: "/path/to/mic-hook.sh"
defaultValue: ""
}
StyledRect {
width: parent.width
height: 1
color: Theme.surfaceVariant
}
StyledText {
text: "Display & Media"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
color: Theme.surfaceText
}
StringSetting {
settingKey: "brightness"
label: "Brightness Changed"
description: "Hook: onBrightnessChanged | Value: percentage (0-100)"
placeholder: "/path/to/brightness-hook.sh"
defaultValue: ""
}
StringSetting {
settingKey: "mediaPlaying"
label: "Media Playback Changed"
description: "Hook: onMediaPlayingChanged | Value: 'playing' or 'paused'"
placeholder: "/path/to/media-hook.sh"
defaultValue: ""
}
StyledRect {
width: parent.width
height: 1
color: Theme.surfaceVariant
}
StyledText {
text: "System"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
color: Theme.surfaceText
}
StringSetting {
settingKey: "doNotDisturb"
label: "Do Not Disturb Changed"
description: "Hook: onDoNotDisturbChanged | Value: 'enabled' or 'disabled'"
placeholder: "/path/to/dnd-hook.sh"
defaultValue: ""
}
StyledRect {
width: parent.width
height: 1
color: Theme.surfaceVariant
}
StyledText {
text: "Hook Script Examples"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.DemiBold
color: Theme.surfaceText
}
StyledText {
text: "Example hook script:"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledRect {
width: parent.width
height: exampleCode.height + 16
color: Theme.surface
radius: Theme.cornerRadius
StyledText {
id: exampleCode
anchors.centerIn: parent
anchors.margins: 8
width: parent.width - 16
text: '#!/bin/bash\n# Save as ~/.config/scripts/hook.sh\n# Make executable: chmod +x ~/.config/scripts/hook.sh\n\nHOOK_NAME="$1" # e.g., "onWallpaperChanged"\nHOOK_VALUE="$2" # e.g., "/path/to/wallpaper.jpg"\n\necho "Hook: $HOOK_NAME, Value: $HOOK_VALUE"\nnotify-send "$HOOK_NAME" "$HOOK_VALUE"'
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: Theme.surfaceText
wrapMode: Text.WordWrap
}
}
StyledText {
text: "All hooks pass two arguments: $1 = hook name (e.g., 'onBatteryLevelChanged'), $2 = event value. See descriptions above for each hook's values."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
}

View file

@ -1,50 +0,0 @@
# Dank Hooks Plugin
## Available Hooks
### Appearance & Theme
| Hook | Trigger | Hook Name | Value |
|------|---------|-----------|-------|
| **Wallpaper Changed** | When wallpaper changes | `onWallpaperChanged` | Wallpaper file path |
| **Light/Dark Mode Changed** | When switching between modes | `onLightModeChanged` | `light` or `dark` |
| **Theme Changed** | When color theme changes | `onThemeChanged` | Theme name (e.g., `blue`, `red`, `dynamic`) |
| **Matugen Completed** | When matugen finishes generating colors | `onMatugenCompleted` | `<mode>:<result>` (e.g., `dark:success`, `light:no-changes`, `dark:error`) |
| **Night Mode Changed** | When night mode toggles | `onNightModeChanged` | `enabled` or `disabled` |
### Power & Battery
| Hook | Trigger | Hook Name | Value |
|------|---------|-----------|-------|
| **Battery Level Changed** | When battery percentage changes | `onBatteryLevelChanged` | Battery percentage (0-100) |
| **Battery Charging State Changed** | When charging state changes | `onBatteryChargingChanged` | `charging` or `not-charging` |
| **Power Adapter Changed** | When power adapter connects/disconnects | `onBatteryPluggedInChanged` | `plugged-in` or `on-battery` |
### Network
| Hook | Trigger | Hook Name | Value |
|------|---------|-----------|-------|
| **WiFi Connection Changed** | When WiFi connects/disconnects | `onWifiConnectedChanged` | `connected` or `disconnected` |
| **WiFi Network Changed** | When connected WiFi network changes | `onWifiSSIDChanged` | SSID name or `none` |
| **Ethernet Connection Changed** | When Ethernet connects/disconnects | `onEthernetConnectedChanged` | `connected` or `disconnected` |
### Audio
| Hook | Trigger | Hook Name | Value |
|------|---------|-----------|-------|
| **Audio Volume Changed** | When speaker volume changes | `onAudioVolumeChanged` | Volume percentage (0-100) |
| **Audio Mute Changed** | When speakers mute/unmute | `onAudioMuteChanged` | `muted` or `unmuted` |
| **Microphone Mute Changed** | When microphone mutes/unmutes | `onMicMuteChanged` | `muted` or `unmuted` |
### Display & Media
| Hook | Trigger | Hook Name | Value |
|------|---------|-----------|-------|
| **Brightness Changed** | When screen brightness changes | `onBrightnessChanged` | Brightness percentage (0-100) |
| **Media Playback Changed** | When media starts/stops playing | `onMediaPlayingChanged` | `playing` or `paused` |
### System
| Hook | Trigger | Hook Name | Value |
|------|---------|-----------|-------|
| **Do Not Disturb Changed** | When DND mode toggles | `onDoNotDisturbChanged` | `enabled` or `disabled` |

View file

@ -1,16 +0,0 @@
{
"id": "dankHooks",
"name": "Dank Hooks",
"description": "Execute custom scripts on system events like wallpaper changes, theme updates, battery level changes, and more",
"version": "1.0.7",
"license": "MIT",
"author": "Avenge Media",
"icon": "webhook",
"type": "daemon",
"component": "./DankHooks.qml",
"settings": "./DankHooksSettings.qml",
"permissions": [
"settings_read",
"settings_write"
]
}

View file

@ -1,156 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "dankPomodoroTimer"
StyledText {
width: parent.width
text: "Pomodoro Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Configure timer durations and behavior"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
StyledRect {
width: parent.width
height: durationsColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: durationsColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
StyledText {
text: "Durations"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StringSetting {
settingKey: "workDuration"
label: "Work Duration (minutes)"
description: "Length of each focus session"
placeholder: "25"
defaultValue: "25"
}
StringSetting {
settingKey: "shortBreakDuration"
label: "Short Break (minutes)"
description: "Break after each pomodoro"
placeholder: "5"
defaultValue: "5"
}
StringSetting {
settingKey: "longBreakDuration"
label: "Long Break (minutes)"
description: "Break after 4 pomodoros"
placeholder: "15"
defaultValue: "15"
}
}
}
StyledRect {
width: parent.width
height: behaviorColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: behaviorColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
StyledText {
text: "Behavior"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
ToggleSetting {
settingKey: "autoStartBreaks"
label: "Auto-start Breaks"
description: "Automatically start break timers after work sessions"
defaultValue: false
}
ToggleSetting {
settingKey: "autoStartPomodoros"
label: "Auto-start Pomodoros"
description: "Automatically start work sessions after breaks"
defaultValue: false
}
ToggleSetting {
settingKey: "autoSetDND"
label: "Do Not Disturb Work"
description: "Automatically enable Do Not Disturb mode during work sessions"
defaultValue: false
}
}
}
StyledRect {
width: parent.width
height: infoColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surface
Column {
id: infoColumn
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: "About Pomodoro Technique"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "The Pomodoro Technique uses 25-minute focused work sessions followed by short breaks. After 4 pomodoros, take a longer break to recharge.\n\n• Work: 25 minutes of focused work\n• Short Break: 5 minute rest\n• Long Break: 15 minutes after 4 pomodoros\n\nNotifications will alert you when each session completes."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
lineHeight: 1.4
}
}
}
}

View file

@ -1,497 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property int workDuration: pluginData.workDuration || 25
property int shortBreakDuration: pluginData.shortBreakDuration || 5
property int longBreakDuration: pluginData.longBreakDuration || 15
property bool autoStartBreaks: pluginData.autoStartBreaks ?? false
property bool autoStartPomodoros: pluginData.autoStartPomodoros ?? false
property bool autoSetDND: pluginData.autoSetDND ?? false
onWorkDurationChanged: {
if (globalTimerState.value === "work" && globalTotalSeconds.value > 0) {
const newTotal = workDuration * 60
const elapsed = globalTotalSeconds.value - globalRemainingSeconds.value
globalTotalSeconds.set(newTotal)
globalRemainingSeconds.set(Math.max(1, newTotal - elapsed))
}
}
onShortBreakDurationChanged: {
if (globalTimerState.value === "shortBreak" && globalTotalSeconds.value > 0) {
const newTotal = shortBreakDuration * 60
const elapsed = globalTotalSeconds.value - globalRemainingSeconds.value
globalTotalSeconds.set(newTotal)
globalRemainingSeconds.set(Math.max(1, newTotal - elapsed))
}
}
onLongBreakDurationChanged: {
if (globalTimerState.value === "longBreak" && globalTotalSeconds.value > 0) {
const newTotal = longBreakDuration * 60
const elapsed = globalTotalSeconds.value - globalRemainingSeconds.value
globalTotalSeconds.set(newTotal)
globalRemainingSeconds.set(Math.max(1, newTotal - elapsed))
}
}
PluginGlobalVar {
id: globalRemainingSeconds
varName: "remainingSeconds"
defaultValue: 0
}
PluginGlobalVar {
id: globalTotalSeconds
varName: "totalSeconds"
defaultValue: 0
}
PluginGlobalVar {
id: globalIsRunning
varName: "isRunning"
defaultValue: false
}
PluginGlobalVar {
id: globalTimerState
varName: "timerState"
defaultValue: "work"
}
PluginGlobalVar {
id: globalCompletedPomodoros
varName: "completedPomodoros"
defaultValue: 0
}
PluginGlobalVar {
id: globalTimerOwnerId
varName: "timerOwnerId"
defaultValue: ""
}
property string instanceId: Math.random().toString(36).substring(2)
Timer {
id: pomodoroTimer
interval: 1000
repeat: true
running: globalIsRunning.value && globalTimerOwnerId.value === root.instanceId
onTriggered: {
if (globalRemainingSeconds.value > 0) {
globalRemainingSeconds.set(globalRemainingSeconds.value - 1)
} else {
root.timerComplete()
}
}
}
function timerComplete() {
globalIsRunning.set(false)
if (globalTimerState.value === "work") {
globalCompletedPomodoros.set(globalCompletedPomodoros.value + 1)
const isLongBreak = globalCompletedPomodoros.value % 4 === 0
Quickshell.execDetached(["sh", "-c", "notify-send 'Pomodoro Complete' 'Time for a " + (isLongBreak ? "long" : "short") + " break!' -u normal"])
if (root.autoSetDND) {
SessionData.setDoNotDisturb(false)
}
if (isLongBreak) {
root.startLongBreak(root.autoStartBreaks)
} else {
root.startShortBreak(root.autoStartBreaks)
}
} else {
Quickshell.execDetached(["sh", "-c", "notify-send 'Break Complete' 'Ready for another pomodoro?' -u normal"])
root.startWork(root.autoStartPomodoros)
}
}
function startWork(autoStart) {
globalTimerState.set("work")
globalTotalSeconds.set(root.workDuration * 60)
globalRemainingSeconds.set(globalTotalSeconds.value)
if (autoStart) {
globalTimerOwnerId.set(root.instanceId)
if (root.autoSetDND) {
SessionData.setDoNotDisturb(true)
}
}
globalIsRunning.set(autoStart ?? false)
}
function startShortBreak(autoStart) {
if(globalTimerState.value === "work" && root.autoSetDND) {
SessionData.setDoNotDisturb(false)
}
globalTimerState.set("shortBreak")
globalTotalSeconds.set(root.shortBreakDuration * 60)
globalRemainingSeconds.set(globalTotalSeconds.value)
if (autoStart) {
globalTimerOwnerId.set(root.instanceId)
}
globalIsRunning.set(autoStart ?? false)
}
function startLongBreak(autoStart) {
if(globalTimerState.value === "work" && root.autoSetDND) {
SessionData.setDoNotDisturb(false)
}
globalTimerState.set("longBreak")
globalTotalSeconds.set(root.longBreakDuration * 60)
globalRemainingSeconds.set(globalTotalSeconds.value)
if (autoStart) {
globalTimerOwnerId.set(root.instanceId)
}
globalIsRunning.set(autoStart ?? false)
}
function toggleTimer() {
if (!globalIsRunning.value) {
globalTimerOwnerId.set(root.instanceId)
}
globalIsRunning.set(!globalIsRunning.value)
if (root.autoSetDND && globalTimerState.value === "work") {
SessionData.setDoNotDisturb(globalIsRunning.value)
}
}
function resetTimer() {
globalIsRunning.set(false)
if (root.autoSetDND && globalTimerState.value === "work") {
SessionData.setDoNotDisturb(false)
}
globalRemainingSeconds.set(globalTotalSeconds.value)
}
function formatTime(seconds, isVertical = false) {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return isVertical ? mins + "\n" + (secs < 10 ? "0" : "") + secs : mins + " " + (secs < 10 ? "0" : "") + secs
}
function getStateColor() {
if (globalTimerState.value === "work") return Theme.primary
if (globalTimerState.value === "shortBreak") return Theme.success
return Theme.warning
}
function getStateIcon() {
if (globalTimerState.value === "work") return "work"
return "coffee"
}
IpcHandler {
function resetTimer(): string {
root.resetTimer()
return "POMDORO_TIME_RESET_SUCCESS"
}
function toggleTimer(): string {
root.toggleTimer()
return globalIsRunning.value ? "Timer is running" : "Timer is paused"
}
function startWork(): string {
root.startWork(true)
return "POMODORO_WORK_STARTED"
}
function startShortBreak(): string {
root.startShortBreak(true)
return "POMODORO_SHORT_BREAK_STARTED"
}
function startLongBreak(): string {
root.startLongBreak(true)
return "POMODORO_LONG_BREAK_STARTED"
}
target: "pomodoroTimer"
}
Timer {
id: initTimer
interval: 100
repeat: false
running: true
onTriggered: {
if (globalRemainingSeconds.value === 0 && globalTotalSeconds.value === 0) {
root.startWork(false)
}
}
}
horizontalBarPill: Component {
Row {
spacing: Theme.spacingXS
DankIcon {
name: root.getStateIcon()
size: Theme.iconSize - 6
color: root.getStateColor()
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.formatTime(globalRemainingSeconds.value)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
}
verticalBarPill: Component {
Column {
spacing: Theme.spacingXS
DankIcon {
name: root.getStateIcon()
size: Theme.iconSize - 6
color: root.getStateColor()
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: root.formatTime(globalRemainingSeconds.value, true)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
popoutContent: Component {
PopoutComponent {
id: popout
headerText: "Pomodoro Timer"
detailsText: {
if (globalTimerState.value === "work") return "Focus session • " + globalCompletedPomodoros.value + " completed"
if (globalTimerState.value === "shortBreak") return "Short break"
return "Long break"
}
showCloseButton: true
Column {
id: popoutContentColumn
width: parent.width
spacing: Theme.spacingM
Item {
width: parent.width
height: 180
Rectangle {
width: 180
height: 180
radius: 90
anchors.centerIn: parent
color: "transparent"
border.width: 8
border.color: Qt.rgba(root.getStateColor().r, root.getStateColor().g, root.getStateColor().b, 0.2)
Canvas {
id: progressCanvas
width: parent.width - 16
height: parent.height - 16
anchors.centerIn: parent
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
ctx.lineWidth = 8
ctx.strokeStyle = root.getStateColor()
ctx.beginPath()
const centerX = width / 2
const centerY = height / 2
const radius = (width - 8) / 2
const progress = globalRemainingSeconds.value / globalTotalSeconds.value
const startAngle = -Math.PI / 2
const endAngle = startAngle + (2 * Math.PI * progress)
ctx.arc(centerX, centerY, radius, startAngle, endAngle, false)
ctx.stroke()
}
Connections {
target: globalRemainingSeconds
function onValueChanged() {
progressCanvas.requestPaint()
}
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: root.formatTime(globalRemainingSeconds.value)
font.pixelSize: 36
font.weight: Font.Bold
color: root.getStateColor()
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
width: 120
}
StyledText {
text: {
if (globalTimerState.value === "work") return "Work"
if (globalTimerState.value === "shortBreak") return "Short Break"
return "Long Break"
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 64
height: 64
radius: 32
color: playArea.containsMouse ? Qt.rgba(root.getStateColor().r, root.getStateColor().g, root.getStateColor().b, 0.2) : "transparent"
DankIcon {
anchors.centerIn: parent
name: globalIsRunning.value ? "pause" : "play_arrow"
size: 32
color: root.getStateColor()
}
MouseArea {
id: playArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleTimer()
}
}
Rectangle {
width: 64
height: 64
radius: 32
color: resetArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
DankIcon {
anchors.centerIn: parent
name: "refresh"
size: 24
color: Theme.surfaceText
}
MouseArea {
id: resetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.resetTimer()
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Quick Actions"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
Row {
id: quickActionsRow
width: parent.width
spacing: Theme.spacingS
property real buttonWidth: (width - spacing * 2) / 3
DankButton {
text: "Work"
iconName: "work"
width: quickActionsRow.buttonWidth
onClicked: root.startWork(false)
}
DankButton {
text: "Short Break"
iconName: "coffee"
width: quickActionsRow.buttonWidth
onClicked: root.startShortBreak(false)
}
DankButton {
text: "Long Break"
iconName: "weekend"
width: quickActionsRow.buttonWidth
onClicked: root.startLongBreak(false)
}
}
}
StyledRect {
width: parent.width
height: statsColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: statsColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
spacing: Theme.spacingM
DankIcon {
name: "check_circle"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: globalCompletedPomodoros.value + " pomodoros completed"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "Next long break after " + (4 - (globalCompletedPomodoros.value % 4)) + " more"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
leftPadding: Theme.iconSize + Theme.spacingM
}
}
}
}
}
}
}

View file

@ -1,16 +0,0 @@
{
"id": "dankPomodoroTimer",
"name": "Dank Pomodoro Timer",
"description": "Productivity timer with 25-minute work sessions and breaks",
"version": "1.1.5",
"license": "MIT",
"author": "Avenge Media",
"icon": "timer",
"firstParty": true,
"component": "./DankPomodoroWidget.qml",
"settings": "./DankPomodoroSettings.qml",
"permissions": [
"settings_read",
"settings_write"
]
}

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Avenge Media LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,31 +0,0 @@
# DankMaterialShell plugins
A collection of first-party, officially maintained plugins for [DankMaterialShell](https://github.com/AvengeMedia/DankMaterialShell)
## Plugins
### [Dank Actions](./DankActions)
Scriptable, custom actions that can be added to the DankBar.
Allows for creating multiple widgets that execute custom script actions on click, or periodically.
<img width="488" height="638" alt="image" src="https://github.com/user-attachments/assets/36b44c32-69b5-49c9-97d2-87f530e4b7fd" />
### [Dank Pomodoro Timer](./DankPomodoroTimer)
A timer that is intended to improve productivity using the [Pomodoro technique](https://en.wikipedia.org/wiki/Pomodoro_Technique).
<img width="442" height="545" alt="image" src="https://github.com/user-attachments/assets/b51b5f78-5215-403c-850f-c7e137097438" />
### [Dank Hooks](./DankHooks)
Trigger scripts based on various system events, such as `onWallpaperChanged`, `onVolumeChanged`, etc.
<img width="472" height="593" alt="image" src="https://github.com/user-attachments/assets/83e89b5b-0636-4b8e-ba29-1fa4d12169a0" />
### [Dank Battery Alerts](./DankBatteryAlerts)
Trigger notifications when battery reaches low charge levels.
<img width="497" height="710" alt="image" src="https://github.com/user-attachments/assets/4302d886-eb87-41d4-a9a4-1eeaadd787c6" />

View file

@ -1,9 +0,0 @@
[core]
bare = false
filemode = true
[remote "origin"]
url = https://github.com/AvengeMedia/dms-plugins
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master

View file

@ -1 +0,0 @@
86c1e03fd8aa106728b46fe3fe4cb085c33fd698

View file

@ -1,54 +0,0 @@
# C++ objects and libs
*.slo
*.lo
*.o
*.a
*.la
*.lai
*.so
*.so.*
*.dll
*.dylib
# Qt-es
object_script.*.Release
object_script.*.Debug
*_plugin_import.cpp
/.qmake.cache
/.qmake.stash
*.pro.user
*.pro.user.*
*.qbs.user
*.qbs.user.*
*.moc
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
*.qmlc
*.jsc
Makefile*
*build-*
*.qm
*.prl
# Qt unit tests
target_wrapper.*
# QtCreator
*.autosave
# QtCreator Qml
*.qmlproject.user
*.qmlproject.user.*
# QtCreator CMake
CMakeLists.txt.user*
# QtCreator 4.8< compilation database
compile_commands.json
# QtCreator local machine specific files for imported projects
*creator.user*
*_qmlcache.qrc

View file

@ -1,251 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
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
StyledText {
text: "Emoji & Unicode Launcher Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: "Search and copy emojis and unicode characters directly from the launcher."
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - 32
}
StyledRect {
width: parent.width - 32
height: 1
color: Theme.outlineVariant
}
Column {
spacing: 12
width: parent.width - 32
StyledText {
text: "Trigger Configuration"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
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 emojis and unicode characters."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
Row {
spacing: 12
CheckBox {
id: noTriggerToggle
text: "No trigger (always show)"
checked: loadSettings("noTrigger", false)
contentItem: StyledText {
text: noTriggerToggle.text
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
leftPadding: noTriggerToggle.indicator.width + 8
verticalAlignment: Text.AlignVCenter
}
indicator: StyledRect {
implicitWidth: 20
implicitHeight: 20
radius: Theme.cornerRadiusSmall
border.color: noTriggerToggle.checked ? Theme.primary : Theme.outline
border.width: 2
color: noTriggerToggle.checked ? Theme.primary : "transparent"
StyledRect {
width: 12
height: 12
anchors.centerIn: parent
radius: 2
color: Theme.onPrimary
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
StyledText {
text: "Trigger:"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
DankTextField {
id: triggerField
width: 100
height: 40
text: loadSettings("trigger", ":")
placeholderText: ":"
backgroundColor: Theme.surfaceContainer
textColor: Theme.surfaceText
onTextEdited: {
const newTrigger = text.trim()
saveSettings("trigger", newTrigger || ":")
saveSettings("noTrigger", newTrigger === "")
}
}
StyledText {
text: "Examples: :, ;, /emoji, etc."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
}
StyledRect {
width: parent.width - 32
height: 1
color: Theme.outlineVariant
}
Column {
spacing: 8
width: parent.width - 32
StyledText {
text: "Features:"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Column {
spacing: 4
leftPadding: 16
StyledText {
text: "• 1k+ emojis & unicode symbols (faces, tools, math, currency)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• Nerd Font glyph catalog for launcher / terminal icons"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• Search by name, character, or keyword"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• Click to copy to clipboard"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
StyledRect {
width: parent.width - 32
height: 1
color: Theme.outlineVariant
}
Column {
spacing: 8
width: parent.width - 32
StyledText {
text: "Usage:"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Column {
spacing: 4
leftPadding: 16
bottomPadding: 24
StyledText {
text: "1. Open Launcher (Ctrl+Space or click launcher button)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: noTriggerToggle.checked ? "2. Emojis are always visible in the launcher" : "2. Type your trigger (default: :) to filter to emojis/unicode"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: noTriggerToggle.checked ? "3. Search by typing: 'smile', 'heart', 'copyright', etc." : "3. Search by typing: ': smile', ': heart', ': copyright', etc."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "4. Select and press Enter to copy to clipboard"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
function saveSettings(key, value) {
if (pluginService) {
pluginService.savePluginData("emojiLauncher", key, value)
}
}
function loadSettings(key, defaultValue) {
if (pluginService) {
return pluginService.loadPluginData("emojiLauncher", key, defaultValue)
}
return defaultValue
}
}

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 DankMaterialShell Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,130 +0,0 @@
# Emoji & Unicode Launcher
A DankMaterialShell launcher plugin that provides quick access to thousands of emojis, mathematical symbols, and Nerd Font glyphs with instant clipboard copying.
![Emoji & Unicode Launcher Screenshot](screenshot.png)
## Features
- **Expanded Emoji Catalog** - 900+ curated emoji entries combined with the bundled emoji dataset for complete coverage (gear, toolbox, etc.)
- **Unicode & Math Symbols** - Hundreds of useful unicode characters from arrows to operators and currency signs
- **Nerd Font Glyphs** - Searchable Nerd Font icons for launcher, terminal, and code workflows
- **Instant Copy** - One-click copy to clipboard with visual confirmation
- **Smart Search** - Search by name, character, or keywords
- **Configurable Trigger** - Default `:` or set your own trigger (or disable for always-on)
- **Toast Notifications** - Visual feedback for every action
## Installation
### From Plugin Registry (Recommended)
```bash
# Coming soon - will be available via DMS plugin manager
```
### Manual Installation
```bash
# Copy plugin to DMS plugins directory
cp -r EmojiLauncher ~/.config/DankMaterialShell/plugins/
# Enable in DMS
# 1. Open Settings (Ctrl+,)
# 2. Go to Plugins tab
# 3. Click "Scan for Plugins"
# 4. Toggle "Emoji & Unicode Launcher" to enable
```
## Usage
### Default Trigger Mode
1. Open launcher (Ctrl+Space)
2. Type `:` followed by search query
3. Examples:
- `:smile` - Find smiling emojis
- `:heart` - Find heart emojis
- `:copyright` - Find © symbol
- `:arrow` - Find arrow characters
4. Select item and press Enter to copy
### Always-On Mode
Configure in settings to show emoji/unicode items without a trigger prefix.
## Search Examples
**Emojis:**
- `smile` → 😀 😃 😄 😁 😊
- `heart` → ❤️ 🧡 💛 💚 💙 💜
- `fire` → 🔥
- `star` → ⭐ ✨ 🌟
**Unicode Characters:**
- `copyright` → ©
- `trademark` → ™
- `degree` → °
- `pi` → π
- `arrow` → → ← ↑ ↓
- `infinity` → ∞
- `euro` → €
## Configuration
Access settings via DMS Settings → Plugins → Emoji & Unicode Launcher:
- **Trigger**: Set custom trigger character (`:`, `;`, `/emoji`, etc.) or disable for always-on mode
- **No Trigger Mode**: Toggle to show items without trigger prefix
## Character Database
### Data Sources
- `data/emojis.txt` — comprehensive emoji list (Terminal Root)
- `data/math.txt` — math and general-purpose unicode symbols
- `data/nerdfont.txt` — curated Nerd Font glyph export
All files ship with the plugin, so search works fully offline.
### Highlights
- **Emoji coverage:** faces, hands, tools, activities, symbols, and flags (including gear ⚙️ and toolbox 🧰)
- **Unicode symbols:** math operators, arrows, currency, Greek letters, quotes, and miscellaneous symbols
- **Nerd Font glyphs:** VS Code Codicons, Powerline (ple-) separators, development icons, and other monospace-friendly glyphs for terminal/theming
### Updating the catalog
1. Modify the plain-text sources in `data/` (`emojis.txt`, `math.txt`, `nerdfont.txt`).
2. Run `scripts/generate_catalog.py` to rebuild `catalog.js` (the file bundled with the plugin). The script parses the text files, normalizes names, and refreshes search keywords.
## Requirements
- DankMaterialShell >= 0.1.0
- `wl-copy` (from wl-clipboard package)
- Wayland compositor (Niri, Hyprland, etc.)
## Compatibility
- **Compositors**: Niri and Hyprland
- **Distros**: Universal - works on any Linux distribution
## Technical Details
- **Type**: Launcher plugin
- **Trigger**: `:` (configurable)
- **Language**: QML (Qt Modeling Language)
- **Dependencies**: None (uses built-in character database)
## Contributing
Found a bug or want to add more characters? Open an issue or submit a pull request!
## Credits
Emoji database sourced from [Terminal Root's emoji collection](https://terminalroot.com/emojis.txt) - a comprehensive list of emojis with searchable names.
## License
MIT License - See LICENSE file for details
## Author
Created for the DankMaterialShell community
## Links
- [DankMaterialShell](https://github.com/AvengeMedia/DankMaterialShell)
- [Plugin Registry](https://github.com/AvengeMedia/dms-plugin-registry)

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
ref: refs/heads/main

View file

@ -1,9 +0,0 @@
[core]
bare = false
filemode = true
[remote "origin"]
url = https://github.com/devnullvoid/dms-emoji-launcher
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main

View file

@ -1 +0,0 @@
2951ec7f823c983c11b6b231403581a386a7c9f6

View file

@ -1 +0,0 @@
bd2df8723d417d11dc0ce6ddc225d25f707ce2f5

View file

@ -1,14 +0,0 @@
{
"id": "emojiLauncher",
"name": "Emoji & Unicode Launcher",
"description": "Search and copy emojis and unicode characters",
"version": "1.2.0",
"author": "devnullvoid",
"icon": "emoji_emotions",
"type": "launcher",
"capabilities": ["emoji-search", "unicode-search"],
"component": "./EmojiLauncher.qml",
"settings": "./EmojiLauncherSettings.qml",
"trigger": ":",
"permissions": ["settings_read", "settings_write"]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

View file

@ -1,161 +0,0 @@
#!/usr/bin/env python3
"""Builds the catalog.js bundle from the plain-text data sources."""
from __future__ import annotations
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / "data"
OUTPUT = ROOT / "catalog.js"
def main() -> None:
emoji_entries = parse_emoji(DATA / "emojis.txt")
unicode_entries = parse_unicode(DATA / "math.txt")
nerd_font_entries = parse_nerdfont(DATA / "nerdfont.txt")
write_catalog(
emoji_entries=emoji_entries,
unicode_entries=unicode_entries,
nerd_font_entries=nerd_font_entries,
)
def parse_emoji(path: Path) -> list[dict]:
return _parse_symbol_file(
path,
key_name="emoji",
extra_keywords=lambda raw: [],
)
def parse_unicode(path: Path) -> list[dict]:
return _parse_symbol_file(
path,
key_name="char",
extra_keywords=lambda raw: [],
)
def parse_nerdfont(path: Path) -> list[dict]:
def extra_keywords(raw: str) -> list[str]:
extras = ["nerdfont", raw]
if raw.startswith("ple-"):
extras.extend(["powerline", "ple"])
return extras
entries = _parse_symbol_file(
path,
key_name="char",
preprocess_name=lambda name: re.sub(r"[_-]+", " ", name).strip(),
extra_keywords=extra_keywords,
)
for entry in entries:
name = entry.get("name", "")
if name.startswith("Ple "):
entry["name"] = "Powerline " + name[4:]
return entries
def _parse_symbol_file(
path: Path,
*,
key_name: str,
preprocess_name=lambda name: name,
extra_keywords=lambda raw: [],
) -> list[dict]:
text = path.read_text(encoding="utf-8")
entries: list[dict] = []
for raw_line in text.splitlines():
if not raw_line:
continue
trimmed = raw_line.strip()
if not trimmed or trimmed.startswith("#"):
continue
match = re.match(r"^(\S+|\s)\s+(.*)$", raw_line)
if not match:
continue
symbol = match.group(1)
raw_name = match.group(2).strip()
if not raw_name or ".." in symbol:
continue
cleaned_name = preprocess_name(raw_name) or raw_name
friendly = prettify_name(cleaned_name)
keywords = build_keywords(cleaned_name, extra_keywords(raw_name))
entry = {
key_name: symbol,
"name": friendly,
"keywords": keywords,
}
entries.append(entry)
return entries
def prettify_name(raw: str) -> str:
tokens = [token for token in re.split(r"\s+", raw) if token]
return " ".join(token.capitalize() for token in tokens)
def build_keywords(raw: str, extras: list[str]) -> list[str]:
seen: set[str] = set()
keywords: list[str] = []
def add_token(token: str) -> None:
lowered = token.lower()
if lowered and lowered not in seen:
seen.add(lowered)
keywords.append(lowered)
def tokenize(value: str) -> list[str]:
return [part for part in re.split(r"[^a-zA-Z0-9+]+", value.lower()) if part]
for token in tokenize(raw):
add_token(token)
for extra in extras:
for token in tokenize(extra):
add_token(token)
return keywords
def write_catalog(*, emoji_entries, unicode_entries, nerd_font_entries) -> None:
OUTPUT.write_text(
"\n".join(
[
"// Auto-generated by scripts/generate_catalog.py",
"// Do not edit manually.",
".pragma library",
"",
f"var emojiEntries = {json.dumps(emoji_entries, ensure_ascii=False)};",
f"var unicodeEntries = {json.dumps(unicode_entries, ensure_ascii=False)};",
f"var nerdFontEntries = {json.dumps(nerd_font_entries, ensure_ascii=False)};",
"",
"function clone(array) {",
" return array ? array.slice() : [];",
"}",
"",
"function getEmojiEntries() { return clone(emojiEntries); }",
"function getUnicodeEntries() { return clone(unicodeEntries); }",
"function getNerdFontEntries() { return clone(nerdFontEntries); }",
]
)
+ "\n",
encoding="utf-8",
)
if __name__ == "__main__":
main()

View file

@ -1,27 +0,0 @@
import Quickshell
import QtQuick
import qs.Common
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "mediaPlayer"
SliderSetting {
settingKey: "backgroundOpacity"
label: I18n.tr("Background Opacity")
defaultValue: 80
minimum: 0
maximum: 100
unit: "%"
}
SliderSetting {
settingKey: "borderOpacity"
label: I18n.tr("Border Opacity")
defaultValue: 100
minimum: 0
maximum: 100
unit: "%"
}
}

View file

@ -1,693 +0,0 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
DesktopPluginComponent {
id: root
// settings data here
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 80) / 100
property real borderOpacity: (pluginData.borderOpacity ?? 100) / 100
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
opacity: showNoPlayerNow ? 0 : 1
Behavior on opacity { NumberAnimation { duration: 300 } }
property MprisPlayer activePlayer: MprisController.activePlayer
property var allPlayers: MprisController.availablePlayers
property bool isSwitching: false
property string _lastArtUrl: ""
property string _bgArtSource: ""
property string activeTrackArtFile: ""
function loadArtwork(url) {
if (!url)
return;
if (url.startsWith("http://") || url.startsWith("https://")) {
const filename = "/tmp/.dankshell/trackart_" + Date.now() + ".jpg";
activeTrackArtFile = filename;
cleanupProcess.command = ["sh", "-c", "mkdir -p /tmp/.dankshell && find /tmp/.dankshell -name 'trackart_*' ! -name '" + filename.split('/').pop() + "' -delete"];
cleanupProcess.running = true;
imageDownloader.command = ["curl", "-L", "-s", "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", "-o", filename, url];
imageDownloader.targetFile = filename;
imageDownloader.running = true;
return;
}
_bgArtSource = url;
}
function maybeFinishSwitch() {
if (activePlayer && activePlayer.trackTitle !== "") {
isSwitching = false;
_switchHold = false;
_stalePositionDetected = false;
}
}
function getDisplayPosition() {
if (!activePlayer) return 0;
const rawPos = Math.max(0, activePlayer.position || 0);
const length = Math.max(1, activePlayer.length || 1);
// If we detected stale position, show 0 until proper data arrives
if (_stalePositionDetected) {
return 0;
}
// Handle stale position data when switching videos
if (isSwitching && rawPos >= length * 0.9) {
return 0;
}
const pos = activePlayer.length ? rawPos % Math.max(1, activePlayer.length) : rawPos;
return pos;
}
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return minutes + ":" + (secs < 10 ? "0" : "") + secs;
}
Component.onCompleted: {
// Initialize with current player state if available
if (activePlayer) {
// Get actual position after MPRIS fully loads
Qt.callLater(() => {
try {
const actualPos = activePlayer.position || 0;
const length = activePlayer.length || 1;
root._positionSnapshot = actualPos;
if (progressSeekbar && actualPos > 0) {
progressSeekbar.value = Math.min(1, actualPos / length);
}
} catch (e) {
// Handle MPRIS errors
}
});
}
}
// Derived "no players" state: always correct, no timers.
readonly property int _playerCount: allPlayers ? allPlayers.length : 0
readonly property bool _noneAvailable: _playerCount === 0
readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
property bool _switchHold: false
Timer {
id: _switchHoldTimer
interval: 650
repeat: false
onTriggered: {
_switchHold = false;
if (isSwitching) {
isSwitching = false;
}
}
}
onActivePlayerChanged: {
root._positionSnapshot = 0;
root._forceUpdate = !root._forceUpdate;
if (!activePlayer) {
isSwitching = false;
_switchHold = false;
return;
}
isSwitching = true;
_switchHold = true;
_switchHoldTimer.restart();
if (activePlayer.trackArtUrl)
loadArtwork(activePlayer.trackArtUrl);
// Get actual current position after a short delay to allow MPRIS to sync
Qt.callLater(() => {
try {
const actualPos = activePlayer.position || 0;
root._positionSnapshot = actualPos;
if (progressSeekbar && actualPos > 0) {
progressSeekbar.value = Math.min(1, actualPos / Math.max(1, activePlayer.length || 1));
isSwitching = false;
}
} catch (e) {
// Handle errors gracefully
}
});
}
// Responsive sizing with min/max constraints
property real userScale: 1.0
readonly property real minWidth: 320
readonly property real maxWidth: 800
readonly property real minHeight: 160
readonly property real maxHeight: 400
readonly property real baseWidth: 380
readonly property real baseHeight: 200
implicitWidth: Math.max(minWidth, Math.min(maxWidth, baseWidth * userScale))
implicitHeight: Math.max(minHeight, Math.min(maxHeight, baseHeight * userScale))
Connections {
target: activePlayer
function onTrackTitleChanged() {
root._positionSnapshot = 0;
root._forceUpdate = !root._forceUpdate;
// Force immediate position reset for new track
if (activePlayer.position > 0 && activePlayer.length > 0) {
const progressRatio = activePlayer.position / activePlayer.length;
if (progressRatio > 0.9) {
// Likely stale data - force reset
root._stalePositionDetected = true;
}
}
_switchHoldTimer.restart();
maybeFinishSwitch();
// Reset progress bar immediately on track change
if (progressSeekbar) {
progressSeekbar.value = 0;
}
}
function onTrackArtUrlChanged() {
if (activePlayer?.trackArtUrl) {
_lastArtUrl = activePlayer.trackArtUrl;
loadArtwork(activePlayer.trackArtUrl);
}
}
function onPositionChanged() {
try {
if (root._stalePositionDetected && activePlayer.position < activePlayer.length * 0.5) {
// Position updated properly now
root._stalePositionDetected = false;
root._forceUpdate = !root._forceUpdate;
}
} catch (e) {
// MPRIS service disappeared - reset state
root._stalePositionDetected = false;
}
}
}
Connections {
target: MprisController
function onAvailablePlayersChanged() {
const count = (MprisController.availablePlayers?.length || 0);
if (count === 0) {
isSwitching = false;
_switchHold = false;
} else {
_switchHold = true;
_switchHoldTimer.restart();
}
}
}
Process {
id: imageDownloader
running: false
property string targetFile: ""
onExited: exitCode => {
if (exitCode === 0 && targetFile)
_bgArtSource = "file://" + targetFile;
}
}
Process {
id: cleanupProcess
running: false
}
property bool isSeeking: false
property real _positionSnapshot: 0
property bool _forceUpdate: false
property real _animationTick: 0
property bool _stalePositionDetected: false
Timer {
id: positionUpdateTimer
interval: 100
running: true
repeat: true
onTriggered: {
// Update snapshot to trigger binding re-evaluation
if (activePlayer) {
try {
const newPosition = activePlayer.position || 0;
root._positionSnapshot = newPosition;
// Force progress bar refresh when switching
if (isSwitching || _stalePositionDetected) {
if (progressSeekbar) {
progressSeekbar.value = progressSeekbar.calculateProgress();
}
}
} catch (e) {
// Handle MPRIS service errors gracefully
root._positionSnapshot = 0;
}
}
}
}
// Use animation to drive constant updates for smooth progress bar
NumberAnimation {
id: progressUpdateAnimation
target: root
property: "_animationTick"
from: 0
to: 10000
duration: 10000
loops: Animation.Infinite
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
}
Item {
id: bgContainer
anchors.fill: parent
visible: _bgArtSource !== ""
Image {
id: bgImage
anchors.centerIn: parent
width: Math.max(parent.width, parent.height) * 1.1
height: width
// source: _bgArtSource
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
visible: false
onStatusChanged: {
if (status === Image.Ready)
maybeFinishSwitch();
}
}
Item {
id: blurredBg
anchors.fill: parent
visible: false
MultiEffect {
anchors.centerIn: parent
width: bgImage.width
height: bgImage.height
source: bgImage
blurEnabled: true
blurMax: 64
blur: 2
saturation: -0.2
brightness: -0.25
}
}
Rectangle {
id: bgMask
anchors.fill: parent
radius: Theme.cornerRadius
visible: false
layer.enabled: true
}
MultiEffect {
anchors.fill: parent
source: blurredBg
maskEnabled: true
maskSource: bgMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
opacity: 0.7
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surface, root.backgroundOpacity)
}
}
// --- Ocean Wave Background ---
Canvas {
id: waveCanvas
anchors.fill: parent
z: 1 // Ensures it stays behind the main content
opacity: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? 0.4 : 0.1
property real phase: 0
// This timer drives the animation "movement"
Timer {
interval: 16 // ~60 FPS
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
repeat: true
onTriggered: {
waveCanvas.phase += 0.05;
waveCanvas.requestPaint();
}
}
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
// Draw two overlapping waves for a "deep" ocean feel
drawWave(ctx, "#40" + Theme.primary.toString().substring(1), 0.5, 15, phase);
drawWave(ctx, "#60" + Theme.primary.toString().substring(1), 0.8, 10, phase * 0.7);
drawWave(ctx, "#70" + Theme.primary.toString().substring(1), 0.8, 10, phase * 0.9);
}
function drawWave(ctx, color, speed, amplitude, currentPhase) {
ctx.beginPath();
ctx.fillStyle = color;
var waveHeight = height * 0.7; // Position waves at the bottom 30%
ctx.moveTo(0, height);
for (var x = 0; x <= width; x += 5) {
// Sine wave calculation: y = amplitude * sin(frequency * x + phase)
var y = waveHeight + Math.sin(x * 0.02 + currentPhase) * amplitude;
ctx.lineTo(x, y);
}
ctx.lineTo(width, height);
ctx.closePath();
ctx.fill();
}
}
// --- End Ocean Wave Background ---
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
visible: showNoPlayerNow
DankIcon {
name: "music_note"
size: Theme.iconSize * 3
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Main content container - Layout with thumbnail
Item {
anchors.fill: parent
anchors.margins: Theme.spacingM * userScale
visible: !_noneAvailable && (!showNoPlayerNow)
Row {
anchors.fill: parent
spacing: Theme.spacingM * userScale
// Album Thumbnail Section (Left)
Rectangle {
id: thumbnailContainer
width: parent.height * 0.85
height: parent.height * 0.85
anchors.verticalCenter: parent.verticalCenter
radius: 6 * userScale
color: "transparent"
clip: true
property real albumRotation: 0
NumberAnimation {
id: rotationAnimation
target: thumbnailContainer
property: "albumRotation"
from: 0
to: 360
duration: 20000
running: activePlayer?.playbackState === MprisPlaybackState.Playing
loops: Animation.Infinite
}
DankAlbumArt {
id: albumArt
width: parent.width * 0.76
height: parent.height * 0.76
anchors.centerIn: parent
activePlayer: root.activePlayer
rotation: thumbnailContainer.albumRotation
}
// Subtle border
Rectangle {
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, root.borderOpacity)
border.width: 1
}
}
// Content Section (Right)
Column {
width: parent.width - thumbnailContainer.width - parent.spacing
height: parent.height
spacing: Theme.spacingS * userScale
// Song Info Section (Top)
Column {
id: songInfo
width: parent.width
spacing: 2 * userScale
StyledText {
text: activePlayer?.trackTitle || "The (Overdue) Collapse of Wind..."
font.pixelSize: Theme.fontSizeMedium * 1.1 * userScale
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
text: activePlayer?.trackArtist || "Catalyst"
font.pixelSize: Theme.fontSizeSmall * userScale
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
}
}
// Spacer
Item {
width: parent.width
height: Theme.spacingXS * userScale
}
// Controls Row (Middle)
Row {
id: controlsRow
width: parent.width
spacing: Theme.spacingS * userScale
// Previous Button
Rectangle {
width: 32 * userScale
height: 32 * userScale
radius: 4 * userScale
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 28 * userScale
color: Theme.primary
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer)
return;
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0;
} else {
activePlayer.previous();
}
}
}
}
// Play/Pause Button
Rectangle {
width: 32 * userScale
height: 32 * userScale
radius: 4 * userScale
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 28 * userScale
color: Theme.primary
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.togglePlaying()
}
}
// Next Button
Rectangle {
width: 32 * userScale
height: 32 * userScale
radius: 4 * userScale
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 28 * userScale
color: Theme.primary
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
}
// Spacer to push buttons to right
Item {
width: parent.width - (32 * 3 * userScale) - (Theme.spacingS * 3 * userScale)
height: 1
}
}
// Seekbar Section (Bottom)
Column {
id: seekbarSection
width: parent.width
spacing: 2 * userScale
// Integrated DankSeekbar
DankSeekbar {
id: progressSeekbar
width: parent.width
height: 16 * userScale // Consistent with previous design
activePlayer: root.activePlayer
// Keep the Tab's seeking state in sync with the component
onIsSeekingChanged: root.isSeeking = isSeeking
function calculateProgress() {
if (!root.activePlayer || root.activePlayer.length <= 0) return 0;
const rawPos = Math.max(0, root.activePlayer.position || 0);
const length = Math.max(1, root.activePlayer.length || 1);
// If we detected stale position, show 0 until proper data arrives
if (root._stalePositionDetected) {
// Check if position is now valid
if (rawPos < length * 0.8) {
root._stalePositionDetected = false;
root.isSwitching = false;
} else {
return 0;
}
}
// Reset if position seems stale (at end for new video)
if (root.isSwitching && rawPos >= length * 0.9) {
root._stalePositionDetected = true;
return 0;
}
// Force position refresh when switching videos
if (root.isSwitching && rawPos > 0 && rawPos < length * 0.8) {
root.isSwitching = false;
}
return Math.min(1, rawPos / length);
}
// Connect position updates to seekbar value directly
Timer {
interval: 50
running: true
repeat: true
onTriggered: {
if (progressSeekbar && activePlayer) {
try {
progressSeekbar.value = progressSeekbar.calculateProgress();
} catch (e) {
// Handle MPRIS errors
}
}
}
}
}
// Time labels
Item {
width: parent.width
height: 12 * userScale
StyledText {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: {
// Force dependency on position updates
root._positionSnapshot;
return formatTime(getDisplayPosition());
}
font.pixelSize: Theme.fontSizeSmall * 0.9 * userScale
color: Theme.surfaceVariantText
}
StyledText {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: {
if (!activePlayer || !activePlayer.length)
return "0:00";
const dur = Math.max(0, activePlayer.length || 0);
const minutes = Math.floor(dur / 60);
const seconds = Math.floor(dur % 60);
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
}
font.pixelSize: Theme.fontSizeSmall * 0.9 * userScale
color: Theme.surfaceVariantText
}
}
}
}
}
}
}

View file

@ -1,51 +0,0 @@
# Media Player Plugin for Dank Material Shell
![Media Player Screenshot](screenshot_8.png)
A feature-rich media player widget for Dank Material Shell with MPRIS support, volume control, and a beautiful, responsive interface.
## Features
- **MPRIS Integration**: Control any media player that supports the MPRIS D-Bus interface (e.g., Spotify, VLC, MPV, Firefox).
- **Beautiful Visuals**:
- **Dynamic Album Art**: Displays track artwork with a smooth rotation animation during playback.
- **Animated Ocean Wave**: A unique, animated wave background that reacts to the playback state.
- **Blurred Background**: Uses the current track's artwork to create a cohesive, blurred background effect.
- **Playback Controls**:
- Play/Pause toggle.
- Skip to previous and next tracks.
- Smooth seekbar for precise track navigation.
- **Responsive Design**: Automatically adjusts its layout and scaling to fit different screen sizes and user preferences.
- **Remote Artwork Support**: Automatically downloads and caches high-quality artwork from web URLs.
- **Customizable Settings**: Adjust the background opacity to match your desktop theme.
## Configuration
You can customize the following settings in the plugin configuration:
- **Background Opacity**: Adjust the transparency of the player's background (0% to 100%).
## Technical Details
- **Type**: Desktop Widget
- **Capabilities**: `desktop-widget`, `media-player`, `mpris`, `audio-control`
- **Permissions**:
- `settings_read` / `settings_write`: To manage user preferences.
- `mpris_control`: To interact with media players.
- `audio_control`: For system volume management.
## Note
> [!NOTE]
> The media player widget will only be visible when there is an active media player (e.g., Spotify, VLC, Firefox) running and playing media. If no active media session is detected, the widget will automatically hide itself.
## Installation
Ensure this plugin is placed in your Dank Material Shell plugins directory:
`~/.config/DankMaterialShell/plugins/mediaPlayer`
## Requirements
- **Quickshell**: The underlying shell framework.
- **MPRIS compatible player**: Required for media control.
- **Curl**: Used for downloading remote album art.

View file

@ -1 +0,0 @@
ref: refs/heads/main

View file

@ -1,9 +0,0 @@
[core]
bare = false
filemode = true
[remote "origin"]
url = https://github.com/arrifat346afs/mediaPlayer
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main

View file

@ -1 +0,0 @@
4d73e6c810f85093339eb6cc6dad251ea6c15b04

View file

@ -1 +0,0 @@
4d73e6c810f85093339eb6cc6dad251ea6c15b04

View file

@ -1,18 +0,0 @@
{
"id": "mediaPlayer",
"name": "Media Player",
"description": "A feature-rich media player widget with MPRIS support, volume control, playlist management, and audio device switching",
"version": "1.0.0",
"author": "arrifat346afs",
"type": "desktop",
"capabilities": ["desktop-widget", "media-player", "mpris", "audio-control"],
"component": "./MediaPlayerTab.qml",
"settings": "./MediaPlayerSettings.qml",
"icon": "music_note",
"permissions": [
"settings_read",
"settings_write",
"mpris_control",
"audio_control"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 KiB

View file

@ -1 +0,0 @@
/home/lew/.config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather

View file

@ -1 +0,0 @@
/home/lew/.config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks