Add the agents usage bar widget
This commit is contained in:
1 parent
854ce5fe5a
commit
6c83f7089c
18 files changed
+1936
-4
No files matched your search
@@ -0,0 +1,71 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
|
||||
BarWidget {
|
||||
id: root
|
||||
moduleName: "blob.claude-usage"
|
||||
|
||||
readonly property bool opened: panelLoader.item ? panelLoader.item.opened === true : false
|
||||
readonly property bool popoutSwitchClosing: panelLoader.item ? panelLoader.item.popoutSwitchClosing === true : false
|
||||
|
||||
function injectPanel() {
|
||||
var panel = panelLoader.item
|
||||
if (!panel) return
|
||||
panel.bar = root.bar
|
||||
panel.settings = root.settings
|
||||
panel.anchorItem = button
|
||||
panel.hostWidget = root
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (panelLoader.item) panelLoader.item.open()
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (panelLoader.item) panelLoader.item.close()
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
if (panelLoader.item) panelLoader.item.toggle()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (panelLoader.item) panelLoader.item.refresh()
|
||||
}
|
||||
|
||||
function closeForPopoutSwitch() {
|
||||
if (panelLoader.item) panelLoader.item.closeForPopoutSwitch()
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
onBarChanged: injectPanel()
|
||||
onSettingsChanged: injectPanel()
|
||||
|
||||
Loader {
|
||||
id: panelLoader
|
||||
active: true
|
||||
source: Qt.resolvedUrl("Panel.qml")
|
||||
visible: false
|
||||
onLoaded: {
|
||||
root.injectPanel()
|
||||
Qt.callLater(root.injectPanel)
|
||||
}
|
||||
}
|
||||
|
||||
BarIconButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: ""
|
||||
tooltipText: ""
|
||||
|
||||
onPressed: function(mouseButton) {
|
||||
if (!root.bar) return
|
||||
if (mouseButton === Qt.MiddleButton) root.refresh()
|
||||
else root.togglePanel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
function parseUsage(raw) {
|
||||
var values = {}
|
||||
var lines = String(raw || "").split("\n")
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var tabIndex = lines[i].indexOf("\t")
|
||||
if (tabIndex === -1) continue
|
||||
values[lines[i].substring(0, tabIndex)] = lines[i].substring(tabIndex + 1).trim()
|
||||
}
|
||||
|
||||
return {
|
||||
session: limitFrom(values, "session"),
|
||||
weekly: limitFrom(values, "weekly"),
|
||||
weeklyOpus: limitFrom(values, "weekly_opus")
|
||||
}
|
||||
}
|
||||
|
||||
function limitFrom(values, name) {
|
||||
var percentText = values[name + "_percent"]
|
||||
if (percentText === undefined) return null
|
||||
|
||||
var percent = parseInt(percentText, 10)
|
||||
return {
|
||||
percent: isNaN(percent) ? 0 : Math.max(0, Math.min(100, percent)),
|
||||
resetsAt: values[name + "_resets"] || ""
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimeUntil(resetsAt, nowMs) {
|
||||
if (!resetsAt) return ""
|
||||
|
||||
var resetMs = new Date(resetsAt).getTime()
|
||||
if (isNaN(resetMs)) return ""
|
||||
|
||||
var totalMinutes = Math.max(0, Math.round((resetMs - nowMs) / 60000))
|
||||
var days = Math.floor(totalMinutes / 1440)
|
||||
var hours = Math.floor((totalMinutes % 1440) / 60)
|
||||
var minutes = totalMinutes % 60
|
||||
|
||||
if (days > 0) return days + "d " + hours + "h"
|
||||
if (hours > 0) return hours + "h " + minutes + "m"
|
||||
return minutes + "m"
|
||||
}
|
||||
|
||||
function limitDetail(limit, nowMs) {
|
||||
if (!limit) return ""
|
||||
|
||||
var timeUntilReset = formatTimeUntil(limit.resetsAt, nowMs)
|
||||
if (timeUntilReset === "") return limit.percent + "%"
|
||||
return limit.percent + "% resets in " + timeUntilReset
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
parseUsage: parseUsage,
|
||||
formatTimeUntil: formatTimeUntil,
|
||||
limitDetail: limitDetail
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
import "Model.js" as Model
|
||||
|
||||
Panel {
|
||||
id: root
|
||||
moduleName: "blob.claude-usage"
|
||||
ipcTarget: "blob.claude-usage"
|
||||
|
||||
property var anchorItem: null
|
||||
property var hostWidget: null
|
||||
readonly property var barIdentity: hostWidget || root
|
||||
|
||||
property var usage: ({ session: null, weekly: null, weeklyOpus: null })
|
||||
property string errorMessage: ""
|
||||
property bool loading: false
|
||||
property double nowMs: Date.now()
|
||||
|
||||
readonly property bool hasUsage: usage.session !== null || usage.weekly !== null
|
||||
readonly property int refreshMinutes: Math.max(1, parseInt(setting("refreshMinutes", 5), 10) || 5)
|
||||
|
||||
function open() {
|
||||
root.controller.show()
|
||||
root.refresh()
|
||||
}
|
||||
|
||||
function close() {
|
||||
root.controller.hide()
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (root.opened) root.close()
|
||||
else root.open()
|
||||
}
|
||||
|
||||
function switchPanel(direction) {
|
||||
if (root.bar && typeof root.bar.switchPanelFrom === "function")
|
||||
return root.bar.switchPanelFrom(root.barIdentity, direction)
|
||||
return false
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
root.nowMs = Date.now()
|
||||
if (usageProcess.running) return
|
||||
root.loading = true
|
||||
usageProcess.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: usageProcess
|
||||
command: ["blob-claude-usage"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var parsedUsage = Model.parseUsage(text)
|
||||
if (parsedUsage.session !== null || parsedUsage.weekly !== null) {
|
||||
root.usage = parsedUsage
|
||||
root.errorMessage = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var message = String(text || "").trim()
|
||||
if (message !== "") root.errorMessage = message
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
root.loading = false
|
||||
if (exitCode !== 0 && root.errorMessage === "") root.errorMessage = "Could not load usage"
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: root.refreshMinutes * 60 * 1000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 60 * 1000
|
||||
running: root.opened
|
||||
repeat: true
|
||||
onTriggered: root.nowMs = Date.now()
|
||||
}
|
||||
|
||||
KeyboardPanel {
|
||||
id: panel
|
||||
anchorItem: root.anchorItem
|
||||
owner: root.barIdentity
|
||||
bar: root.bar
|
||||
open: root.opened
|
||||
focusTarget: keyCatcher
|
||||
contentWidth: panel.fittedContentWidth(Style.space(340))
|
||||
contentHeight: panel.fittedContentHeight(usageColumn.implicitHeight)
|
||||
|
||||
PanelKeyCatcher {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
onCloseRequested: root.close()
|
||||
onTabRequested: function(direction) { root.switchPanel(direction) }
|
||||
onTextKey: function(key) { if (key === "r") root.refresh() }
|
||||
|
||||
Column {
|
||||
id: usageColumn
|
||||
width: parent.width
|
||||
spacing: Style.space(14)
|
||||
|
||||
PanelSectionHeader {
|
||||
text: "CLAUDE CODE USAGE"
|
||||
foreground: root.barForeground
|
||||
}
|
||||
|
||||
MetricBar {
|
||||
width: parent.width
|
||||
visible: root.usage.session !== null
|
||||
icon: ""
|
||||
name: "Session"
|
||||
percent: root.usage.session ? root.usage.session.percent : 0
|
||||
detail: Model.limitDetail(root.usage.session, root.nowMs)
|
||||
}
|
||||
|
||||
MetricBar {
|
||||
width: parent.width
|
||||
visible: root.usage.weekly !== null
|
||||
icon: ""
|
||||
name: "Weekly"
|
||||
percent: root.usage.weekly ? root.usage.weekly.percent : 0
|
||||
detail: Model.limitDetail(root.usage.weekly, root.nowMs)
|
||||
}
|
||||
|
||||
MetricBar {
|
||||
width: parent.width
|
||||
visible: root.usage.weeklyOpus !== null
|
||||
icon: ""
|
||||
name: "Weekly Opus"
|
||||
percent: root.usage.weeklyOpus ? root.usage.weeklyOpus.percent : 0
|
||||
detail: Model.limitDetail(root.usage.weeklyOpus, root.nowMs)
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: root.errorMessage !== "" || (!root.hasUsage && root.loading)
|
||||
text: root.errorMessage !== "" ? root.errorMessage : "Loading usage..."
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
color: root.errorMessage !== "" ? Color.urgent : Qt.darker(root.barForeground, 1.5)
|
||||
font.family: Style.font.family
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "blob.claude-usage",
|
||||
"name": "Claude Usage",
|
||||
"version": "1.0.0",
|
||||
"author": "Blob",
|
||||
"description": "Claude Code session and weekly usage limits",
|
||||
"kinds": [
|
||||
"bar-widget"
|
||||
],
|
||||
"entryPoints": {
|
||||
"barWidget": "BarWidget.qml"
|
||||
},
|
||||
"barWidget": {
|
||||
"displayName": "Claude Usage",
|
||||
"description": "Claude Code session and weekly usage limits",
|
||||
"category": "Info",
|
||||
"allowMultiple": false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user