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:
parent
9d16d6e6b0
commit
b18328bbad
96 changed files with 24119 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# desktopCommand
|
||||
Desktop Command - DMS plugin that allows running shell command and displaying its output on the desktop
|
||||

|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/main
|
||||
|
|
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
0ff6dad312fa8532e4e152eb37507a11f2fd4663
|
||||
|
|
@ -0,0 +1 @@
|
|||
0ff6dad312fa8532e4e152eb37507a11f2fd4663
|
||||
|
|
@ -0,0 +1 @@
|
|||
__pycache__
|
||||
|
|
@ -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("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace('"', """)
|
||||
)
|
||||
|
||||
|
||||
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 " "
|
||||
html_parts: List[str] = []
|
||||
open_style = ""
|
||||
for cell in line:
|
||||
cell = cell or {"ch": " ", "style": ""}
|
||||
safe_char = " " 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()
|
||||
|
|
@ -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"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue