plasmoids: añade commandoutput y codexbar al tracking

Ambos ya estaban puestos en un panel real pero no viajaban a la otra
máquina -- detectado en la misma auditoría que quitó claudeusageswitcher
y tt.launchpadPlasma (sin usar). Sin esto, una reinstalación desde
arch-setup dejaría el panel sin estos dos widgets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Josevi
2026-09-20 23:15:48 +02:00
co-authored by Claude Sonnet 5
parent d434a3c635
commit b98999cde9
40 changed files with 4029 additions and 0 deletions
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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 <id>”.")
}
}
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
}
}
}
}
}
}
@@ -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
}
}
}
}
}
@@ -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
}
}
@@ -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 {}
}