diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/config.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/config.qml
new file mode 100644
index 0000000..ad9aa63
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/config.qml
@@ -0,0 +1,23 @@
+import QtQuick
+
+import org.kde.plasma.configuration
+
+ConfigModel {
+ ConfigCategory {
+ name: i18n("Command")
+ icon: "configure"
+ source: "config/ConfigCommand.qml"
+ }
+
+ ConfigCategory {
+ name: i18n("Appearance")
+ icon: "preferences-desktop-color"
+ source: "config/ConfigAppearance.qml"
+ }
+
+ ConfigCategory {
+ name: i18n("Actions")
+ icon: "preferences-desktop-mouse"
+ source: "config/ConfigActions.qml"
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/main.xml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/main.xml
new file mode 100644
index 0000000..783239f
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/main.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+ 1000
+
+
+
+ echo "Test $(date +%S)"
+
+
+
+
+
+
+
+
+ false
+
+
+ 13
+
+
+
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ 128
+
+
+
+
+
+
+
+
+ false
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+ 100
+
+
+ false
+
+
+ 100
+
+
+ false
+
+
+
+
+
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/bz/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/bz/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo
new file mode 100644
index 0000000..df0d692
Binary files /dev/null and b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/bz/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo differ
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/fr/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/fr/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo
new file mode 100644
index 0000000..a08f792
Binary files /dev/null and b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/fr/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo differ
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/nl/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/nl/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo
new file mode 100644
index 0000000..36a84c9
Binary files /dev/null and b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/nl/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo differ
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/pt_br/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/pt_br/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo
new file mode 100644
index 0000000..79e4651
Binary files /dev/null and b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/pt_br/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo differ
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigActions.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigActions.qml
new file mode 100644
index 0000000..02fc47b
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigActions.qml
@@ -0,0 +1,83 @@
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+import org.kde.plasma.core as PlasmaCore
+
+import "../libconfig" as LibConfig
+
+LibConfig.FormKCM {
+
+ //-------------------------------------------------------
+ LibConfig.Heading {
+ text: i18n("Tooltip")
+ label.horizontalAlignment: Text.AlignHCenter
+ }
+ ColumnLayout {
+ Kirigami.FormData.isSection: true
+ Layout.preferredWidth: 360 * Screen.devicePixelRatio
+ spacing: 0
+ QQC2.Label {
+ text: i18n("Hover Command:")
+ }
+ LibConfig.TextField {
+ Layout.fillWidth: true
+ configKey: 'tooltipCommand'
+ wrapMode: LibConfig.TextField.Wrap
+ }
+ }
+
+
+ //-------------------------------------------------------
+ LibConfig.Heading {
+ text: i18n("Click")
+ label.horizontalAlignment: Text.AlignHCenter
+ }
+ ColumnLayout {
+ Kirigami.FormData.isSection: true
+ Layout.preferredWidth: 360 * Screen.devicePixelRatio
+ spacing: 0
+ QQC2.Label {
+ text: i18n("Run Command:")
+ }
+ LibConfig.TextField {
+ Layout.fillWidth: true
+ configKey: 'clickCommand'
+ wrapMode: LibConfig.TextField.Wrap
+ }
+ }
+
+
+ //-------------------------------------------------------
+ LibConfig.Heading {
+ text: i18n("Mouse Wheel")
+ label.horizontalAlignment: Text.AlignHCenter
+ }
+ ColumnLayout {
+ Kirigami.FormData.isSection: true
+ Layout.preferredWidth: 360 * Screen.devicePixelRatio
+ spacing: 0
+ QQC2.Label {
+ text: i18n("Scroll Up:")
+ }
+ LibConfig.TextField {
+ Layout.fillWidth: true
+ configKey: 'mousewheelUpCommand'
+ wrapMode: LibConfig.TextField.Wrap
+ }
+ }
+ ColumnLayout {
+ Kirigami.FormData.isSection: true
+ Layout.preferredWidth: 360 * Screen.devicePixelRatio
+ spacing: 0
+ QQC2.Label {
+ text: i18n("Scroll Down:")
+ }
+ LibConfig.TextField {
+ Layout.fillWidth: true
+ configKey: 'mousewheelDownCommand'
+ wrapMode: LibConfig.TextField.Wrap
+ }
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigAppearance.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigAppearance.qml
new file mode 100644
index 0000000..d23a255
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigAppearance.qml
@@ -0,0 +1,94 @@
+import QtQuick
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+import org.kde.plasma.core as PlasmaCore
+
+import "../libconfig" as LibConfig
+
+LibConfig.FormKCM {
+
+ //-------------------------------------------------------
+ LibConfig.Heading {
+ text: i18n("Font")
+ }
+ LibConfig.FontFamily {
+ Kirigami.FormData.label: i18n("Font Family:")
+ configKey: 'fontFamily'
+ }
+ LibConfig.SpinBox {
+ Kirigami.FormData.label: i18n("Font Size:")
+ configKey: 'fontSize'
+ suffix: i18n("px")
+ }
+ LibConfig.TextFormat {
+ boldConfigKey: 'bold'
+ italicConfigKey: 'italic'
+ underlineConfigKey: 'underline'
+ alignConfigKey: 'textAlign'
+ vertAlignConfigKey: 'vertAlign'
+ }
+
+
+ //-------------------------------------------------------
+ LibConfig.Heading {
+ text: i18n("Colors")
+ }
+ LibConfig.ColorField {
+ Kirigami.FormData.label: i18n("Text:")
+ configKey: 'textColor'
+ defaultColor: PlasmaCore.Theme.textColor
+ }
+ RowLayout {
+ Kirigami.FormData.label: i18n("Outline:")
+ spacing: 0
+ LibConfig.CheckBox {
+ configKey: 'showOutline'
+ }
+ LibConfig.ColorField {
+ configKey: 'outlineColor'
+ defaultColor: PlasmaCore.Theme.backgroundColor
+ }
+ }
+
+
+ //-------------------------------------------------------
+ LibConfig.Heading {
+ text: i18n("Misc")
+ }
+ LibConfig.CheckBox {
+ Kirigami.FormData.label: i18n("Desktop Widget:")
+ configKey: 'showBackground'
+ text: i18n("Show background")
+ visible: plasmoid.location == PlasmaCore.Types.Floating
+ }
+ RowLayout {
+ Kirigami.FormData.label: i18n("Fixed Width:")
+ spacing: 0
+ visible: plasmoid.formFactor == PlasmaCore.Types.Horizontal
+ LibConfig.CheckBox {
+ configKey: 'useFixedWidth'
+ }
+ LibConfig.SpinBox {
+ configKey: 'fixedWidth'
+ suffix: i18n("px")
+ }
+ }
+ RowLayout {
+ Kirigami.FormData.label: i18n("Fixed Height:")
+ spacing: 0
+ visible: plasmoid.formFactor == PlasmaCore.Types.Vertical
+ LibConfig.CheckBox {
+ configKey: 'useFixedHeight'
+ }
+ LibConfig.SpinBox {
+ configKey: 'fixedHeight'
+ suffix: i18n("px")
+ }
+ }
+ LibConfig.CheckBox {
+ Kirigami.FormData.label: i18n("Replace all newlines with spaces")
+ configKey: 'replaceAllNewlines'
+ }
+
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigCommand.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigCommand.qml
new file mode 100644
index 0000000..446f6b8
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigCommand.qml
@@ -0,0 +1,41 @@
+import QtQuick
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+import org.kde.plasma.core as PlasmaCore
+
+import "../libconfig" as LibConfig
+
+LibConfig.FormKCM {
+
+ property alias cfg_command: command.text
+ property alias cfg_interval: interval.value
+ property alias cfg_waitForCompletion: waitForCompletion.checked
+
+ //-------------------------------------------------------
+ LibConfig.Heading {
+ text: i18n("Command")
+ }
+ LibConfig.TextField {
+ id: command
+ // configKey: 'command'
+ Kirigami.FormData.isSection: true
+ Layout.fillWidth: true
+ Layout.preferredWidth: 240 * Screen.devicePixelRatio // + ~180px from label column
+ wrapMode: LibConfig.TextField.Wrap
+ }
+ LibConfig.SpinBox {
+ id: interval
+ Kirigami.FormData.label: i18n("Run every ")
+ configKey: 'interval'
+ suffix: i18n("ms")
+ stepSize: 500
+ }
+ LibConfig.CheckBox {
+ id: waitForCompletion
+ Kirigami.FormData.label: i18n("Wait for completion")
+ // configKey: 'waitForCompletion'
+ text: i18n("Enabled")
+ }
+
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/CheckBox.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/CheckBox.qml
new file mode 100644
index 0000000..1b5dcbb
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/CheckBox.qml
@@ -0,0 +1,12 @@
+// Version 5
+
+import QtQuick
+import QtQuick.Controls as QQC2
+
+QQC2.CheckBox {
+ id: configCheckBox
+
+ property string configKey: ''
+ checked: plasmoid.configuration[configKey]
+ onClicked: plasmoid.configuration[configKey] = !plasmoid.configuration[configKey]
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/ColorField.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/ColorField.qml
new file mode 100644
index 0000000..264064d
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/ColorField.qml
@@ -0,0 +1,159 @@
+// Version 8
+
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Dialogs as QtDialogs
+import QtQuick.Window
+import org.kde.kirigami as Kirigami
+
+// https://doc.qt.io/qt-6/qtgraphicaleffects5-index.html
+import Qt5Compat.GraphicalEffects as QtGraphicalEffects // TODO Deprecated in Qt6
+
+
+QQC2.TextField {
+ id: colorField
+ font.family: "monospace"
+ readonly property string defaultText: "#AARRGGBB"
+ placeholderText: defaultColor ? defaultColor : defaultText
+
+ onTextChanged: {
+ // Make sure the text is:
+ // Empty (use default)
+ // or #123 or #112233 or #11223344 before applying the color.
+ if (text.length === 0
+ || (text.indexOf('#') === 0 && (text.length == 4 || text.length == 7 || text.length == 9))
+ ) {
+ colorField.value = text
+ }
+ }
+
+ property bool showAlphaChannel: true
+ property bool showPreviewBg: true
+
+ property string configKey: ''
+ property string defaultColor: ''
+ property string value: {
+ if (configKey) {
+ return plasmoid.configuration[configKey]
+ } else {
+ return "#000"
+ }
+ }
+
+ readonly property color defaultColorValue: defaultColor
+ readonly property color valueColor: {
+ if (value == '' && defaultColor) {
+ return defaultColor
+ } else {
+ return value
+ }
+ }
+
+ onValueChanged: {
+ if (!activeFocus) {
+ text = colorField.value
+ }
+ if (configKey) {
+ if (value == defaultColorValue) {
+ plasmoid.configuration[configKey] = ""
+ } else {
+ plasmoid.configuration[configKey] = value
+ }
+ }
+ }
+
+ leftPadding: rightPadding + mouseArea.height + rightPadding
+
+ FontMetrics {
+ id: fontMetrics
+ font.family: colorField.font.family
+ font.italic: colorField.font.italic
+ font.pointSize: colorField.font.pointSize
+ font.pixelSize: colorField.font.pixelSize
+ font.weight: colorField.font.weight
+ }
+ readonly property int defaultWidth: Math.ceil(fontMetrics.advanceWidth(defaultText))
+ implicitWidth: rightPadding + Math.max(defaultWidth, contentWidth) + leftPadding
+
+ MouseArea {
+ id: mouseArea
+ anchors.leftMargin: parent.rightPadding
+ anchors.topMargin: parent.topPadding
+ anchors.bottomMargin: parent.bottomPadding
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: height
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+
+ onClicked: dialogLoader.active = true
+
+ // Color Preview Circle
+ Rectangle {
+ id: previewBgMask
+ visible: false
+ anchors.fill: parent
+ border.width: 1 * Screen.devicePixelRatio
+ border.color: "transparent"
+ radius: width / 2
+ }
+ QtGraphicalEffects.ConicalGradient {
+ id: previewBgGradient
+ visible: colorField.showPreviewBg
+ anchors.fill: parent
+ angle: 0.0
+ gradient: Gradient {
+ GradientStop { position: 0.00; color: "white" }
+ GradientStop { position: 0.24; color: "white" }
+ GradientStop { position: 0.25; color: "#cccccc" }
+ GradientStop { position: 0.49; color: "#cccccc" }
+ GradientStop { position: 0.50; color: "white" }
+ GradientStop { position: 0.74; color: "white" }
+ GradientStop { position: 0.75; color: "#cccccc" }
+ GradientStop { position: 1.00; color: "#cccccc" }
+ }
+ source: previewBgMask
+ }
+ Rectangle {
+ id: previewFill
+ anchors.fill: parent
+ color: colorField.valueColor
+ border.width: 1 * Screen.devicePixelRatio
+ border.color: Kirigami.ColorUtils.linearInterpolation(color, Kirigami.Theme.textColor, 0.5)
+ radius: width / 2
+ }
+ }
+
+ Loader {
+ id: dialogLoader
+ active: false
+ sourceComponent: QtDialogs.ColorDialog {
+ id: dialog
+ visible: true
+ modality: Qt.WindowModal
+ options: colorField.showAlphaChannel ? QtDialogs.ColorDialog.ShowAlphaChannel : 0
+ selectedColor: colorField.valueColor
+ onSelectedColorChanged: {
+ if (visible) {
+ colorField.text = selectedColor
+ }
+ }
+ onAccepted: {
+ colorField.text = selectedColor
+ dialogLoader.active = false
+ }
+ onRejected: {
+ // This event is also triggered when the user clicks outside the popup modal.
+ // TODO Find a way to only trigger when Cancel is clicked.
+ colorField.text = initColor
+ dialogLoader.active = false
+ }
+
+ property color initColor
+ Component.onCompleted: {
+ initColor = colorField.valueColor
+ }
+ }
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/ComboBox.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/ComboBox.qml
new file mode 100644
index 0000000..18b6aff
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/ComboBox.qml
@@ -0,0 +1,102 @@
+// Version 8
+
+import QtQuick
+import QtQuick.Controls as QQC2
+
+/*
+** Example:
+**
+import './libconfig' as LibConfig
+LibConfig.ComboBox {
+ configKey: "appDescription"
+ model: [
+ { value: "a", text: i18n("A") },
+ { value: "b", text: i18n("B") },
+ { value: "c", text: i18n("C") },
+ ]
+}
+LibConfig.ComboBox {
+ configKey: "appDescription"
+ populated: false
+ onPopulate: {
+ model = [
+ { value: "a", text: i18n("A") },
+ { value: "b", text: i18n("B") },
+ { value: "c", text: i18n("C") },
+ ]
+ populated = true
+ }
+}
+*/
+QQC2.ComboBox {
+ id: configComboBox
+
+ property string configKey: ''
+ readonly property string configValue: configKey ? plasmoid.configuration[configKey] : ""
+ onConfigValueChanged: {
+ if (!focus && value != configValue) {
+ selectValue(configValue)
+ }
+ }
+
+ readonly property var currentItem: currentIndex >= 0 ? model[currentIndex] : null
+
+ textRole: "text" // Doesn't autodeduce from model if we manually populate it
+
+ // Note that ComboBox.valueRole and ComboBox.currentValue was introduced in Qt 5.14.
+ // Ubuntu 20.04 only has Qt 5.12. We cannot define a currentValue property or it will
+ // break when users upgrade to Qt 5.14.
+ property string _valueRole: "value"
+ readonly property var _currentValue: _valueRole && currentIndex >= 0 ? model[currentIndex][_valueRole] : null
+ readonly property alias value: configComboBox._currentValue
+
+ model: []
+
+ signal populate()
+ property bool populated: true
+
+ Component.onCompleted: {
+ populate()
+ selectValue(configValue)
+ }
+
+ onCurrentIndexChanged: {
+ if (typeof model !== 'number' && 0 <= currentIndex && currentIndex < count) {
+ var item = model[currentIndex]
+ if (typeof item !== "undefined") {
+ var val = item[_valueRole]
+ if (configKey && (typeof val !== "undefined") && populated) {
+ plasmoid.configuration[configKey] = val
+ }
+ }
+ }
+ }
+
+ function size() {
+ if (typeof model === "number") {
+ return model
+ } else if (typeof model.count === "number") {
+ return model.count
+ } else if (typeof model.length === "number") {
+ return model.length
+ } else {
+ return 0
+ }
+ }
+
+ function findValue(val) {
+ for (var i = 0; i < size(); i++) {
+ if (model[i][_valueRole] == val) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ function selectValue(val) {
+ var index = findValue(val)
+ if (index >= 0) {
+ currentIndex = index
+ }
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FontFamily.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FontFamily.qml
new file mode 100644
index 0000000..3f9f8bb
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FontFamily.qml
@@ -0,0 +1,24 @@
+// Version 4
+
+import QtQuick
+
+import "." as LibConfig
+
+LibConfig.ComboBox {
+ id: configFontFamily
+
+ populated: false
+
+ // Based on: org.kde.plasma.digitalclock
+ onPopulate: {
+ var arr = [] // Use temp array to avoid constant binding stuff
+ arr.push({ text: i18nc("Use default font", "Default"), value: "" })
+
+ var fonts = Qt.fontFamilies()
+ for (var i = 0; i < fonts.length; i++) {
+ arr.push({ text: fonts[i], value: fonts[i] })
+ }
+ model = arr
+ populated = true
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FormKCM.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FormKCM.qml
new file mode 100644
index 0000000..e2ba60b
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FormKCM.qml
@@ -0,0 +1,33 @@
+// Version 3
+
+import QtQuick
+import QtQuick.Window
+import org.kde.kirigami as Kirigami
+import org.kde.kcmutils as KCM
+
+KCM.SimpleKCM {
+ id: simpleKCM
+ default property alias _formChildren: formLayout.data
+
+ Kirigami.FormLayout {
+ id: formLayout
+ }
+
+ // https://invent.kde.org/plasma/plasma-desktop/-/blame/master/desktoppackage/contents/configuration/AppletConfiguration.qml
+ // AppletConfiguration.implicitWidth: Kirigami.Units.gridUnit * 40 = 720
+ // AppletConfiguration.Layout.minimumWidth: Kirigami.Units.gridUnit * 30 = 540
+ // In practice, Window.width = 744px is a typical FormLayout.wideMode switchWidth
+ // A rough guess is 128+24+180+10+360+24+20 = 746px
+ // TabSidebar=128x, Padding=24px, Labels=180px, Spacing=10px, Controls=360px, Padding=24px, Scrollbar=20px
+ // However the default is only 720px. So we'll set it to a 800px minimum to avoid wideMode=false
+ property int wideModeMinWidth: 800 * Screen.devicePixelRatio
+ Window.onWindowChanged: {
+ if (Window.window) {
+ Window.window.visibleChanged.connect(function(){
+ if (Window.window && Window.window.visible && Window.window.width < wideModeMinWidth) {
+ Window.window.width = wideModeMinWidth
+ }
+ })
+ }
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/Heading.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/Heading.qml
new file mode 100644
index 0000000..bff0043
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/Heading.qml
@@ -0,0 +1,83 @@
+// Version 6
+
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+/*
+** Example:
+**
+import './libconfig' as LibConfig
+LibConfig.Heading {
+ text: i18n("SpinBox (Double)")
+}
+*/
+
+// While the following Kirigami is very simple:
+// Kirigami.Separator {
+// Kirigami.FormData.label: "Heading"
+// Kirigami.FormData.isSection: true
+// }
+//
+// I want to be able to adjust the label size and make it bold.
+// Kirigami's buddy Heading is level=3, which does not stand out
+// very well. I also want to center the heading.
+// Since we can't access the Heading in the buddy component, we
+// need to make sure the Heading has no text, and draw our own.
+ColumnLayout {
+ id: heading
+ spacing: 0
+
+ property string text: ""
+ property alias separator: separator
+ property alias label: label
+ property bool useThickTopMargin: true
+
+ property Item __formLayout: {
+ if (parent && typeof parent.wideMode === 'boolean') {
+ return parent
+ } else if (typeof formLayout !== 'undefined' && typeof formLayout.wideMode === 'boolean') {
+ return formLayout
+ } else if (typeof page !== 'undefined' && typeof page.wideMode === 'boolean') {
+ return page
+ } else {
+ return null
+ }
+ }
+
+ Layout.fillWidth: true
+ // Kirigami.FormData.isSection: true
+ Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel
+
+ Kirigami.Separator {
+ id: separator
+ visible: false
+ Layout.fillWidth: true
+ Layout.topMargin: Kirigami.Units.largeSpacing
+ Layout.bottomMargin: Kirigami.Units.largeSpacing
+ }
+
+ Kirigami.Heading {
+ id: label
+ Layout.topMargin: useThickTopMargin ? Kirigami.Units.largeSpacing * 3 : Kirigami.Units.largeSpacing
+ Layout.bottomMargin: Kirigami.Units.smallSpacing
+ Layout.fillWidth: true
+ text: heading.text
+ level: 1
+ font.weight: Font.Bold
+ // horizontalAlignment: (!__formLayout || __formLayout.wideMode) ? Text.AlignHCenter : Text.AlignLeft
+ verticalAlignment: (!__formLayout || __formLayout.wideMode) ? Text.AlignVCenter : Text.AlignBottom
+ }
+}
+
+//--- Test Default Kirigami Heading
+// Kirigami.Separator {
+// property string text: ""
+// Kirigami.FormData.label: text
+// Kirigami.FormData.isSection: true
+// property alias separator: separator
+// Item {
+// id: separator
+// }
+// }
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/SpinBox.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/SpinBox.qml
new file mode 100644
index 0000000..eb8908e
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/SpinBox.qml
@@ -0,0 +1,225 @@
+// Version 7
+
+import QtQuick
+import QtQuick.Controls as QQC2
+
+/*
+** Example:
+**
+import './libconfig' as LibConfig
+// Integer
+LibConfig.SpinBox {
+ configKey: "leftPadding"
+ suffix: "px"
+ from: 0
+ to: 1000
+ stepSize: 5
+}
+// Double
+LibConfig.SpinBox {
+ configKey: "distance"
+ decimals: 3
+ suffix: "m"
+ minimumValue: 0.0
+ maximumValue: 1000.0
+ stepSize: Math.round(0.5 * factor)
+}
+*/
+// QQC1.SpinBox: https://github.com/qt/qtquickcontrols/blob/dev/src/controls/SpinBox.qml
+// QQC2.SpinBox: https://github.com/qt/qtquickcontrols2/blob/5.15/src/imports/controls/SpinBox.qml
+// KDE Config Theme: https://invent.kde.org/frameworks/qqc2-desktop-style/-/blob/master/org.kde.desktop/SpinBox.qml
+// Qt6 QQC2.SpinBox: https://github.com/qt/qtquickcontrols2/blob/dev/src/imports/controls/basic/SpinBox.qml
+QQC2.SpinBox {
+ id: spinBox
+
+ property string configKey: ''
+ readonly property var configValue: configKey ? plasmoid.configuration[configKey] : 0
+
+ readonly property real factor: Math.pow(10, decimals)
+ readonly property real valueReal: value / factor
+ value: Math.round(configValue * factor)
+ onValueRealChanged: serializeTimer.start()
+
+ readonly property int spinBox_MININT: Math.ceil(-2147483648 / factor)
+ readonly property int spinBox_MAXINT: Math.floor(2147483647 / factor)
+ from: Math.round(minimumValue * factor)
+ to: Math.round(maximumValue * factor)
+
+ // Reimplement QQC1 properties
+ // https://github.com/qt/qtquickcontrols/blob/dev/src/controls/SpinBox.qml
+ property int decimals: 0
+ property alias prefix: prefixLabel.text
+ property alias suffix: suffixLabel.text
+ property real minimumValue: 0
+ property real maximumValue: spinBox_MAXINT
+
+ // Avoid selecting prefix/suffix by drawing them overlayed on top.
+ // As a bonus, we can draw with in a different color (textColor at 60% opacity).
+ QQC2.Label {
+ id: prefixLabel
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: spinBox.leftPadding
+ anchors.topMargin: spinBox.topPadding
+ anchors.bottomMargin: spinBox.bottomPadding
+ font: spinBox.font
+ horizontalAlignment: Qt.AlignHCenter
+ verticalAlignment: Qt.AlignVCenter
+ color: spinBox.palette.text
+ opacity: 0.6
+ }
+ QQC2.Label {
+ id: suffixLabel
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.rightMargin: spinBox.rightPadding
+ anchors.topMargin: spinBox.topPadding
+ anchors.bottomMargin: spinBox.bottomPadding
+ font: spinBox.font
+ horizontalAlignment: Qt.AlignHCenter
+ verticalAlignment: Qt.AlignVCenter
+ color: spinBox.palette.text
+ opacity: 0.6
+ }
+
+ Timer { // throttle
+ id: serializeTimer
+ interval: 300
+ onTriggered: {
+ if (configKey) {
+ if (decimals == 0) {
+ plasmoid.configuration[configKey] = spinBox.value
+ } else {
+ plasmoid.configuration[configKey] = spinBox.valueReal
+ }
+ }
+ }
+ }
+
+ // Note: Qt5 used RegExpValidator { regExp: /[\-\.\d]+/ }
+ // validator: RegularExpressionValidator {
+ // regularExpression: /[\-\.\d]+/
+ // }
+ validator: DoubleValidator {
+ bottom: Math.min(spinBox.from, spinBox.to)
+ top: Math.max(spinBox.from, spinBox.to)
+ decimals: spinBox.decimals
+ notation: DoubleValidator.StandardNotation
+ }
+
+ textFromValue: function(value, locale) {
+ return Number(value / factor).toFixed(decimals)
+ }
+
+ valueFromText: function(text, locale) {
+ var text2 = text
+ .replace(/[^\-\.\d]/g, '') // Remove non digit characters
+ .replace(/\.+/g, '.') // Allow user to type '.' instead of RightArrow to enter to decimals
+ var val = Number(text2)
+ if (isNaN(val)) {
+ val = -0
+ }
+ // console.log('valueFromText', text, val)
+ return Math.round(val * factor)
+ }
+
+ // Select value on foucs
+ onActiveFocusChanged: {
+ if (activeFocus) {
+ selectValue()
+ }
+ }
+ function selectValue() {
+ // Check if SpinBox.contentItem == TextInput
+ // https://invent.kde.org/frameworks/qqc2-desktop-style/-/blob/master/org.kde.desktop/SpinBox.qml
+ // https://doc.qt.io/qt-5/qml-qtquick-textinput.html#select-method
+ if (contentItem && contentItem instanceof TextInput) {
+ contentItem.selectAll()
+ }
+ }
+
+ function fixMinus(str) {
+ var minusIndex = str.indexOf('-')
+ if (minusIndex >= 0) {
+ var a = str.substr(0, minusIndex)
+ var b = str.substr(minusIndex+1)
+ console.log('a', a, 'b', b)
+
+ return '-' + a + b
+ } else {
+ return str
+ }
+ }
+ function fixDecimals(str) {
+ var periodIndex = str.indexOf('.')
+ var a = str.substr(0, periodIndex+1)
+ var b = str.substr(periodIndex+1)
+ return a + b.replace(/\.+/g, '') // Remove extra periods
+ }
+
+ function fixText(str) {
+ return fixMinus(fixDecimals(str))
+ }
+
+ function onTextEdited() {
+ var oldText = spinBox.contentItem.text
+ // console.log('onTextEdited', 'oldText1', oldText)
+ oldText = fixText(oldText)
+ // console.log('onTextEdited', 'oldText2', oldText)
+ var oldPeriodIndex = oldText.indexOf('.')
+ if (oldPeriodIndex == -1) {
+ oldPeriodIndex = oldText.length
+ }
+ var oldCursorPosition = spinBox.contentItem.cursorPosition
+ var oldCursorDelta = oldPeriodIndex - oldCursorPosition
+
+ spinBox.value = spinBox.valueFromText(oldText, spinBox.locale)
+ spinBox.valueModified()
+
+ var newText = spinBox.contentItem.text
+ // console.log('onTextEdited', 'newText1', newText)
+ newText = fixText(newText)
+ // console.log('onTextEdited', 'newText2', newText)
+ var newPeriodIndex = newText.indexOf('.')
+ if (newPeriodIndex == -1) {
+ newPeriodIndex = newText.length
+ }
+ if (newText != spinBox.contentItem.text) {
+ spinBox.contentItem.text = Qt.binding(function(){
+ return spinBox.textFromValue(spinBox.value, spinBox.locale)
+ })
+ }
+ spinBox.contentItem.cursorPosition = newPeriodIndex - oldCursorDelta
+ }
+
+ function bindContentItem() {
+ if (contentItem && contentItem instanceof TextInput) {
+ // We bind the left/right padding in the TextInput so that
+ // clicking the prefix/suffix will focus the TextInput. If we set
+ // the SpinBox left/right padding, then they do not focus the TextInput.
+ contentItem.leftPadding = Qt.binding(function(){ return prefixLabel.implicitWidth })
+ contentItem.rightPadding = Qt.binding(function(){ return suffixLabel.implicitWidth })
+
+ // Bind value update on keypress, while retaining cursor position
+ spinBox.contentItem.textEdited.connect(spinBox.onTextEdited)
+ }
+ }
+
+ onContentItemChanged: {
+ bindContentItem()
+ }
+
+ Component.onCompleted: {
+ for (var i = 0; i < data.length; i++) {
+ if (data[i] instanceof Connections) {
+ // Remove the Connections where it changes the text/cursor when typing.
+ // onTextEdited { value = valueFromText() }
+ data[i].destroy()
+ break
+ }
+ }
+ bindContentItem()
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextAlign.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextAlign.qml
new file mode 100644
index 0000000..daaff61
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextAlign.qml
@@ -0,0 +1,57 @@
+// Version 4
+
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+
+RowLayout {
+ id: configTextAlign
+
+ property string configKey: ''
+ readonly property string configValue: configKey ? plasmoid.configuration[configKey] : ""
+
+ function setValue(val) {
+ if (configKey) {
+ plasmoid.configuration[configKey] = val
+ }
+ updateChecked()
+ }
+
+ function updateChecked() {
+ // button.checked bindings are unbound when clicked
+ justifyLeftButton.checked = Qt.binding(function(){ return configValue == Text.AlignLeft })
+ justifyCenterButton.checked = Qt.binding(function(){ return configValue == Text.AlignHCenter })
+ justifyRightButton.checked = Qt.binding(function(){ return configValue == Text.AlignRight })
+ justifyFillButton.checked = Qt.binding(function(){ return configValue == Text.AlignJustify })
+ }
+
+ Component.onCompleted: updateChecked()
+
+ QQC2.Button {
+ id: justifyLeftButton
+ icon.name: "format-justify-left-symbolic"
+ checkable: true
+ onClicked: setValue(Text.AlignLeft)
+ }
+
+ QQC2.Button {
+ id: justifyCenterButton
+ icon.name: "format-justify-center-symbolic"
+ checkable: true
+ onClicked: setValue(Text.AlignHCenter)
+ }
+
+ QQC2.Button {
+ id: justifyRightButton
+ icon.name: "format-justify-right-symbolic"
+ checkable: true
+ onClicked: setValue(Text.AlignRight)
+ }
+
+ QQC2.Button {
+ id: justifyFillButton
+ icon.name: "format-justify-fill-symbolic"
+ checkable: true
+ onClicked: setValue(Text.AlignJustify)
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextField.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextField.qml
new file mode 100644
index 0000000..69fd6a0
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextField.qml
@@ -0,0 +1,48 @@
+// Version 6
+
+import QtQuick
+import QtQuick.Controls as QQC2
+import org.kde.kirigami as Kirigami
+
+QQC2.TextField {
+ id: textField
+
+ property string configKey: ''
+ readonly property string configValue: configKey ? plasmoid.configuration[configKey] : ""
+ onConfigValueChanged: {
+ if (!focus && text != configValue) {
+ text = configValue
+ }
+ }
+
+ text: configValue
+ onTextChanged: serializeTimer.start()
+
+ property string defaultValue: ""
+
+ rightPadding: clearButton.width + Kirigami.Units.smallSpacing
+
+ property alias clearButton: clearButton
+ QQC2.ToolButton {
+ id: clearButton
+ visible: textField.text != textField.defaultValue
+ icon.name: "edit-clear"
+ onClicked: textField.text = defaultValue
+
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+
+ width: implicitBackgroundHeight
+ }
+
+ Timer { // throttle
+ id: serializeTimer
+ interval: 300
+ onTriggered: {
+ if (configKey) {
+ plasmoid.configuration[configKey] = textField.text
+ }
+ }
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextFormat.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextFormat.qml
new file mode 100644
index 0000000..b3f2bdb
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextFormat.qml
@@ -0,0 +1,63 @@
+// Version 6
+
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+import "." as LibConfig
+
+Flow {
+ id: configTextFormat
+
+ spacing: Kirigami.Units.smallSpacing * 2
+ Layout.fillWidth: true
+
+ property alias boldConfigKey: configBold.configKey
+ property alias italicConfigKey: configItalic.configKey
+ property alias underlineConfigKey: configUnderline.configKey
+ property alias alignConfigKey: configTextAlign.configKey
+ property alias vertAlignConfigKey: configVertAlign.configKey
+
+ RowLayout {
+ QQC2.Button {
+ id: configBold
+ property string configKey: ''
+ visible: configKey
+ icon.name: 'format-text-bold-symbolic'
+ checkable: true
+ checked: configKey ? plasmoid.configuration[configKey] : false
+ onClicked: plasmoid.configuration[configKey] = checked
+ }
+
+ QQC2.Button {
+ id: configItalic
+ property string configKey: ''
+ visible: configKey
+ icon.name: 'format-text-italic-symbolic'
+ checkable: true
+ checked: configKey ? plasmoid.configuration[configKey] : false
+ onClicked: plasmoid.configuration[configKey] = checked
+ }
+
+ QQC2.Button {
+ id: configUnderline
+ property string configKey: ''
+ visible: configKey
+ icon.name: 'format-text-underline-symbolic'
+ checkable: true
+ checked: configKey ? plasmoid.configuration[configKey] : false
+ onClicked: plasmoid.configuration[configKey] = checked
+ }
+ }
+
+ LibConfig.TextAlign {
+ id: configTextAlign
+ visible: configKey
+ }
+
+ LibConfig.VertAlign {
+ id: configVertAlign
+ visible: configKey
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/VertAlign.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/VertAlign.qml
new file mode 100644
index 0000000..5c4d585
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/VertAlign.qml
@@ -0,0 +1,49 @@
+// Version 5
+
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+
+RowLayout {
+ id: configVertAlign
+
+ property string configKey: ''
+ readonly property string configValue: configKey ? plasmoid.configuration[configKey] : ""
+
+ function setValue(val) {
+ if (configKey) {
+ plasmoid.configuration[configKey] = val
+ }
+ updateChecked()
+ }
+
+ function updateChecked() {
+ // button.checked bindings are unbound when clicked
+ vertTopButton.checked = Qt.binding(function(){ return configValue == Text.AlignTop })
+ vertCenterButton.checked = Qt.binding(function(){ return configValue == Text.AlignVCenter })
+ vertBottomButton.checked = Qt.binding(function(){ return configValue == Text.AlignBottom })
+ }
+
+ Component.onCompleted: updateChecked()
+
+ QQC2.Button {
+ id: vertTopButton
+ icon.name: "align-vertical-top"
+ checkable: true
+ onClicked: setValue(Text.AlignTop)
+ }
+
+ QQC2.Button {
+ id: vertCenterButton
+ icon.name: "align-vertical-center"
+ checkable: true
+ onClicked: setValue(Text.AlignVCenter)
+ }
+
+ QQC2.Button {
+ id: vertBottomButton
+ icon.name: "align-vertical-bottom"
+ checkable: true
+ onClicked: setValue(Text.AlignBottom)
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/main.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/main.qml
new file mode 100644
index 0000000..fce613d
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/main.qml
@@ -0,0 +1,393 @@
+import QtQuick
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+import org.kde.plasma.plasmoid
+import org.kde.plasma.core as PlasmaCore
+import org.kde.plasma.components as PlasmaComponent
+import org.kde.plasma.plasma5support as Plasma5Support
+
+PlasmoidItem {
+ id: widget
+
+ // https://github.com/KDE/plasma-workspace/blob/master/dataengines/executable/executable.h
+ // https://github.com/KDE/plasma-workspace/blob/master/dataengines/executable/executable.cpp
+ // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/core/datasource.h
+ // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/core/datasource.cpp
+ // https://github.com/KDE/plasma-framework/blob/master/src/plasma/scripting/dataenginescript.cpp
+ Plasma5Support.DataSource {
+ id: executable
+ engine: "executable"
+ connectedSources: []
+ onNewData: (sourceName, data) => {
+ var exitCode = data["exit code"]
+ var exitStatus = data["exit status"]
+ var stdout = data["stdout"]
+ var stderr = data["stderr"]
+ exited(sourceName, exitCode, exitStatus, stdout, stderr)
+ disconnectSource(sourceName) // cmd finished
+ }
+ function exec(cmd) {
+ if (cmd) {
+ connectSource(cmd)
+ }
+ }
+ signal exited(string cmd, int exitCode, int exitStatus, string stdout, string stderr)
+ }
+
+ function performClick() {
+ executable.exec(plasmoid.configuration.clickCommand)
+ }
+
+ function performMouseWheelUp() {
+ executable.exec(plasmoid.configuration.mousewheelUpCommand)
+ }
+
+ function performMouseWheelDown() {
+ executable.exec(plasmoid.configuration.mousewheelDownCommand)
+ }
+
+ Item {
+ id: config
+ readonly property bool active: !!command
+ readonly property bool waitForCompletion: plasmoid.configuration.waitForCompletion
+ readonly property int interval: Math.max(0, plasmoid.configuration.interval)
+ readonly property string command: plasmoid.configuration.command || ''
+ readonly property string tooltipCommand: plasmoid.configuration.tooltipCommand || ''
+ readonly property bool clickEnabled: !!plasmoid.configuration.clickCommand
+ readonly property bool mousewheelEnabled: (plasmoid.configuration.mousewheelUpCommand || plasmoid.configuration.mousewheelDownCommand)
+ readonly property color textColor: plasmoid.configuration.textColor || Kirigami.Theme.textColor
+ readonly property color outlineColor: plasmoid.configuration.outlineColor || Kirigami.Theme.backgroundColor
+ readonly property bool showOutline: plasmoid.configuration.showOutline
+
+ onCommandChanged: widget.runCommand()
+ onTooltipCommandChanged: widget.runCommand()
+ onIntervalChanged: {
+ // interval=0 stops the timer even with Timer.repeat=true, so we may
+ // need to restart the timer. Might as well restart the interval too.
+ timer.restart()
+ }
+ onWaitForCompletionChanged: {
+ if (!waitForCompletion) {
+ // The timer needs to be restarted in case the timer was already
+ // triggered and the command is running. If we don't restart the
+ // timer, it'll stop forever.
+ timer.restart()
+ }
+ }
+ }
+
+ // https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
+ property var ansiColors: ({
+ 30: '#000000', // Black
+ 31: '#aa0000', // Red
+ 32: '#00aa00', // Green
+ 33: '#aa6500', // Yellow
+ 34: '#0000aa', // Blue
+ 35: '#aa00aa', // Magenta
+ 36: '#00aaaa', // Cyan
+ 37: '#aaaaaa', // White
+ 90: '#656565', // Bright Black
+ 91: '#ff6565', // Bright Red
+ 92: '#65ff65', // Bright Green
+ 93: '#ffff65', // Bright Yellow
+ 94: '#6565ff', // Bright Blue
+ 95: '#ff65ff', // Bright Magenta
+ 96: '#65ffff', // Bright Cyan
+ 97: '#ffffff', // Bright White
+ })
+ function resetState(state) {
+ var out = state.closeTags.join(' ')
+ state.bold = false
+ state.closeTags = []
+ return out
+ }
+ function parseAnsiCode(n, i, tokens, state) {
+ if (n == 0) { // Reset
+ return resetState(state)
+ } else if (n == 1) {
+ state.closeTags.push('')
+ state.bold = true
+ return ''
+ } else if (30 <= n && n <= 37 || 90 <= n && n <= 97) {
+ if (state.bold && 30 <= n && n <= 37) {
+ // Bold also intensifies the colors to "Bright".
+ // 30 => 90
+ n += 60
+ }
+ var hexColor = ansiColors[n]
+ state.closeTags.push('')
+ return ''
+ } else {
+ return ''
+ }
+ }
+ // https://stackoverflow.com/questions/4745317/converting-integers-to-hex-string-in-javascript
+ function formatHexInt(n) {
+ var num = Number(n)
+ if (isNaN(num)) {
+ return "00"
+ }
+ num = Math.max(0, Math.min(num, 255))
+ var str = num.toString(16)
+ return str.length == 1 ? '0' + str : str
+ }
+ function rgbToHex(r, g, b) {
+ return '#' + formatHexInt(r) + formatHexInt(g) + formatHexInt(b)
+ }
+ function parseColorMode(i, tokens) {
+ var colorMode = parseInt(tokens[++i], 10)
+ if (colorMode == 2) { // RGB
+ var r = parseInt(tokens[++i], 10)
+ var g = parseInt(tokens[++i], 10)
+ var b = parseInt(tokens[++i], 10)
+ return rgbToHex(r, g, b)
+ } else if (colorMode == 5) { // Preset of 256 colors
+ // Logic taken from Konsole
+ // https://invent.kde.org/utilities/konsole/-/blob/master/src/autotests/CharacterColorTest.cpp#L159
+ var n = parseInt(tokens[++i], 10)
+ if (0 <= n && n <= 7) { // Normal
+ var u = n + 30
+ return ansiColors[u]
+ } else if (8 <= n && n <= 15) { // Bright
+ var u = n - 8 + 90
+ return ansiColors[u]
+ } else if (16 <= n && n <= 231) { // 212
+ var u = n - 16
+ var r = Math.floor(((u / 36) % 6) != 0 ? (40 * ((u / 36) % 6) + 55) : 0)
+ var g = Math.floor(((u / 6) % 6) != 0 ? (40 * ((u / 6) % 6) + 55) : 0)
+ var b = Math.floor(((u / 1) % 6) != 0 ? (40 * ((u / 1) % 6) + 55) : 0)
+ return rgbToHex(r, g, b)
+ } else if (232 <= n && n <= 255) {
+ var gray = Math.floor((n - 232) * 10 + 8)
+ return rgbToHex(gray, gray, gray)
+ }
+ }
+ return null
+ }
+ function parseAnsiEscape(codes, state) {
+ var tokens = codes.split(';')
+ var out = ''
+ for (var i = 0; i < tokens.length; i++) {
+ tokens[i] = parseInt(tokens[i], 10)
+ }
+ for (var i = 0; i < tokens.length; i++) {
+ var token = tokens[i]
+ if (token == 38) { // Set FG
+ var hexColor = parseColorMode(i, tokens)
+ if (hexColor) {
+ state.closeTags.push('')
+ out += ''
+ }
+ } else if (token == 48) { // Set BG
+ var hexColor = parseColorMode(i, tokens)
+ // Ignore
+ } else {
+ out += parseAnsiCode(token, i, tokens, state)
+ }
+ }
+ return out
+ }
+
+ property string outputText: ''
+ property string tooltipText: ''
+
+ function formatOutputText(stdout) {
+ var formattedText = stdout
+
+ // Newlines
+ if (plasmoid.configuration.replaceAllNewlines) {
+ formattedText = formattedText.replace(/\n/g, ' ').trim()
+ } else if (formattedText.length >= 1 && formattedText[formattedText.length-1] == '\n') {
+ formattedText = formattedText.substr(0, formattedText.length-1)
+ }
+
+ // Terminal Colors (Issue #7)
+ var state = {
+ html: false,
+ bold: false,
+ closeTags: [],
+ }
+ formattedText = formattedText.replace(/\033\[(\d+(;\d+)*)?m/g, function(match, p1, p2){
+ state.html = true
+ if (typeof p1 === 'string') {
+ return parseAnsiEscape(p1, state)
+ } else { // \033[m is Reset
+ return parseAnsiEscape('0', state)
+ }
+ })
+ formattedText += resetState(state)
+
+ // Format Newlines when in HTML mode
+ if (state.html) {
+ formattedText = formattedText.replace(/\n/g, '
')
+ }
+ return formattedText
+ }
+
+ Connections {
+ target: executable
+ function onExited(cmd, exitCode, exitStatus, stdout, stderr) {
+ if ((cmd == config.command) || (cmd == config.tooltipCommand)) {
+ var formattedText = formatOutputText(stdout)
+
+ // console.log('[commandoutput]', 'stdout', JSON.stringify(stdout))
+ // console.log('[commandoutput]', 'format', JSON.stringify(formattedText))
+
+ if (cmd == config.command) {
+ widget.outputText = formattedText
+ } else if (cmd == config.tooltipCommand) {
+ widget.tooltipText = formattedText
+ }
+
+ if (config.waitForCompletion) {
+ timer.restart()
+ }
+ }
+ }
+ }
+
+ function runCommand() {
+ // console.log('[commandoutput]', Date.now(), 'runCommand', config.command)
+ executable.exec(config.command)
+ executable.exec(config.tooltipCommand)
+ }
+
+ Timer {
+ id: timer
+ interval: config.interval
+ running: true
+ repeat: !config.waitForCompletion
+ onTriggered: widget.runCommand()
+ // onIntervalChanged: console.log('interval', interval)
+ // onRunningChanged: console.log('running', running)
+ // onRepeatChanged: console.log('repeat', repeat)
+
+ Component.onCompleted: {
+ // Run right away in case the interval is very long.
+ triggered()
+ }
+ }
+
+ Plasmoid.onActivated: widget.performClick()
+
+ Plasmoid.backgroundHints: plasmoid.configuration.showBackground ? PlasmaCore.Types.DefaultBackground : PlasmaCore.Types.NoBackground
+
+ fullRepresentation: Item {
+ id: panelItem
+
+ readonly property bool isHorizontal: plasmoid.formFactor == PlasmaCore.Types.Horizontal
+ readonly property bool isVertical: plasmoid.formFactor == PlasmaCore.Types.Vertical
+ readonly property bool isInPanel: isHorizontal || isVertical
+ readonly property bool isOnDesktop: !isInPanel
+
+ readonly property int itemWidth: {
+ if (isOnDesktop) {
+ return Math.ceil(output.contentWidth)
+ } else if (isHorizontal && plasmoid.configuration.useFixedWidth) {
+ return plasmoid.configuration.fixedWidth * Kirigami.Units.devicePixelRatio
+ } else { // isHorizontal || isVertical
+ return Math.ceil(output.implicitWidth)
+ }
+ }
+ Layout.minimumWidth: isHorizontal ? itemWidth : -1
+ Layout.fillWidth: isVertical
+ Layout.preferredWidth: itemWidth // Panel widget default
+ // width: itemWidth // Desktop widget default
+ // onItemWidthChanged: console.log('itemWidth', itemWidth, 'implicitWidth', output.implicitWidth, 'contentWidth', output.contentWidth)
+
+ readonly property int itemHeight: {
+ if (isOnDesktop) {
+ return Math.ceil(output.contentHeight)
+ } else if (isVertical && plasmoid.configuration.useFixedHeight) {
+ return plasmoid.configuration.fixedHeight * Kirigami.Units.devicePixelRatio
+ } else { // isHorizontal || isVertical
+ return Math.ceil(output.implicitHeight)
+ }
+ }
+ Layout.minimumHeight: isVertical ? itemHeight : -1
+ Layout.fillHeight: isHorizontal
+ Layout.preferredHeight: itemHeight // Panel widget default
+ // height: itemHeight // Desktop widget default
+ // onItemHeightChanged: console.log('itemHeight', itemHeight, 'implicitHeight', output.implicitHeight, 'contentHeight', output.contentHeight)
+
+
+ // Note MouseArea is below the Text so
+ // that we don't eat the link clicks.
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: config.clickEnabled
+
+ cursorShape: output.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
+
+ onClicked: {
+ widget.performClick()
+ }
+
+ property int wheelDelta: 0
+ onWheel: (wheel) => {
+ var delta = wheel.angleDelta.y || wheel.angleDelta.x
+ wheelDelta += delta
+ // Magic number 120 for common "one click"
+ // See: http://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop
+ while (wheelDelta >= 120) {
+ wheelDelta -= 120
+ widget.performMouseWheelUp()
+ }
+ while (wheelDelta <= -120) {
+ wheelDelta += 120
+ widget.performMouseWheelDown()
+ }
+ wheel.accepted = true
+ }
+ }
+
+ Text {
+ id: output
+ width: parent.width
+ height: parent.height
+
+ PlasmaCore.ToolTipArea {
+ anchors.fill: parent
+ mainText: widget.tooltipText
+ enabled: widget.tooltipText
+ }
+
+ text: widget.outputText
+
+ color: config.textColor
+ style: config.showOutline ? Text.Outline : Text.Normal
+ styleColor: config.outlineColor
+
+ linkColor: Kirigami.Theme.linkColor
+ onLinkActivated: Qt.openUrlExternally(link)
+
+ font.pointSize: plasmoid.configuration.fontSize
+ font.family: plasmoid.configuration.fontFamily || Kirigami.Theme.defaultFont.family
+ font.weight: plasmoid.configuration.bold ? Font.Bold : Font.Normal
+ font.italic: plasmoid.configuration.italic
+ font.underline: plasmoid.configuration.underline
+ fontSizeMode: Text.FixedSize
+ horizontalAlignment: plasmoid.configuration.textAlign
+ verticalAlignment: plasmoid.configuration.vertAlign
+
+ property bool isFixedWidth: {
+ if (plasmoid.formFactor == PlasmaCore.Types.Planar) { // Desktop Widget
+ return true
+ } else if (plasmoid.formFactor == PlasmaCore.Types.Horizontal) {
+ return plasmoid.configuration.useFixedWidth
+ } else if (plasmoid.formFactor == PlasmaCore.Types.Vertical) {
+ return true
+ } else {
+ return false
+ }
+ }
+ elide: Text.ElideRight
+ wrapMode: isFixedWidth ? Text.Wrap : Text.NoWrap
+ }
+
+ }
+
+ preferredRepresentation: fullRepresentation
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/metadata.json b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/metadata.json
new file mode 100644
index 0000000..f8e09a1
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/metadata.json
@@ -0,0 +1,36 @@
+{
+ "KPackageStructure": "Plasma/Applet",
+ "KPlugin": {
+ "Authors": [
+ {
+ "Email": "zrenfire@gmail.com",
+ "Name": "Chris Holland"
+ },
+ {
+ "Email": "aricaldeira@gmail.com",
+ "Name": "Ari Caldeira"
+ },
+ {
+ "Email": "oded@geek.co.il",
+ "Name": "Oded Arbel"
+ }
+ ],
+ "Category": "System Information",
+ "Description": "Run a command periodically and render its output.",
+ "Description[bz]": "Ezekuta un komandu a kada X milisegundus, i mòstra a saida/rezuwtadu",
+ "Description[fr]": "Exécute une commande périodiquement et affiche son retour.",
+ "Description[nl]": "Voer een opdracht periodiek uit en toon de uitvoer ervan.",
+ "Description[pt_br]": "Executa um comando a cada X milissegundos, e mostra a saída/resultado",
+ "Icon": "utilities-terminal",
+ "Id": "com.github.zren.commandoutput",
+ "License": "GPL-2.0+",
+ "Name": "Command Output",
+ "Name[bz]": "Saida di Komandus",
+ "Name[fr]": "Command Output",
+ "Name[nl]": "Opdrachtuitvoer",
+ "Name[pt_br]": "Saída de Comandos",
+ "Version": "14",
+ "Website": "https://github.com/Zren/plasma-applet-commandoutput"
+ },
+ "X-Plasma-API-Minimum-Version": "6.0"
+}
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/ReadMe.md b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/ReadMe.md
new file mode 100644
index 0000000..f24b36e
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/ReadMe.md
@@ -0,0 +1,53 @@
+# Translate
+
+## Status
+
+| Locale | Lines | % Done|
+|----------|---------|-------|
+| Template | 30 | |
+| bz | 26/30 | 86% |
+| fr | 30/30 | 100% |
+| nl | 30/30 | 100% |
+| pt_br | 27/30 | 90% |
+
+
+## New Translations
+
+* Fill out [`template.pot`](template.pot) with your translations then open a [new issue](https://github.com/Zren/plasma-applet-commandoutput/issues/new), name the file `spanish.txt`, attach the txt file to the issue (drag and drop).
+
+Or if you know how to make a pull request
+
+* Copy the `template.pot` file and name it your locale's code (Eg: `en`/`de`/`fr`) with the extension `.po`. Then fill out all the `msgstr ""`.
+* Your region's locale code can be found at: https://stackoverflow.com/questions/3191664/list-of-all-locales-and-their-short-codes/28357857#28357857
+
+## Scripts
+
+Zren's `kpac` script can easily run the `gettext` commands for you, parsing the `metadata.json` and filling out any placeholders for you. `kpac` can be [downloaded here](https://github.com/Zren/plasma-applet-lib/blob/master/kpac) and should be placed at `~/Code/plasmoid-widgetname/kpac` to edit translations at `~/Code/plasmoid-widgetname/package/translate/`.
+
+
+* `python3 ./kpac i18n` will parse the `i18n()` calls in the `*.qml` files and write it to the `template.pot` file. Then it will merge any changes into the `*.po` language files. Then it converts the `*.po` files to it's binary `*.mo` version and move it to `contents/locale/...` which will bundle the translations in the `*.plasmoid` without needing the user to manually install them.
+* `python3 ./kpac localetest` will convert the `.po` to the `*.mo` files then run `plasmoidviewer` (part of `plasma-sdk`).
+
+## How it works
+
+Since KDE Frameworks v5.37, translations can be bundled with the zipped `*.plasmoid` file downloaded from the store.
+
+* `xgettext` extracts the messages from the source code into a `template.pot`.
+* Translators copy the `template.pot` to `fr.po` to translate the French language.
+* When the source code is updated, we use `msgmerge` to update the `fr.po` based on the updated `template.pot`.
+* When testing or releasing the widget, we convert the `.po` files to their binary `.mo` form with `msgfmt`.
+
+The binary `.mo` translation files are placed in `package/contents/locale/` so you may want to add `*.mo` to your `.gitignore`.
+
+```
+package/contents/locale/fr/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo
+```
+
+## Links
+
+* https://develop.kde.org/docs/plasma/widget/translations-i18n/
+* https://l10n.kde.org/stats/gui/trunk-kf5/team/fr/plasma-desktop/
+* https://techbase.kde.org/Development/Tutorials/Localization/i18n_Build_Systems
+* https://api.kde.org/frameworks/ki18n/html/prg_guide.html
+
+> Version 8 of [Zren's i18n scripts](https://github.com/Zren/plasma-applet-lib).
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/bz.po b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/bz.po
new file mode 100644
index 0000000..4c1ca45
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/bz.po
@@ -0,0 +1,166 @@
+# Translation of commandoutput in fr
+# Copyright (C) 2020
+# This file is distributed under the same license as the commandoutput package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: commandoutput\n"
+"Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-commandoutput\n"
+"POT-Creation-Date: 2024-03-12 16:30-0400\n"
+"PO-Revision-Date: 2020-10-05 21:55+020\n"
+"Last-Translator: Arí KAWDERA \n"
+"Language-Team: \n"
+"Language: bz_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../metadata.json
+msgid "Command Output"
+msgstr "Saida di Komandus"
+
+#: ../metadata.json
+msgid "Run a command periodically and render its output."
+msgstr "Ezekuta un komandu a kada X milisegundus, i mòstra a saida/rezuwtadu"
+
+#: ../contents/config/config.qml ../contents/ui/config/ConfigCommand.qml
+msgid "Command"
+msgstr "Komandu"
+
+#: ../contents/config/config.qml
+msgid "Appearance"
+msgstr "Aparensya"
+
+#: ../contents/config/config.qml
+msgid "Actions"
+msgstr "Asoyns"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Tooltip"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Hover Command:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Click"
+msgstr "Kliki"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Run Command:"
+msgstr "Rodar u komandu:"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Mouse Wheel"
+msgstr "Rodiỹa du mawzi"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Scroll Up:"
+msgstr "Rolar pra sima:"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Scroll Down:"
+msgstr "Rolar pra baxu:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font"
+msgstr "Fonti"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font Family:"
+msgstr "Familya:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font Size:"
+msgstr "Tamaỹu:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "px"
+msgstr "px"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Colors"
+msgstr "Koris"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Text:"
+msgstr "Testu:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Outline:"
+msgstr "Kontornu:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Misc"
+msgstr "Divèrsus"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Desktop Widget:"
+msgstr "Widigèti pru dèskitòpi:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Show background"
+msgstr "nuwn dexar transparenti"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Fixed Width:"
+msgstr "Largura fiksa:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Fixed Height:"
+msgstr "Awtura fiksa:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Replace all newlines with spaces"
+msgstr "Junta as liỹas numa liỹa sò, separandu kun espasus"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Run every "
+msgstr "Rodar a kada "
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "ms"
+msgstr ""
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Wait for completion"
+msgstr "Esperar a ezekusawn terminar"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Enabled"
+msgstr ""
+
+#: ../contents/ui/libconfig/FontFamily.qml
+msgctxt "Use default font"
+msgid "Default"
+msgstr "Padrawn"
+
+#~ msgid "Version: %1"
+#~ msgstr "Versawn: %1"
+
+#~ msgid "Mouse Click"
+#~ msgstr "Kliki du mawzi"
+
+#~ msgid "Command:"
+#~ msgstr "Komandu:"
+
+#~ msgid "pt"
+#~ msgstr "pt"
+
+#~ msgid "Desktop Widget: Show background"
+#~ msgstr "Widigèti pru dèskitòpi: nuwn dexar transparenti"
+
+#~ msgid "Tooltip Command"
+#~ msgstr "Komandu pra pasajen du mawzi"
+
+#~ msgid "Run"
+#~ msgstr "Ezekusawn"
+
+#~ msgid "Tooltip command"
+#~ msgstr "Komandu pra pasajen du mawzi"
+
+#~ msgid "General"
+#~ msgstr "Jeraw"
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/fr.po b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/fr.po
new file mode 100644
index 0000000..6748c7a
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/fr.po
@@ -0,0 +1,148 @@
+# Translation of commandoutput in fr
+# Copyright (C) 2020
+# This file is distributed under the same license as the commandoutput package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: commandoutput\n"
+"Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-commandoutput\n"
+"POT-Creation-Date: 2024-03-12 16:30-0400\n"
+"PO-Revision-Date: 2024-03-14 09:56+020\n"
+"Last-Translator: Sasha MOREL \n"
+"Language-Team: \n"
+"Language: fr_FR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../metadata.json
+msgid "Command Output"
+msgstr "Command Output"
+
+#: ../metadata.json
+msgid "Run a command periodically and render its output."
+msgstr "Exécute une commande périodiquement et affiche son retour."
+
+#: ../contents/config/config.qml ../contents/ui/config/ConfigCommand.qml
+msgid "Command"
+msgstr "Commande"
+
+#: ../contents/config/config.qml
+msgid "Appearance"
+msgstr "Apparence"
+
+#: ../contents/config/config.qml
+msgid "Actions"
+msgstr "Actions"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Tooltip"
+msgstr "Infobulle"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Hover Command:"
+msgstr "Commande au survol :"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Click"
+msgstr "Clic"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Run Command:"
+msgstr "Lancer la commande :"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Mouse Wheel"
+msgstr "Molette de souris"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Scroll Up:"
+msgstr "Défilement vers le haut :"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Scroll Down:"
+msgstr "Défilement vers le bas :"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font"
+msgstr "Police"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font Family:"
+msgstr "Famille :"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font Size:"
+msgstr "Taille :"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "px"
+msgstr "px"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Colors"
+msgstr "Couleurs"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Text:"
+msgstr "Texte :"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Outline:"
+msgstr "Contours :"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Misc"
+msgstr "Divers"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Desktop Widget:"
+msgstr "Pour le widget de bureau :"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Show background"
+msgstr "afficher un fond"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Fixed Width:"
+msgstr "Largeur fixe :"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Fixed Height:"
+msgstr "Hauteur fixe :"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Replace all newlines with spaces"
+msgstr "Remplacer les sauts de lignes par des espaces"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Run every "
+msgstr "Exécuter toutes les "
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "ms"
+msgstr " ms"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Wait for completion"
+msgstr "Attendre la fin de l'éxécution"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Enabled"
+msgstr "Activé"
+
+#: ../contents/ui/libconfig/FontFamily.qml
+msgctxt "Use default font"
+msgid "Default"
+msgstr "Par défaut"
+
+#~ msgid "Version: %1"
+#~ msgstr "Version : %1"
+
+#~ msgid "Desktop Widget: Show background"
+#~ msgstr "Pour le widget de bureau : afficher un fond"
+
+#~ msgid "General"
+#~ msgstr "Général"
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/nl.po b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/nl.po
new file mode 100644
index 0000000..c44df0c
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/nl.po
@@ -0,0 +1,149 @@
+# Translation of commandoutput in nl_NL
+# Copyright (C) 2019
+# This file is distributed under the same license as the commandoutput package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: commandoutput\n"
+"Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-commandoutput\n"
+"POT-Creation-Date: 2024-03-12 16:30-0400\n"
+"PO-Revision-Date: 2024-03-14 10:27+0100\n"
+"Last-Translator: Heimen Stoffels \n"
+"Language-Team: Dutch \n"
+"Language: nl_NL\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 3.4.2\n"
+
+#: ../metadata.json
+msgid "Command Output"
+msgstr "Opdrachtuitvoer"
+
+#: ../metadata.json
+msgid "Run a command periodically and render its output."
+msgstr "Voer een opdracht periodiek uit en toon de uitvoer ervan."
+
+#: ../contents/config/config.qml ../contents/ui/config/ConfigCommand.qml
+msgid "Command"
+msgstr "Opdracht"
+
+#: ../contents/config/config.qml
+msgid "Appearance"
+msgstr "Vormgeving"
+
+#: ../contents/config/config.qml
+msgid "Actions"
+msgstr "Acties"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Tooltip"
+msgstr "Hulpballon"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Hover Command:"
+msgstr "Opdracht na aanwijzen:"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Click"
+msgstr "Klikken"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Run Command:"
+msgstr "Opdracht uitvoeren:"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Mouse Wheel"
+msgstr "Muiswiel"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Scroll Up:"
+msgstr "Omhoog scrollen:"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Scroll Down:"
+msgstr "Omlaag scrollen:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font"
+msgstr "Lettertype"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font Family:"
+msgstr "Lettertype:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font Size:"
+msgstr "Tekstgrootte:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "px"
+msgstr "px"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Colors"
+msgstr "Kleuren"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Text:"
+msgstr "Tekst:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Outline:"
+msgstr "Contour:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Misc"
+msgstr "Overig"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Desktop Widget:"
+msgstr "Bureaubladwidget:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Show background"
+msgstr "Achtergrond tonen"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Fixed Width:"
+msgstr "Vaste breedte:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Fixed Height:"
+msgstr "Vaste hoogte:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Replace all newlines with spaces"
+msgstr "Alle witregels vervangen door spaties"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Run every "
+msgstr "Uitvoeren, elke "
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "ms"
+msgstr "ms"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Wait for completion"
+msgstr "Wachten tot opdracht is afgerond"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Enabled"
+msgstr "Ingeschakeld"
+
+#: ../contents/ui/libconfig/FontFamily.qml
+msgctxt "Use default font"
+msgid "Default"
+msgstr "Standaard"
+
+#~ msgid "Version: %1"
+#~ msgstr "Versie: %1"
+
+#~ msgid "Desktop Widget: Show background"
+#~ msgstr "Bureaubladwidget: achtergrond tonen"
+
+#~ msgid "General"
+#~ msgstr "Algemeen"
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/pt_br.po b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/pt_br.po
new file mode 100644
index 0000000..dfc6e6b
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/pt_br.po
@@ -0,0 +1,166 @@
+# Translation of commandoutput in fr
+# Copyright (C) 2020
+# This file is distributed under the same license as the commandoutput package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: commandoutput\n"
+"Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-commandoutput\n"
+"POT-Creation-Date: 2024-03-12 16:30-0400\n"
+"PO-Revision-Date: 2020-10-05 21:55+020\n"
+"Last-Translator: Ari CALDEIRA \n"
+"Language-Team: \n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../metadata.json
+msgid "Command Output"
+msgstr "Saída de Comandos"
+
+#: ../metadata.json
+msgid "Run a command periodically and render its output."
+msgstr "Executa um comando a cada X milissegundos, e mostra a saída/resultado"
+
+#: ../contents/config/config.qml ../contents/ui/config/ConfigCommand.qml
+msgid "Command"
+msgstr "Comando"
+
+#: ../contents/config/config.qml
+msgid "Appearance"
+msgstr "Aparência"
+
+#: ../contents/config/config.qml
+msgid "Actions"
+msgstr "Ações"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Tooltip"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Hover Command:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Click"
+msgstr "Clique"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Run Command:"
+msgstr "Rodar o comando:"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Mouse Wheel"
+msgstr "Rodinha do mouse"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Scroll Up:"
+msgstr "Rolar para cima:"
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Scroll Down:"
+msgstr "Rolar para baixo:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font"
+msgstr "Fonte"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font Family:"
+msgstr "Família:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font Size:"
+msgstr "Tamanho:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "px"
+msgstr "px"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Colors"
+msgstr "Cores"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Text:"
+msgstr "Texto:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Outline:"
+msgstr "Contorno:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Misc"
+msgstr "Diversos"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Desktop Widget:"
+msgstr "Widget para Desktop:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Show background"
+msgstr "não deixa transparente"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Fixed Width:"
+msgstr "Largura fixa:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Fixed Height:"
+msgstr "Altura fixa:"
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Replace all newlines with spaces"
+msgstr "Junta as linhas numa linha só, separando com espaços"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Run every "
+msgstr "Executar a cada "
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "ms"
+msgstr "ms"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Wait for completion"
+msgstr "Esperar a execução terminar"
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Enabled"
+msgstr ""
+
+#: ../contents/ui/libconfig/FontFamily.qml
+msgctxt "Use default font"
+msgid "Default"
+msgstr "Padrão"
+
+#~ msgid "Version: %1"
+#~ msgstr "Versão: %1"
+
+#~ msgid "Mouse Click"
+#~ msgstr "Clique do mouse"
+
+#~ msgid "Command:"
+#~ msgstr "Comando:"
+
+#~ msgid "pt"
+#~ msgstr "pt"
+
+#~ msgid "Desktop Widget: Show background"
+#~ msgstr "Widget para Desktop: não deixa transparente"
+
+#~ msgid "Tooltip Command"
+#~ msgstr "Comando para a passagem do mouse"
+
+#~ msgid "Run"
+#~ msgstr "Execução"
+
+#~ msgid "Tooltip command"
+#~ msgstr "Comando para a passagem do mouse"
+
+#~ msgid "General"
+#~ msgstr "Geral"
diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/template.pot b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/template.pot
new file mode 100644
index 0000000..a20c88b
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/template.pot
@@ -0,0 +1,139 @@
+# Translation of commandoutput in LANGUAGE
+# Copyright (C) 2024
+# This file is distributed under the same license as the commandoutput package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: commandoutput\n"
+"Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-commandoutput\n"
+"POT-Creation-Date: 2024-03-12 16:30-0400\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../metadata.json
+msgid "Command Output"
+msgstr ""
+
+#: ../metadata.json
+msgid "Run a command periodically and render its output."
+msgstr ""
+
+#: ../contents/config/config.qml ../contents/ui/config/ConfigCommand.qml
+msgid "Command"
+msgstr ""
+
+#: ../contents/config/config.qml
+msgid "Appearance"
+msgstr ""
+
+#: ../contents/config/config.qml
+msgid "Actions"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Tooltip"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Hover Command:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Click"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Run Command:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Mouse Wheel"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Scroll Up:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigActions.qml
+msgid "Scroll Down:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font Family:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Font Size:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "px"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Colors"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Text:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Outline:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Misc"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Desktop Widget:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Show background"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Fixed Width:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Fixed Height:"
+msgstr ""
+
+#: ../contents/ui/config/ConfigAppearance.qml
+msgid "Replace all newlines with spaces"
+msgstr ""
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Run every "
+msgstr ""
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "ms"
+msgstr ""
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Wait for completion"
+msgstr ""
+
+#: ../contents/ui/config/ConfigCommand.qml
+msgid "Enabled"
+msgstr ""
+
+#: ../contents/ui/libconfig/FontFamily.qml
+msgctxt "Use default font"
+msgid "Default"
+msgstr ""
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/code/parser.js b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/code/parser.js
new file mode 100644
index 0000000..0caeead
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/code/parser.js
@@ -0,0 +1,548 @@
+// Parsing and formatting helpers for the CodexBar plasmoid.
+// This file is imported both from QML (import "../code/parser.js" as Parser)
+// and from Node.js unit tests (tests/parser.test.mjs), so it must stay
+// engine-neutral: no QML pragmas, no Node-only APIs.
+
+// Catalog captured from `codexbar config providers` (CodexBar CLI v0.42.1).
+var PROVIDER_CATALOG = [
+ { id: "codex", name: "Codex" },
+ { id: "openai", name: "OpenAI" },
+ { id: "azureopenai", name: "Azure OpenAI" },
+ { id: "claude", name: "Claude" },
+ { id: "cursor", name: "Cursor" },
+ { id: "opencode", name: "OpenCode" },
+ { id: "opencodego", name: "OpenCode Go" },
+ { id: "alibaba", name: "Alibaba" },
+ { id: "alibabatokenplan", name: "Alibaba Token Plan" },
+ { id: "factory", name: "Droid" },
+ { id: "gemini", name: "Gemini" },
+ { id: "antigravity", name: "Antigravity" },
+ { id: "copilot", name: "Copilot" },
+ { id: "devin", name: "Devin" },
+ { id: "zai", name: "z.ai" },
+ { id: "minimax", name: "MiniMax" },
+ { id: "manus", name: "Manus" },
+ { id: "kimi", name: "Kimi" },
+ { id: "kilo", name: "Kilo" },
+ { id: "kiro", name: "Kiro" },
+ { id: "vertexai", name: "Vertex AI" },
+ { id: "augment", name: "Augment" },
+ { id: "jetbrains", name: "JetBrains AI" },
+ { id: "kimik2", name: "Kimi K2 (unofficial)" },
+ { id: "moonshot", name: "Moonshot / Kimi API" },
+ { id: "amp", name: "Amp" },
+ { id: "t3chat", name: "T3 Chat" },
+ { id: "ollama", name: "Ollama" },
+ { id: "synthetic", name: "Synthetic" },
+ { id: "warp", name: "Warp" },
+ { id: "openrouter", name: "OpenRouter" },
+ { id: "elevenlabs", name: "ElevenLabs" },
+ { id: "windsurf", name: "Windsurf" },
+ { id: "zed", name: "Zed" },
+ { id: "perplexity", name: "Perplexity" },
+ { id: "mimo", name: "Xiaomi MiMo" },
+ { id: "doubao", name: "Doubao" },
+ { id: "sakana", name: "Sakana AI" },
+ { id: "abacus", name: "Abacus AI" },
+ { id: "mistral", name: "Mistral" },
+ { id: "deepseek", name: "DeepSeek" },
+ { id: "codebuff", name: "Codebuff" },
+ { id: "crof", name: "Crof" },
+ { id: "venice", name: "Venice" },
+ { id: "commandcode", name: "Command Code" },
+ { id: "qoder", name: "Qoder" },
+ { id: "stepfun", name: "StepFun" },
+ { id: "bedrock", name: "AWS Bedrock" },
+ { id: "grok", name: "Grok" },
+ { id: "groq", name: "Groq" },
+ { id: "llmproxy", name: "LLM Proxy" },
+ { id: "litellm", name: "LiteLLM" },
+ { id: "deepgram", name: "Deepgram" },
+ { id: "poe", name: "Poe" },
+ { id: "chutes", name: "Chutes" },
+ { id: "crossmodel", name: "CrossModel" },
+ { id: "clawrouter", name: "ClawRouter" },
+ { id: "wayfinder", name: "Wayfinder" },
+];
+
+var SHORT_NAMES = {
+ codex: "Cx",
+ openai: "OA",
+ azureopenai: "Az",
+ claude: "Cl",
+ cursor: "Cu",
+ gemini: "Gm",
+ copilot: "Cp",
+ antigravity: "Ag",
+ openrouter: "OR",
+ minimax: "MM",
+ deepseek: "DS",
+ jetbrains: "JB",
+ vertexai: "Vx",
+ bedrock: "BR",
+ litellm: "LL",
+ llmproxy: "LP",
+ groq: "Gq",
+ grok: "Gk",
+};
+
+function providerDisplayName(id) {
+ for (var i = 0; i < PROVIDER_CATALOG.length; i++) {
+ if (PROVIDER_CATALOG[i].id === id) {
+ return PROVIDER_CATALOG[i].name;
+ }
+ }
+ return id;
+}
+
+function shortName(id) {
+ if (SHORT_NAMES[id]) {
+ return SHORT_NAMES[id];
+ }
+ if (!id || id.length === 0) {
+ return "?";
+ }
+ var base = id.slice(0, 2);
+ return base.charAt(0).toUpperCase() + base.slice(1).toLowerCase();
+}
+
+function windowLabel(windowMinutes) {
+ if (windowMinutes === null || windowMinutes === undefined) {
+ return "Usage";
+ }
+ if (windowMinutes === 10080) {
+ return "Weekly";
+ }
+ if (windowMinutes === 43200) {
+ return "Monthly";
+ }
+ if (windowMinutes === 1440) {
+ return "Daily";
+ }
+ if (windowMinutes >= 180 && windowMinutes <= 360) {
+ return "Session";
+ }
+ if (windowMinutes % 1440 === 0) {
+ return (windowMinutes / 1440) + "d";
+ }
+ if (windowMinutes % 60 === 0) {
+ return (windowMinutes / 60) + "h";
+ }
+ return windowMinutes + "m";
+}
+
+function usageStage(usedPercent) {
+ if (usedPercent >= 90) {
+ return "crit";
+ }
+ if (usedPercent >= 70) {
+ return "warn";
+ }
+ return "ok";
+}
+
+function formatCountdown(resetsAtIso, nowMs) {
+ if (!resetsAtIso) {
+ return "";
+ }
+ var target = Date.parse(resetsAtIso);
+ if (isNaN(target)) {
+ return "";
+ }
+ var diffMinutes = Math.floor((target - nowMs) / 60000);
+ if (diffMinutes <= 0) {
+ return "now";
+ }
+ var days = Math.floor(diffMinutes / 1440);
+ var hours = Math.floor((diffMinutes % 1440) / 60);
+ var minutes = diffMinutes % 60;
+ if (days > 0) {
+ return days + "d " + hours + "h";
+ }
+ if (hours > 0) {
+ return hours + "h " + minutes + "m";
+ }
+ return minutes + "m";
+}
+
+function makeWindow(key, raw, pace, labelOverride, usageKnown) {
+ var paceSummary = "";
+ if (pace && pace[key] && pace[key].summary) {
+ paceSummary = pace[key].summary;
+ }
+ var minutes = raw.windowMinutes === undefined ? null : raw.windowMinutes;
+ return {
+ key: key,
+ label: labelOverride || windowLabel(minutes),
+ windowMinutes: minutes,
+ usedPercent: Math.round(raw.usedPercent || 0),
+ resetsAt: raw.resetsAt || null,
+ resetDescription: raw.resetDescription || "",
+ paceSummary: paceSummary,
+ usageKnown: usageKnown === undefined ? true : usageKnown,
+ };
+}
+
+function payloadToModel(payload) {
+ var usage = payload.usage || null;
+ var windows = [];
+ if (usage) {
+ var keys = ["primary", "secondary", "tertiary"];
+ for (var i = 0; i < keys.length; i++) {
+ var raw = usage[keys[i]];
+ if (!raw || raw.isSyntheticPlaceholder) {
+ continue;
+ }
+ windows.push(makeWindow(keys[i], raw, payload.pace || null));
+ }
+ // Named model-specific lanes (e.g. Codex Spark, Code review).
+ var extras = usage.extraRateWindows || [];
+ for (var j = 0; j < extras.length; j++) {
+ var extra = extras[j];
+ if (!extra || !extra.window || extra.window.isSyntheticPlaceholder) {
+ continue;
+ }
+ windows.push(makeWindow(
+ "extra:" + (extra.id || j),
+ extra.window,
+ null,
+ extra.title || extra.id || "Extra",
+ extra.usageKnown === undefined ? true : extra.usageKnown));
+ }
+ }
+ // OpenAI web dashboard exposes code review as a bare remaining percent.
+ var dashboard = payload.openaiDashboard || null;
+ if (dashboard && typeof dashboard.codeReviewRemainingPercent === "number") {
+ var hasReviewLane = windows.some(function(w) {
+ return w.label.toLowerCase().indexOf("code review") !== -1;
+ });
+ if (!hasReviewLane) {
+ windows.push(makeWindow(
+ "extra:code-review",
+ { usedPercent: 100 - dashboard.codeReviewRemainingPercent, windowMinutes: null, resetsAt: null },
+ null,
+ "Code review",
+ true));
+ }
+ }
+ var account = null;
+ if (usage && usage.identity && usage.identity.accountEmail) {
+ account = usage.identity.accountEmail;
+ } else if (usage && usage.accountEmail) {
+ account = usage.accountEmail;
+ } else if (payload.account) {
+ account = payload.account;
+ }
+ var plan = null;
+ if (usage && usage.identity && usage.identity.loginMethod) {
+ plan = usage.identity.loginMethod;
+ } else if (usage && usage.loginMethod) {
+ plan = usage.loginMethod;
+ }
+ var credits = null;
+ if (payload.credits && typeof payload.credits.remaining === "number") {
+ credits = payload.credits.remaining;
+ }
+ var status = null;
+ if (payload.status && payload.status.indicator && payload.status.indicator !== "none") {
+ status = {
+ indicator: payload.status.indicator,
+ description: payload.status.description || "",
+ };
+ }
+ return {
+ id: payload.provider || "?",
+ name: providerDisplayName(payload.provider || "?"),
+ source: payload.source || "",
+ version: payload.version || "",
+ account: account,
+ plan: plan,
+ credits: credits,
+ status: status,
+ error: payload.error ? String(payload.error.message || "error") : null,
+ windows: windows,
+ updatedAt: usage && usage.updatedAt ? usage.updatedAt : null,
+ };
+}
+
+function parseUsageJson(text) {
+ var parsed;
+ try {
+ parsed = JSON.parse(text);
+ } catch (e) {
+ return { ok: false, error: "Invalid JSON from codexbar: " + e.message, models: [] };
+ }
+ if (!Array.isArray(parsed)) {
+ return { ok: false, error: "Unexpected codexbar payload (expected array)", models: [] };
+ }
+ var models = [];
+ for (var i = 0; i < parsed.length; i++) {
+ models.push(payloadToModel(parsed[i]));
+ }
+ return { ok: true, error: "", models: models };
+}
+
+function worstUsedPercent(model) {
+ var worst = -1;
+ for (var i = 0; i < model.windows.length; i++) {
+ if (model.windows[i].usedPercent > worst) {
+ worst = model.windows[i].usedPercent;
+ }
+ }
+ return worst;
+}
+
+// Ring assignment for the radial gauge: outer ring shows the short
+// (session/daily) window, inner ring the long (weekly/monthly) window.
+function gaugeRings(model) {
+ var outerIdx = -1;
+ var innerIdx = -1;
+ var firstKnownIdx = -1;
+ for (var i = 0; i < model.windows.length; i++) {
+ if (model.windows[i].usageKnown === false) {
+ continue;
+ }
+ if (firstKnownIdx === -1) {
+ firstKnownIdx = i;
+ }
+ var minutes = model.windows[i].windowMinutes;
+ if (minutes !== null && minutes !== undefined && minutes <= 1440 && outerIdx === -1) {
+ outerIdx = i;
+ } else if (minutes !== null && minutes !== undefined && minutes >= 10080 && innerIdx === -1) {
+ innerIdx = i;
+ }
+ }
+ if (outerIdx === -1 && innerIdx === -1 && firstKnownIdx !== -1) {
+ outerIdx = firstKnownIdx;
+ }
+ return { outerIdx: outerIdx, innerIdx: innerIdx };
+}
+
+function gaugeCenterPercent(model) {
+ var rings = gaugeRings(model);
+ var idx = rings.outerIdx !== -1 ? rings.outerIdx : rings.innerIdx;
+ if (idx === -1) {
+ return -1;
+ }
+ return Math.max(0, Math.min(100, model.windows[idx].usedPercent));
+}
+
+function sweepAngle(percent) {
+ var clamped = Math.max(0, Math.min(100, percent));
+ return clamped * 3.6;
+}
+
+// Local token-cost scan payloads from `codexbar cost --format json`.
+function parseCostJson(text) {
+ var parsed;
+ try {
+ parsed = JSON.parse(text);
+ } catch (e) {
+ return { ok: false, error: "Invalid cost JSON: " + e.message, byId: {} };
+ }
+ if (!Array.isArray(parsed)) {
+ return { ok: false, error: "Unexpected cost payload (expected array)", byId: {} };
+ }
+ var byId = {};
+ for (var i = 0; i < parsed.length; i++) {
+ var p = parsed[i];
+ if (!p || !p.provider) {
+ continue;
+ }
+ var daily = [];
+ var rawDaily = p.daily || [];
+ for (var j = 0; j < rawDaily.length; j++) {
+ daily.push({
+ date: rawDaily[j].date || "",
+ totalCost: Number(rawDaily[j].totalCost) || 0,
+ totalTokens: Number(rawDaily[j].totalTokens) || 0,
+ });
+ }
+ byId[p.provider] = {
+ todayCostUSD: typeof p.sessionCostUSD === "number" ? p.sessionCostUSD : null,
+ todayTokens: typeof p.sessionTokens === "number" ? p.sessionTokens : null,
+ month30CostUSD: typeof p.last30DaysCostUSD === "number" ? p.last30DaysCostUSD : null,
+ month30Tokens: typeof p.last30DaysTokens === "number" ? p.last30DaysTokens : null,
+ daily: daily,
+ };
+ }
+ return { ok: true, error: "", byId: byId };
+}
+
+function humanTokens(value) {
+ if (value === null || value === undefined || !isFinite(value)) {
+ return "";
+ }
+ var n = Number(value);
+ if (n < 1000) {
+ return String(Math.round(n));
+ }
+ if (n < 1000000) {
+ return (Math.round(n / 100) / 10) + "K";
+ }
+ if (n < 1000000000) {
+ return (Math.round(n / 100000) / 10) + "M";
+ }
+ return (Math.round(n / 100000000) / 10) + "B";
+}
+
+function formatMoney(value) {
+ if (value === null || value === undefined || !isFinite(value)) {
+ return "";
+ }
+ return "$" + Number(value).toFixed(2);
+}
+
+// Zero-filled per-day cost series for the history chart, ending at todayIso.
+function chartSeries(daily, days, todayIso) {
+ var byDate = {};
+ for (var i = 0; i < (daily || []).length; i++) {
+ byDate[daily[i].date] = daily[i];
+ }
+ var series = [];
+ var today = new Date(todayIso + "T00:00:00Z");
+ for (var d = days - 1; d >= 0; d--) {
+ var day = new Date(today.getTime() - d * 86400000);
+ var iso = day.toISOString().slice(0, 10);
+ var entry = byDate[iso];
+ series.push({
+ date: iso,
+ value: entry ? entry.totalCost : 0,
+ tokens: entry ? entry.totalTokens : 0,
+ });
+ }
+ return series;
+}
+
+function shellQuote(value) {
+ return "'" + String(value).replace(/'/g, "'\"'\"'") + "'";
+}
+
+function commandPrefix(proxyUrl) {
+ // ~/.local/bin is not always on plasmashell's PATH.
+ var prefix = 'env PATH="$HOME/.local/bin:$PATH"';
+ if (proxyUrl && proxyUrl.trim().length > 0) {
+ var quoted = shellQuote(proxyUrl.trim());
+ prefix += " http_proxy=" + quoted + " https_proxy=" + quoted
+ + " HTTP_PROXY=" + quoted + " HTTPS_PROXY=" + quoted
+ + " ALL_PROXY=" + quoted;
+ }
+ return prefix;
+}
+
+// One usage command per selected provider; a single unscoped command when the
+// selection is empty (the CLI then reports the providers enabled in its own
+// config file).
+function buildCommands(codexbarPath, selected, proxyUrl) {
+ var base = commandPrefix(proxyUrl) + " " + codexbarPath + " usage --format json --no-color";
+ if (!selected || selected.length === 0) {
+ return [base];
+ }
+ return selected.map(function(id) {
+ return base + " --provider " + id;
+ });
+}
+
+function buildCostCommand(codexbarPath, proxyUrl) {
+ return commandPrefix(proxyUrl) + " " + codexbarPath + " cost --format json --no-color";
+}
+
+// Merge freshly parsed provider models into the cached map. Successful
+// fetches replace the cache entry; errors keep the last good data and only
+// mark it stale, so panel gauges never blank out during background refreshes.
+function applyUpdate(prevById, models, nowMs) {
+ var byId = {};
+ var key;
+ for (key in prevById) {
+ byId[key] = prevById[key];
+ }
+ for (var i = 0; i < models.length; i++) {
+ var model = models[i];
+ var cached = byId[model.id];
+ if (model.error && cached && cached.windows && cached.windows.length > 0) {
+ var stale = {};
+ for (key in cached) {
+ stale[key] = cached[key];
+ }
+ stale.staleError = model.error;
+ byId[model.id] = stale;
+ continue;
+ }
+ model.fetchedAtMs = nowMs;
+ model.staleError = null;
+ byId[model.id] = model;
+ }
+ return { byId: byId };
+}
+
+function formatAgo(thenMs, nowMs) {
+ if (thenMs === null || thenMs === undefined || thenMs < 0) {
+ return "";
+ }
+ var minutes = Math.floor((nowMs - thenMs) / 60000);
+ if (minutes < 1) {
+ return "just now";
+ }
+ if (minutes < 60) {
+ return minutes + "m ago";
+ }
+ var hours = Math.floor(minutes / 60);
+ if (hours < 24) {
+ return hours + "h " + (minutes % 60) + "m ago";
+ }
+ var days = Math.floor(hours / 24);
+ return days + "d " + (hours % 24) + "h ago";
+}
+
+function selectionList(csv) {
+ if (!csv) {
+ return [];
+ }
+ var out = [];
+ var parts = csv.split(",");
+ for (var i = 0; i < parts.length; i++) {
+ var id = parts[i].trim();
+ if (id.length > 0) {
+ out.push(id);
+ }
+ }
+ return out;
+}
+
+function toggleSelection(csv, id, checked) {
+ var list = selectionList(csv);
+ var index = list.indexOf(id);
+ if (checked && index === -1) {
+ list.push(id);
+ } else if (!checked && index !== -1) {
+ list.splice(index, 1);
+ }
+ return list.join(",");
+}
+
+// Node.js test hook; ignored by the QML JS engine.
+if (typeof module !== "undefined" && module.exports) {
+ module.exports = {
+ PROVIDER_CATALOG: PROVIDER_CATALOG,
+ providerDisplayName: providerDisplayName,
+ shortName: shortName,
+ windowLabel: windowLabel,
+ usageStage: usageStage,
+ formatCountdown: formatCountdown,
+ parseUsageJson: parseUsageJson,
+ worstUsedPercent: worstUsedPercent,
+ selectionList: selectionList,
+ toggleSelection: toggleSelection,
+ gaugeRings: gaugeRings,
+ gaugeCenterPercent: gaugeCenterPercent,
+ sweepAngle: sweepAngle,
+ applyUpdate: applyUpdate,
+ formatAgo: formatAgo,
+ parseCostJson: parseCostJson,
+ humanTokens: humanTokens,
+ formatMoney: formatMoney,
+ chartSeries: chartSeries,
+ buildCommands: buildCommands,
+ buildCostCommand: buildCostCommand,
+ };
+}
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/config.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/config.qml
new file mode 100644
index 0000000..11e1dc9
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/config.qml
@@ -0,0 +1,15 @@
+import QtQuick
+import org.kde.plasma.configuration
+
+ConfigModel {
+ ConfigCategory {
+ name: i18n("General")
+ icon: "configure"
+ source: "ConfigGeneral.qml"
+ }
+ ConfigCategory {
+ name: i18n("Providers")
+ icon: "applications-development"
+ source: "ConfigProviders.qml"
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/main.xml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/main.xml
new file mode 100644
index 0000000..7a4032c
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/main.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+ codexbar
+
+
+
+ 5
+
+
+
+ true
+
+
+
+ true
+
+
+
+ true
+
+
+
+
+
+
+
+ codex,claude
+
+
+
+
+
+
+
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/icons/codexbar.svg b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/icons/codexbar.svg
new file mode 100644
index 0000000..e4ebf74
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/icons/codexbar.svg
@@ -0,0 +1,15 @@
+
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/CompactRepresentation.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/CompactRepresentation.qml
new file mode 100644
index 0000000..4c8b19f
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/CompactRepresentation.qml
@@ -0,0 +1,65 @@
+import QtQuick
+import QtQuick.Layouts
+import org.kde.plasma.plasmoid
+import org.kde.plasma.core as PlasmaCore
+import org.kde.kirigami as Kirigami
+import "../code/parser.js" as Parser
+
+// Panel view: one two-ring radial gauge per provider
+// (outer ring = session left, inner ring = weekly left).
+Item {
+ id: compact
+
+ readonly property bool vertical: Plasmoid.formFactor === PlasmaCore.Types.Vertical
+ readonly property int gaugeSize: compact.vertical
+ ? Math.min(compact.width, Kirigami.Units.gridUnit * 2.4)
+ : Math.min(compact.height, Kirigami.Units.gridUnit * 2.4)
+ readonly property int count: Math.max(1, root.models.length)
+
+ Layout.minimumWidth: compact.vertical ? compact.gaugeSize : compact.count * (compact.gaugeSize + gaugeGrid.spacing)
+ Layout.minimumHeight: compact.vertical ? compact.count * (compact.gaugeSize + gaugeGrid.spacing) : compact.gaugeSize
+
+ Grid {
+ id: gaugeGrid
+ anchors.centerIn: parent
+ columns: compact.vertical ? 1 : compact.count
+ spacing: Kirigami.Units.smallSpacing
+
+ Kirigami.Icon {
+ visible: root.models.length === 0
+ source: Qt.resolvedUrl("../icons/codexbar.svg")
+ width: compact.gaugeSize
+ height: compact.gaugeSize
+ opacity: root.loading ? 0.5 : 1
+ }
+
+ Repeater {
+ model: root.models
+
+ delegate: RadialGauge {
+ id: meter
+ required property var modelData
+ readonly property var rings: Parser.gaugeRings(modelData)
+ readonly property int centerPercent: Parser.gaugeCenterPercent(modelData)
+
+ width: compact.gaugeSize
+ height: compact.gaugeSize
+ outerColor: root.sessionColor
+ innerColor: root.weeklyColor
+ outerPercent: rings.outerIdx >= 0
+ ? modelData.windows[rings.outerIdx].usedPercent : -1
+ innerPercent: rings.innerIdx >= 0
+ ? modelData.windows[rings.innerIdx].usedPercent : -1
+ centerText: modelData.error
+ ? "!"
+ : (meter.centerPercent >= 0 ? String(meter.centerPercent) : "")
+ centerTextScale: 0.34
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: root.expanded = !root.expanded
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigGeneral.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigGeneral.qml
new file mode 100644
index 0000000..802d2cb
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigGeneral.qml
@@ -0,0 +1,70 @@
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+Kirigami.FormLayout {
+ id: page
+
+ property alias cfg_codexbarPath: pathField.text
+ property alias cfg_refreshMinutes: refreshSpin.value
+ property alias cfg_showPace: paceCheck.checked
+ property alias cfg_showCost: costCheck.checked
+ property alias cfg_showHistory: historyCheck.checked
+ property alias cfg_proxyUrl: proxyField.text
+
+ QQC2.TextField {
+ id: pathField
+ Kirigami.FormData.label: i18n("codexbar binary:")
+ placeholderText: "codexbar"
+ Layout.fillWidth: true
+ }
+
+ QQC2.Label {
+ text: i18n("Plain name is resolved via PATH (including ~/.local/bin); an absolute path also works.")
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ opacity: 0.6
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+
+ QQC2.SpinBox {
+ id: refreshSpin
+ Kirigami.FormData.label: i18n("Refresh every:")
+ from: 1
+ to: 120
+ textFromValue: function(value) { return i18np("%1 minute", "%1 minutes", value) }
+ valueFromText: function(text) { return parseInt(text) || 5 }
+ }
+
+ QQC2.CheckBox {
+ id: paceCheck
+ Kirigami.FormData.label: i18n("Details:")
+ text: i18n("Show pace summaries")
+ }
+
+ QQC2.CheckBox {
+ id: costCheck
+ text: i18n("Show local token costs (Codex/Claude)")
+ }
+
+ QQC2.CheckBox {
+ id: historyCheck
+ text: i18n("Show daily spend history chart")
+ }
+
+ QQC2.TextField {
+ id: proxyField
+ Kirigami.FormData.label: i18n("Proxy:")
+ placeholderText: "socks5://127.0.0.1:1080"
+ Layout.fillWidth: true
+ }
+
+ QQC2.Label {
+ text: i18n("Exported as http_proxy/https_proxy/ALL_PROXY for every CLI call. Useful when provider APIs are blocked in your region. Leave empty for a direct connection.")
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ opacity: 0.6
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigProviders.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigProviders.qml
new file mode 100644
index 0000000..73bdce0
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigProviders.qml
@@ -0,0 +1,70 @@
+import QtQuick
+import QtQuick.Controls as QQC2
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+import "../code/parser.js" as Parser
+
+ColumnLayout {
+ id: page
+
+ property string cfg_selectedProviders: ""
+
+ readonly property var selectedList: Parser.selectionList(page.cfg_selectedProviders)
+
+ function isSelected(id) {
+ return page.selectedList.indexOf(id) !== -1
+ }
+
+ function toggle(id, checked) {
+ page.cfg_selectedProviders = Parser.toggleSelection(page.cfg_selectedProviders, id, checked)
+ }
+
+ QQC2.Label {
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ text: i18n("Pick the coding agents to display. With nothing selected, the widget shows the providers enabled in the CodexBar CLI config (~/.config/codexbar/config.json).")
+ }
+
+ Kirigami.SearchField {
+ id: searchField
+ Layout.fillWidth: true
+ placeholderText: i18n("Filter providers…")
+ }
+
+ QQC2.ScrollView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ListView {
+ id: providerListView
+ clip: true
+ model: {
+ var query = searchField.text.toLowerCase().trim()
+ if (query.length === 0) {
+ return Parser.PROVIDER_CATALOG
+ }
+ return Parser.PROVIDER_CATALOG.filter(function(p) {
+ return p.id.indexOf(query) !== -1
+ || p.name.toLowerCase().indexOf(query) !== -1
+ })
+ }
+
+ delegate: QQC2.CheckDelegate {
+ required property var modelData
+ width: providerListView.width
+ text: modelData.name + " (" + modelData.id + ")"
+ checked: page.isSelected(modelData.id)
+ onToggled: page.toggle(modelData.id, checked)
+ }
+ }
+ }
+
+ QQC2.Label {
+ Layout.fillWidth: true
+ text: page.selectedList.length > 0
+ ? i18np("%1 provider selected", "%1 providers selected", page.selectedList.length)
+ : i18n("Following the CLI config")
+ opacity: 0.6
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/FullRepresentation.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/FullRepresentation.qml
new file mode 100644
index 0000000..f23d2c6
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/FullRepresentation.qml
@@ -0,0 +1,372 @@
+import QtQuick
+import QtQuick.Layouts
+import org.kde.plasma.plasmoid
+import org.kde.plasma.components as PlasmaComponents3
+import org.kde.plasma.extras as PlasmaExtras
+import org.kde.kirigami as Kirigami
+import "../code/parser.js" as Parser
+
+PlasmaExtras.Representation {
+ id: full
+
+ Layout.preferredWidth: Kirigami.Units.gridUnit * 22
+ Layout.preferredHeight: Kirigami.Units.gridUnit * 26
+ Layout.minimumWidth: Kirigami.Units.gridUnit * 16
+ Layout.minimumHeight: Kirigami.Units.gridUnit * 10
+
+ collapseMarginsHint: true
+
+ function stageColor(usedPercent) {
+ var stage = Parser.usageStage(usedPercent)
+ if (stage === "crit") {
+ return Kirigami.Theme.negativeTextColor
+ }
+ if (stage === "warn") {
+ return Kirigami.Theme.neutralTextColor
+ }
+ return Kirigami.Theme.positiveTextColor
+ }
+
+ function resetText(w) {
+ var countdown = Parser.formatCountdown(w.resetsAt, root.nowMs)
+ if (countdown === "now") {
+ return i18n("resets soon")
+ }
+ if (countdown.length > 0) {
+ return i18n("resets in %1", countdown)
+ }
+ return w.resetDescription
+ }
+
+ function costLine(cost, kind) {
+ var money = Parser.formatMoney(kind === "today" ? cost.todayCostUSD : cost.month30CostUSD)
+ var tokens = Parser.humanTokens(kind === "today" ? cost.todayTokens : cost.month30Tokens)
+ if (money.length === 0 && tokens.length === 0) {
+ return ""
+ }
+ return money + (tokens.length > 0 ? " · " + tokens + " tok" : "")
+ }
+
+ header: PlasmaExtras.PlasmoidHeading {
+ RowLayout {
+ anchors.fill: parent
+ spacing: Kirigami.Units.smallSpacing
+
+ Kirigami.Heading {
+ Layout.fillWidth: true
+ Layout.leftMargin: Kirigami.Units.smallSpacing
+ level: 2
+ text: i18n("CodexBar")
+ }
+
+ PlasmaComponents3.Label {
+ visible: root.lastUpdatedMs > 0
+ text: i18n("updated %1", Parser.formatAgo(root.lastUpdatedMs, root.nowMs))
+ opacity: 0.6
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ }
+
+ PlasmaComponents3.ToolButton {
+ id: refreshButton
+ icon.name: "view-refresh"
+ enabled: !root.loading
+ onClicked: root.refresh()
+ PlasmaComponents3.ToolTip { text: i18n("Refresh") }
+
+ rotation: 0
+ RotationAnimation on rotation {
+ running: root.loading
+ from: 0
+ to: 360
+ duration: 1100
+ loops: Animation.Infinite
+ }
+ onEnabledChanged: if (enabled) { rotation = 0 }
+ }
+ }
+ }
+
+ contentItem: ListView {
+ id: providerList
+ clip: true
+ spacing: Kirigami.Units.smallSpacing
+ model: root.models
+ topMargin: Kirigami.Units.smallSpacing
+ bottomMargin: Kirigami.Units.smallSpacing
+ leftMargin: Kirigami.Units.smallSpacing
+ rightMargin: Kirigami.Units.smallSpacing
+
+ PlasmaExtras.PlaceholderMessage {
+ anchors.centerIn: parent
+ width: parent.width - Kirigami.Units.gridUnit * 2
+ visible: root.models.length === 0
+ iconName: root.globalError.length > 0 ? "data-warning" : "office-chart-bar"
+ text: {
+ if (root.globalError.length > 0) {
+ return root.globalError
+ }
+ if (root.loading) {
+ return i18n("Fetching usage…")
+ }
+ return i18n("No providers configured. Pick providers in the widget settings or enable them with “codexbar config enable --provider ”.")
+ }
+ }
+
+ delegate: Item {
+ id: card
+ required property var modelData
+ readonly property var rings: Parser.gaugeRings(modelData)
+ readonly property int centerPercent: Parser.gaugeCenterPercent(modelData)
+ readonly property var cost: root.showCost ? (root.costById[modelData.id] || null) : null
+
+ width: providerList.width - providerList.leftMargin - providerList.rightMargin
+ height: cardContent.implicitHeight + Kirigami.Units.largeSpacing * 2
+
+ function ringColor(index) {
+ if (index === card.rings.outerIdx) {
+ return root.sessionColor
+ }
+ if (index === card.rings.innerIdx) {
+ return root.weeklyColor
+ }
+ return Qt.alpha(Kirigami.Theme.textColor, 0.4)
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Kirigami.Units.cornerRadius !== undefined ? Kirigami.Units.cornerRadius : 5
+ color: Qt.alpha(Kirigami.Theme.textColor, 0.06)
+ }
+
+ RowLayout {
+ id: cardContent
+ anchors.fill: parent
+ anchors.margins: Kirigami.Units.largeSpacing
+ spacing: Kirigami.Units.largeSpacing
+
+ RadialGauge {
+ Layout.preferredWidth: Kirigami.Units.gridUnit * 4.5
+ Layout.preferredHeight: Kirigami.Units.gridUnit * 4.5
+ Layout.alignment: Qt.AlignTop
+ outerColor: root.sessionColor
+ innerColor: root.weeklyColor
+ outerPercent: card.rings.outerIdx >= 0
+ ? card.modelData.windows[card.rings.outerIdx].usedPercent : -1
+ innerPercent: card.rings.innerIdx >= 0
+ ? card.modelData.windows[card.rings.innerIdx].usedPercent : -1
+ centerText: card.modelData.error
+ ? "!"
+ : (card.centerPercent >= 0 ? card.centerPercent + "%" : "")
+ centerTextScale: 0.2
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Kirigami.Units.smallSpacing
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Kirigami.Units.smallSpacing
+
+ Kirigami.Heading {
+ level: 4
+ text: card.modelData.name
+ }
+
+ Rectangle {
+ visible: card.modelData.status !== null
+ width: Kirigami.Units.smallSpacing * 2
+ height: width
+ radius: width / 2
+ color: Kirigami.Theme.negativeTextColor
+
+ PlasmaComponents3.ToolTip {
+ text: card.modelData.status
+ ? (card.modelData.status.indicator + ": " + card.modelData.status.description)
+ : ""
+ }
+ }
+
+ Item { Layout.fillWidth: true }
+
+ PlasmaComponents3.Label {
+ visible: !!card.modelData.plan
+ text: card.modelData.plan || ""
+ opacity: 0.6
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ }
+ }
+
+ PlasmaComponents3.Label {
+ visible: !!card.modelData.account
+ Layout.fillWidth: true
+ text: card.modelData.account || ""
+ elide: Text.ElideMiddle
+ opacity: 0.6
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ }
+
+ PlasmaComponents3.Label {
+ visible: !!card.modelData.error
+ Layout.fillWidth: true
+ text: card.modelData.error || ""
+ wrapMode: Text.WordWrap
+ color: Kirigami.Theme.negativeTextColor
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ }
+
+ PlasmaComponents3.Label {
+ visible: !!card.modelData.staleError
+ Layout.fillWidth: true
+ text: i18n("Refresh failed, showing data from %1: %2",
+ Parser.formatAgo(card.modelData.fetchedAtMs !== undefined ? card.modelData.fetchedAtMs : -1, root.nowMs),
+ card.modelData.staleError || "")
+ wrapMode: Text.WordWrap
+ color: Kirigami.Theme.neutralTextColor
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ }
+
+ Repeater {
+ model: card.modelData.windows
+
+ delegate: ColumnLayout {
+ id: windowRow
+ required property var modelData
+ required property int index
+ Layout.fillWidth: true
+ spacing: 0
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Kirigami.Units.smallSpacing
+
+ Rectangle {
+ width: Kirigami.Units.smallSpacing * 2
+ height: width
+ radius: width / 2
+ color: card.ringColor(windowRow.index)
+ }
+
+ PlasmaComponents3.Label {
+ text: windowRow.modelData.label
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ }
+
+ Item { Layout.fillWidth: true }
+
+ PlasmaComponents3.Label {
+ text: windowRow.modelData.usageKnown === false
+ ? i18n("usage n/a")
+ : i18n("%1% used", windowRow.modelData.usedPercent)
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ color: windowRow.modelData.usageKnown === false
+ ? Kirigami.Theme.disabledTextColor
+ : full.stageColor(windowRow.modelData.usedPercent)
+ }
+
+ PlasmaComponents3.Label {
+ text: "· " + full.resetText(windowRow.modelData)
+ visible: full.resetText(windowRow.modelData).length > 0
+ opacity: 0.6
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ }
+ }
+
+ PlasmaComponents3.Label {
+ visible: root.showPace && windowRow.modelData.paceSummary.length > 0
+ Layout.fillWidth: true
+ Layout.leftMargin: Kirigami.Units.smallSpacing * 3
+ text: windowRow.modelData.paceSummary
+ elide: Text.ElideRight
+ opacity: 0.5
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ }
+ }
+ }
+
+ PlasmaComponents3.Label {
+ visible: card.modelData.credits !== null && card.modelData.credits > 0
+ text: i18n("Credits: %1", card.modelData.credits !== null
+ ? Number(card.modelData.credits).toLocaleString(Qt.locale(), "f", 1)
+ : "")
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ opacity: 0.7
+ }
+
+ // Local token cost scan (Codex/Claude): spend and tokens.
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.topMargin: Kirigami.Units.smallSpacing
+ visible: card.cost !== null
+ spacing: Kirigami.Units.largeSpacing
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ PlasmaComponents3.Label {
+ text: i18n("Today")
+ opacity: 0.6
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ }
+
+ PlasmaComponents3.Label {
+ text: card.cost ? full.costLine(card.cost, "today") : ""
+ font.bold: true
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ PlasmaComponents3.Label {
+ text: i18n("Last 30 days")
+ opacity: 0.6
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ horizontalAlignment: Text.AlignRight
+ Layout.fillWidth: true
+ }
+
+ PlasmaComponents3.Label {
+ text: card.cost ? full.costLine(card.cost, "month30") : ""
+ font.bold: true
+ horizontalAlignment: Text.AlignRight
+ Layout.fillWidth: true
+ }
+ }
+ }
+
+ HistoryChart {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Kirigami.Units.gridUnit * 2.2
+ visible: root.showHistory && card.cost !== null && card.cost.daily.length > 0
+ accentColor: root.sessionColor
+ series: card.cost !== null
+ ? Parser.chartSeries(card.cost.daily, 14, new Date(root.nowMs).toISOString().slice(0, 10))
+ : []
+ }
+
+ PlasmaComponents3.Label {
+ Layout.fillWidth: true
+ visible: text.length > 0
+ text: {
+ var parts = []
+ if (card.modelData.source) {
+ parts.push(i18n("Source: %1", card.modelData.source))
+ }
+ if (card.modelData.version) {
+ parts.push(i18n("Version: %1", card.modelData.version))
+ }
+ return parts.join(" · ")
+ }
+ opacity: 0.45
+ font.pointSize: Kirigami.Theme.smallFont.pointSize
+ horizontalAlignment: Text.AlignRight
+ elide: Text.ElideLeft
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/HistoryChart.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/HistoryChart.qml
new file mode 100644
index 0000000..315be71
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/HistoryChart.qml
@@ -0,0 +1,49 @@
+import QtQuick
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+// Rounded per-day spend bars for the last N days (zero days render as dots).
+Item {
+ id: chart
+
+ property var series: []
+ property color accentColor: Kirigami.Theme.highlightColor
+
+ readonly property real maxValue: {
+ var max = 0
+ for (var i = 0; i < chart.series.length; i++) {
+ if (chart.series[i].value > max) {
+ max = chart.series[i].value
+ }
+ }
+ return max
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 2
+
+ Repeater {
+ model: chart.series
+
+ delegate: Item {
+ required property var modelData
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: chart.maxValue > 0
+ ? Math.max(3, parent.height * modelData.value / chart.maxValue)
+ : 3
+ radius: Math.min(width / 2, 4)
+ color: chart.accentColor
+ opacity: modelData.value > 0 ? 0.75 : 0.2
+ }
+ }
+ }
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/RadialGauge.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/RadialGauge.qml
new file mode 100644
index 0000000..8073d43
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/RadialGauge.qml
@@ -0,0 +1,101 @@
+import QtQuick
+import QtQuick.Shapes
+import org.kde.kirigami as Kirigami
+import "../code/parser.js" as Parser
+
+// Two-ring radial gauge: outer ring = session window, inner ring = weekly
+// window. A percent of -1 leaves the ring as a bare track.
+Item {
+ id: gauge
+
+ property real outerPercent: -1
+ property real innerPercent: -1
+ property color outerColor: Kirigami.Theme.highlightColor
+ property color innerColor: "#f67400"
+ property string centerText: ""
+ property real centerTextScale: 0.32
+
+ readonly property real ringWidth: Math.max(2, Math.min(width, height) / 15)
+ readonly property real ringGap: ringWidth * 1.2
+ readonly property real outerRadius: (Math.min(width, height) - ringWidth) / 2
+ readonly property real innerRadius: outerRadius - ringWidth - ringGap
+ readonly property color trackColor: Qt.alpha(Kirigami.Theme.textColor, 0.18)
+
+ Shape {
+ anchors.fill: parent
+ preferredRendererType: Shape.CurveRenderer
+
+ // Outer track.
+ ShapePath {
+ strokeColor: gauge.trackColor
+ fillColor: "transparent"
+ strokeWidth: gauge.ringWidth
+ capStyle: ShapePath.FlatCap
+ PathAngleArc {
+ centerX: gauge.width / 2
+ centerY: gauge.height / 2
+ radiusX: gauge.outerRadius
+ radiusY: gauge.outerRadius
+ startAngle: 0
+ sweepAngle: 360
+ }
+ }
+
+ // Inner track.
+ ShapePath {
+ strokeColor: gauge.trackColor
+ fillColor: "transparent"
+ strokeWidth: gauge.ringWidth
+ capStyle: ShapePath.FlatCap
+ PathAngleArc {
+ centerX: gauge.width / 2
+ centerY: gauge.height / 2
+ radiusX: gauge.innerRadius
+ radiusY: gauge.innerRadius
+ startAngle: 0
+ sweepAngle: 360
+ }
+ }
+
+ // Outer progress (session).
+ ShapePath {
+ strokeColor: gauge.outerColor
+ fillColor: "transparent"
+ strokeWidth: gauge.ringWidth
+ capStyle: ShapePath.RoundCap
+ PathAngleArc {
+ centerX: gauge.width / 2
+ centerY: gauge.height / 2
+ radiusX: gauge.outerRadius
+ radiusY: gauge.outerRadius
+ startAngle: -90
+ sweepAngle: gauge.outerPercent >= 0 ? Parser.sweepAngle(gauge.outerPercent) : 0
+ }
+ }
+
+ // Inner progress (weekly).
+ ShapePath {
+ strokeColor: gauge.innerColor
+ fillColor: "transparent"
+ strokeWidth: gauge.ringWidth
+ capStyle: ShapePath.RoundCap
+ PathAngleArc {
+ centerX: gauge.width / 2
+ centerY: gauge.height / 2
+ radiusX: gauge.innerRadius
+ radiusY: gauge.innerRadius
+ startAngle: -90
+ sweepAngle: gauge.innerPercent >= 0 ? Parser.sweepAngle(gauge.innerPercent) : 0
+ }
+ }
+ }
+
+ Text {
+ anchors.centerIn: parent
+ visible: gauge.centerText.length > 0
+ text: gauge.centerText
+ color: Kirigami.Theme.textColor
+ font.pixelSize: Math.max(7, Math.min(gauge.width, gauge.height) * gauge.centerTextScale)
+ font.bold: true
+ }
+}
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/main.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/main.qml
new file mode 100644
index 0000000..0ae4ce8
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/main.qml
@@ -0,0 +1,224 @@
+import QtQuick
+import org.kde.plasma.plasmoid
+import org.kde.plasma.core as PlasmaCore
+import org.kde.plasma.plasma5support as Plasma5Support
+import org.kde.kirigami as Kirigami
+import "../code/parser.js" as Parser
+
+PlasmoidItem {
+ id: root
+
+ // Cached provider view models keyed by provider id. Entries are only
+ // replaced when fresh data arrives, so the panel never blanks out while
+ // a background refresh is in flight.
+ property var modelsById: ({})
+ // Display order of provider ids (selection order, or arrival order when
+ // following the CLI config).
+ property var modelOrder: []
+ // Ordered list consumed by the representations.
+ property var models: []
+ property string globalError: ""
+ property bool loading: false
+ property int pendingCount: 0
+ property double lastUpdatedMs: -1
+ // Ticks every 30s so countdown and "ago" labels stay fresh.
+ property double nowMs: Date.now()
+
+ // Ring palette, shared by compact and full representations. The weekly
+ // orange is darkened on light themes to keep contrast against the track.
+ readonly property bool darkTheme: Kirigami.Theme.textColor.hslLightness > 0.5
+ readonly property color sessionColor: root.darkTheme ? "#3daee9" : "#2980b9"
+ readonly property color weeklyColor: root.darkTheme ? "#f67400" : "#c85400"
+
+ // Local token-cost stats keyed by provider id (Codex/Claude scans).
+ property var costById: ({})
+
+ readonly property string codexbarPath: Plasmoid.configuration.codexbarPath || "codexbar"
+ readonly property bool showPace: Plasmoid.configuration.showPace
+ readonly property bool showCost: Plasmoid.configuration.showCost
+ readonly property bool showHistory: Plasmoid.configuration.showHistory
+ readonly property string proxyUrl: Plasmoid.configuration.proxyUrl || ""
+ readonly property var selectedProviders: Parser.selectionList(Plasmoid.configuration.selectedProviders || "")
+
+ readonly property var commands: Parser.buildCommands(root.codexbarPath, root.selectedProviders, root.proxyUrl)
+ readonly property string costCommand: Parser.buildCostCommand(root.codexbarPath, root.proxyUrl)
+
+ switchWidth: Kirigami.Units.gridUnit * 15
+ switchHeight: Kirigami.Units.gridUnit * 12
+
+ Plasmoid.icon: "office-chart-bar"
+ toolTipMainText: i18n("CodexBar")
+ toolTipSubText: {
+ var lines = []
+ for (var i = 0; i < root.models.length; i++) {
+ var m = root.models[i]
+ if (m.error) {
+ lines.push(m.name + ": " + m.error)
+ continue
+ }
+ var parts = []
+ for (var j = 0; j < m.windows.length; j++) {
+ var w = m.windows[j]
+ if (w.usageKnown === false) {
+ continue
+ }
+ parts.push(w.label + " " + w.usedPercent + "% used")
+ }
+ lines.push(m.name + ": " + (parts.length ? parts.join(", ") : i18n("no data")))
+ }
+ if (root.globalError.length > 0 && root.models.length === 0) {
+ lines.push(root.globalError)
+ }
+ var ago = Parser.formatAgo(root.lastUpdatedMs, root.nowMs)
+ if (root.loading) {
+ lines.push(ago.length > 0
+ ? i18n("Updating… (last update %1)", ago)
+ : i18n("Updating…"))
+ } else if (ago.length > 0) {
+ lines.push(i18n("Updated %1", ago))
+ }
+ return lines.join("\n")
+ }
+
+ onCommandsChanged: root.refresh()
+
+ function refresh() {
+ if (root.loading) {
+ return
+ }
+ root.loading = true
+ root.pendingCount = root.commands.length + (root.showCost ? 1 : 0)
+ // Old models stay on screen; the executable engine reruns each
+ // command and results merge in as they arrive.
+ executable.connectedSources = []
+ for (var i = 0; i < root.commands.length; i++) {
+ executable.connectSource(root.commands[i])
+ }
+ if (root.showCost) {
+ executable.connectSource(root.costCommand)
+ }
+ }
+
+ function handleCostResult(stdout) {
+ var parsed = Parser.parseCostJson(stdout)
+ if (parsed.ok) {
+ root.costById = parsed.byId
+ root.persistCache()
+ }
+ // A failed cost scan keeps the previous numbers; usage is unaffected.
+ }
+
+ function handleResult(stdout, stderr, exitCode) {
+ var parsed = Parser.parseUsageJson(stdout)
+ if (!parsed.ok) {
+ var message = stderr && stderr.trim().length > 0
+ ? stderr.trim().split("\n").pop()
+ : parsed.error
+ if (exitCode === 127) {
+ message = i18n("codexbar binary not found — set its path in the widget settings")
+ }
+ // No parseable payload: keep whatever is cached, surface the error.
+ root.globalError = root.models.length === 0 ? message : ""
+ return
+ }
+ root.globalError = ""
+ var now = Date.now()
+ var state = Parser.applyUpdate(root.modelsById, parsed.models, now)
+ root.modelsById = state.byId
+ var hadFreshData = parsed.models.some(function(m) { return !m.error })
+ if (hadFreshData) {
+ root.lastUpdatedMs = now
+ }
+ for (var i = 0; i < parsed.models.length; i++) {
+ var id = parsed.models[i].id
+ if (root.modelOrder.indexOf(id) === -1) {
+ root.modelOrder = root.modelOrder.concat([id])
+ }
+ }
+ root.rebuildModels()
+ root.persistCache()
+ }
+
+ function rebuildModels() {
+ var order = root.selectedProviders.length > 0
+ ? root.selectedProviders
+ : root.modelOrder
+ var list = []
+ for (var i = 0; i < order.length; i++) {
+ var m = root.modelsById[order[i]]
+ if (m) {
+ list.push(m)
+ }
+ }
+ root.models = list
+ }
+
+ function persistCache() {
+ Plasmoid.configuration.cacheJson = JSON.stringify({
+ byId: root.modelsById,
+ order: root.modelOrder,
+ costById: root.costById,
+ updatedAtMs: root.lastUpdatedMs,
+ })
+ }
+
+ function loadCache() {
+ var raw = Plasmoid.configuration.cacheJson || ""
+ if (raw.length === 0) {
+ return
+ }
+ try {
+ var cache = JSON.parse(raw)
+ root.modelsById = cache.byId || {}
+ root.modelOrder = cache.order || []
+ root.costById = cache.costById || {}
+ root.lastUpdatedMs = cache.updatedAtMs !== undefined ? cache.updatedAtMs : -1
+ root.rebuildModels()
+ } catch (e) {
+ // Corrupt cache is not fatal; the next refresh rewrites it.
+ }
+ }
+
+ Plasma5Support.DataSource {
+ id: executable
+ engine: "executable"
+ connectedSources: []
+ onNewData: function(source, data) {
+ executable.disconnectSource(source)
+ root.pendingCount = Math.max(0, root.pendingCount - 1)
+ if (root.pendingCount === 0) {
+ root.loading = false
+ }
+ if (source === root.costCommand) {
+ root.handleCostResult(data["stdout"] || "")
+ } else {
+ root.handleResult(
+ data["stdout"] || "",
+ data["stderr"] || "",
+ data["exit code"])
+ }
+ }
+ }
+
+ Timer {
+ interval: Math.max(1, Plasmoid.configuration.refreshMinutes) * 60 * 1000
+ running: true
+ repeat: true
+ onTriggered: root.refresh()
+ }
+
+ Timer {
+ interval: 30 * 1000
+ running: true
+ repeat: true
+ onTriggered: root.nowMs = Date.now()
+ }
+
+ Component.onCompleted: {
+ root.loadCache()
+ root.refresh()
+ }
+
+ compactRepresentation: CompactRepresentation {}
+ fullRepresentation: FullRepresentation {}
+}
diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/metadata.json b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/metadata.json
new file mode 100644
index 0000000..5b9306d
--- /dev/null
+++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/metadata.json
@@ -0,0 +1,23 @@
+{
+ "KPlugin": {
+ "Authors": [
+ {
+ "Email": "freelancerevil@gmail.com",
+ "Name": "Pavel Rykov"
+ }
+ ],
+ "Category": "System Information",
+ "Description": "AI coding-provider limits from the CodexBar CLI",
+ "Description[ru]": "Лимиты AI-агентов из CodexBar CLI",
+ "Icon": "office-chart-bar",
+ "Id": "org.rpa.codexbar",
+ "License": "MIT",
+ "Name": "CodexBar",
+ "Version": "0.5.0",
+ "Website": "https://github.com/EvilFreelancer/CodexBar-KDE"
+ },
+ "KPackageStructure": "Plasma/Applet",
+ "X-Plasma-API-Minimum-Version": "6.0",
+ "X-Plasma-NotificationArea": true,
+ "X-Plasma-NotificationAreaCategory": "SystemServices"
+}