Add .config/DankMaterialShell/firefox.css

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

View file

@ -0,0 +1,21 @@
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

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

View file

@ -0,0 +1,250 @@
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

@ -0,0 +1,229 @@
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.

After

Width:  |  Height:  |  Size: 194 KiB

View file

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

View file

@ -0,0 +1,9 @@
[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

@ -0,0 +1 @@
0ff6dad312fa8532e4e152eb37507a11f2fd4663

View file

@ -0,0 +1 @@
0ff6dad312fa8532e4e152eb37507a11f2fd4663

View file

@ -0,0 +1 @@
__pycache__

View file

@ -0,0 +1,493 @@
#!/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

@ -0,0 +1,14 @@
{
"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"]
}