Add the agents usage bar widget

This commit is contained in:
SirBlob committed 2026-10-04 02:24:47 -04:00
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
}
}
+162
View File
@@ -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
}
}