diff --git a/contents/scripts/usage.sh b/contents/scripts/usage.sh index 366728c..565a057 100755 --- a/contents/scripts/usage.sh +++ b/contents/scripts/usage.sh @@ -1,21 +1,14 @@ -#!/usr/bin/env python3 -"""Backend per il plasmoide KDE Plasma 6: Antigravity Usage Dashboard. +#!/usr/bin/env bash +# Backend per il plasmoide KDE Plasma 6: Antigravity Usage Dashboard. +# Esegue il client Python incorporato per recuperare le quote da Google CloudCode PA. -Interroga il gateway Google CloudCode PA (https://daily-cloudcode-pa.googleapis.com) -leggendo il token OAuth da ~/.gemini/antigravity-cli/antigravity-oauth-token -ed eseguendo il refresh automatico se scaduto o non valido. -Restituisce lo stato delle quote per le 3 famiglie di modelli: -- Gemini (Gemini 3.x Flash / Pro) -- Claude (Claude Sonnet 4.6 / Opus 4.6) -- GPT-OSS (GPT-OSS 120B) -""" - -import json +exec /usr/bin/python3 - "$@" << 'EOF' import os import sys +import json import time -import urllib.error import urllib.request +import urllib.error from datetime import datetime, timezone TOKEN_FILE = os.path.expanduser("~/.gemini/antigravity-cli/antigravity-oauth-token") @@ -27,7 +20,6 @@ UA = "antigravity/cli/1.1.17" API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1" METADATA = json.dumps({"ideType": "ANTIGRAVITY", "platform": "LINUX", "pluginType": "GEMINI"}) - def get_token_data(): if not os.path.exists(TOKEN_FILE): return None @@ -37,7 +29,6 @@ def get_token_data(): except Exception: return None - def save_token_data(data): try: tmp = TOKEN_FILE + ".tmp" @@ -47,7 +38,6 @@ def save_token_data(data): except Exception: pass - def refresh_token(refresh_tok): req_body = json.dumps({ "client_id": CLIENT_ID, @@ -60,7 +50,6 @@ def refresh_token(refresh_tok): res = json.loads(resp.read().decode("utf-8")) return res.get("access_token"), int(res.get("expires_in", 3600)) - def call_api(path, token, payload): url = f"{BASE_URL}{path}" headers = { @@ -75,7 +64,6 @@ def call_api(path, token, payload): with urllib.request.urlopen(req, timeout=15) as resp: return json.loads(resp.read().decode("utf-8")) - def parse_iso_epoch(ts): if not ts: return 0 @@ -85,7 +73,6 @@ def parse_iso_epoch(ts): except Exception: return 0 - def main(): data = get_token_data() if not data or "token" not in data: @@ -144,6 +131,7 @@ def main(): gem_reset_str = gem_b.get("resetTime", "") gemini_info = { "id": "gemini", + "tag": "GEMINI", "name": "Gemini 3.x", "models": "Pro / Flash", "remaining": round(gem_rem, 4), @@ -159,6 +147,7 @@ def main(): cld_reset_str = cld_b.get("resetTime", "") claude_info = { "id": "claude", + "tag": "CLAUDE", "name": "Claude 4.6", "models": "Sonnet / Opus", "remaining": round(cld_rem, 4), @@ -173,6 +162,7 @@ def main(): gpt_reset_str = gpt_b.get("resetTime", "") gpt_info = { "id": "gpt-oss", + "tag": "GPT", "name": "GPT-OSS", "models": "120B Medium", "remaining": round(gpt_rem, 4), @@ -184,10 +174,13 @@ def main(): out = { "ok": True, "project": project_id, + "gemini": gemini_info, + "claude": claude_info, + "gpt": gpt_info, "groups": [gemini_info, claude_info, gpt_info] } print(json.dumps(out, indent=2)) - if __name__ == "__main__": main() +EOF diff --git a/contents/ui/GroupCard.qml b/contents/ui/GroupCard.qml new file mode 100644 index 0000000..6269700 --- /dev/null +++ b/contents/ui/GroupCard.qml @@ -0,0 +1,126 @@ +/* Card per gruppo di modelli Antigravity (Gemini, Claude, GPT-OSS) */ +import QtQuick +import QtQuick.Layouts +import org.kde.plasma.components 3.0 as PC3 +import org.kde.kirigami as Kirigami + +Rectangle { + id: root + + property string tag: "" + property string title: "" + property string models: "" + property real remaining: 1.0 + property real usedPercent: 0.0 + property int resetAt: 0 + property int clockTick: 0 + property color accentColor: "#818cf8" + + function countdown() { + const tick = root.clockTick + if (!resetAt || resetAt <= 0) return "—" + let mins = Math.max(0, Math.floor((resetAt * 1000 - Date.now()) / 60000)) + if (mins === 0) return i18n("ora") + const days = Math.floor(mins / 1440) + mins -= days * 1440 + const hours = Math.floor(mins / 60) + mins -= hours * 60 + return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, mins) + } + + Layout.fillWidth: true + Layout.preferredHeight: 46 + radius: 5 + color: Qt.rgba(0, 0, 0, 0.28) + border.width: 1 + border.color: root.accentColor + + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + spacing: 2 + + // Riga 1: Tag + Titolo + Modelli + % Residuo + RowLayout { + Layout.fillWidth: true + spacing: 4 + + Rectangle { + Layout.preferredWidth: 38 + Layout.preferredHeight: 14 + radius: 7 + color: root.accentColor + PC3.Label { + anchors.centerIn: parent + text: root.tag + color: "#101012" + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + + PC3.Label { + text: root.title + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1 + } + + PC3.Label { + text: "· " + root.models + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + Layout.fillWidth: true + elide: Text.ElideRight + } + + PC3.Label { + text: Math.round(root.remaining * 100) + "%" + color: root.accentColor + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize + } + } + + // Riga 2: Barra di avanzamento + Item { + Layout.fillWidth: true + Layout.preferredHeight: 5 + + Rectangle { + id: track + anchors.fill: parent + radius: 2.5 + color: Qt.rgba(0, 0, 0, 0.5) + } + + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: track.width * Math.max(0, Math.min(1, root.remaining)) + height: parent.height + radius: 2.5 + color: root.accentColor + } + } + + // Riga 3: Dettaglio usato + Reset countdown + RowLayout { + Layout.fillWidth: true + spacing: 4 + + PC3.Label { + text: i18n("%1% usato", root.usedPercent.toFixed(1)) + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + + Item { Layout.fillWidth: true } + + PC3.Label { + text: i18n("⏱ %1", root.countdown()) + color: root.accentColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } + } +} diff --git a/contents/ui/main.qml b/contents/ui/main.qml index 0ed1fff..1474005 100644 --- a/contents/ui/main.qml +++ b/contents/ui/main.qml @@ -11,7 +11,7 @@ import org.kde.kirigami as Kirigami PlasmoidItem { id: root - property var data: null // { ok, project, groups: [ { id, name, models, remaining, used_percent, reset_time, reset_at } ] } + property var data: null property string lastUpdate: "" property string errorMsg: "" property bool loading: false @@ -30,10 +30,10 @@ PlasmoidItem { Plasmoid.icon: "compass" Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground toolTipMainText: i18n("Antigravity — quote modelli") - toolTipSubText: root.data && root.data.groups && root.data.groups.length > 0 + toolTipSubText: root.data && root.data.gemini ? i18n("Gemini: %1% · Claude: %2%", - Math.round((root.data.groups[0].remaining || 0) * 100), - Math.round((root.data.groups.length > 1 ? root.data.groups[1].remaining || 0 : 0) * 100)) + Math.round((root.data.gemini.remaining || 0) * 100), + Math.round((root.data.claude ? root.data.claude.remaining || 0 : 0) * 100)) : (root.errorMsg || "") preferredRepresentation: fullRepresentation @@ -53,11 +53,12 @@ PlasmoidItem { return } try { - const parsed = JSON.parse((result["stdout"] || "").trim()) + const stdout = (result["stdout"] || "").trim() + const parsed = JSON.parse(stdout) if (parsed && parsed.ok === false) { root.errorMsg = parsed.error + (parsed.detail ? " — " + parsed.detail : "") root.data = null - } else if (parsed && parsed.ok && parsed.groups) { + } else if (parsed && parsed.ok) { root.data = parsed root.errorMsg = "" root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss") @@ -89,18 +90,6 @@ PlasmoidItem { return root.themeIndigo } - function countdownFor(resetAt) { - const tick = root.clockTick - if (!resetAt || resetAt <= 0) return "—" - let mins = Math.max(0, Math.floor((resetAt * 1000 - Date.now()) / 60000)) - if (mins === 0) return i18n("ora") - const days = Math.floor(mins / 1440) - mins -= days * 1440 - const hours = Math.floor(mins / 60) - mins -= hours * 60 - return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, mins) - } - Component.onCompleted: root.refresh() Timer { @@ -196,7 +185,7 @@ PlasmoidItem { color: Qt.rgba(0.66, 0.33, 0.97, 0.35) } - // Visualizzazione Errore + // Messaggio Errore PC3.Label { visible: root.errorMsg.length > 0 Layout.fillWidth: true @@ -205,95 +194,40 @@ PlasmoidItem { text: i18n("Errore: %1", root.errorMsg) } - // Lista delle 3 quote (Gemini, Claude, GPT-OSS) - Repeater { - model: root.data && root.data.groups ? root.data.groups : [] + // 1. Gemini Card + GroupCard { + tag: "GEMINI" + title: "Gemini 3.x" + models: root.data && root.data.gemini ? (root.data.gemini.models || "Pro/Flash") : "Pro/Flash" + remaining: root.data && root.data.gemini ? (root.data.gemini.remaining || 0) : 1.0 + usedPercent: root.data && root.data.gemini ? (root.data.gemini.used_percent || 0) : 0 + resetAt: root.data && root.data.gemini ? (root.data.gemini.reset_at || 0) : 0 + clockTick: root.clockTick + accentColor: root.accentColorFor(Math.round((root.data && root.data.gemini ? root.data.gemini.remaining : 1.0) * 100)) + } - delegate: Rectangle { - required property var modelData - required property int index + // 2. Claude Card + GroupCard { + tag: "CLAUDE" + title: "Claude 4.6" + models: root.data && root.data.claude ? (root.data.claude.models || "Sonnet/Opus") : "Sonnet/Opus" + remaining: root.data && root.data.claude ? (root.data.claude.remaining || 0) : 1.0 + usedPercent: root.data && root.data.claude ? (root.data.claude.used_percent || 0) : 0 + resetAt: root.data && root.data.claude ? (root.data.claude.reset_at || 0) : 0 + clockTick: root.clockTick + accentColor: root.accentColorFor(Math.round((root.data && root.data.claude ? root.data.claude.remaining : 1.0) * 100)) + } - Layout.fillWidth: true - Layout.preferredHeight: 46 - radius: 5 - color: Qt.rgba(0, 0, 0, 0.28) - border.width: 1 - border.color: root.accentColorFor(Math.round((modelData.remaining || 0) * 100)) - - ColumnLayout { - anchors.fill: parent - anchors.margins: 4 - spacing: 2 - - // Riga 1: Nome gruppo + Modelli + % Residuo - RowLayout { - Layout.fillWidth: true - spacing: 4 - - PC3.Label { - text: modelData.name || "" - font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1 - } - - PC3.Label { - text: "· " + (modelData.models || "") - color: Kirigami.Theme.disabledTextColor - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 - Layout.fillWidth: true - elide: Text.ElideRight - } - - PC3.Label { - text: Math.round((modelData.remaining || 0) * 100) + "%" - color: root.accentColorFor(Math.round((modelData.remaining || 0) * 100)) - font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize - } - } - - // Riga 2: Barra di avanzamento - Item { - Layout.fillWidth: true - Layout.preferredHeight: 5 - - Rectangle { - anchors.fill: parent - radius: 2.5 - color: Qt.rgba(0, 0, 0, 0.5) - } - - Rectangle { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: parent.width * Math.max(0, Math.min(1, modelData.remaining || 0)) - height: parent.height - radius: 2.5 - color: root.accentColorFor(Math.round((modelData.remaining || 0) * 100)) - } - } - - // Riga 3: Dettaglio usato + Reset countdown - RowLayout { - Layout.fillWidth: true - spacing: 4 - - PC3.Label { - text: i18n("%1% usato", modelData.used_percent !== undefined ? modelData.used_percent : 0) - color: Kirigami.Theme.disabledTextColor - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 - } - - Item { Layout.fillWidth: true } - - PC3.Label { - text: i18n("reset %1", root.countdownFor(modelData.reset_at)) - color: root.themeIndigo - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 - } - } - } - } + // 3. GPT Card + GroupCard { + tag: "GPT" + title: "GPT-OSS" + models: root.data && root.data.gpt ? (root.data.gpt.models || "120B") : "120B" + remaining: root.data && root.data.gpt ? (root.data.gpt.remaining || 0) : 1.0 + usedPercent: root.data && root.data.gpt ? (root.data.gpt.used_percent || 0) : 0 + resetAt: root.data && root.data.gpt ? (root.data.gpt.reset_at || 0) : 0 + clockTick: root.clockTick + accentColor: root.accentColorFor(Math.round((root.data && root.data.gpt ? root.data.gpt.remaining : 1.0) * 100)) } Item { Layout.fillHeight: true }