Files

1268 lines
41 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Ui
BarWidget {
id: root
moduleName: "omarchy.model-usage"
property bool popupOpen: false
property bool settingsMode: false
property var draftSettings: ({})
property string settingsStatusText: ""
property int selectedTabIndex: 0
property bool refreshFlash: false
readonly property color foreground: bar ? bar.foreground : Color.foreground
readonly property color background: Color.popups.background
readonly property color border: Color.popups.border
readonly property color urgent: bar ? bar.urgent : Color.urgent
readonly property color dim: Qt.darker(foreground, 1.45)
readonly property color card: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.055)
readonly property color cardHover: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.085)
readonly property color outline: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
readonly property color track: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.24)
readonly property string fontFamily: bar ? bar.fontFamily : "JetBrainsMono Nerd Font"
readonly property var providers: usageMain.enabledProviders
readonly property var selectedProvider: providers.length > 0 ? providers[Math.min(selectedTabIndex, providers.length - 1)] : null
function close() {
popupOpen = false
settingsMode = false
}
function triggerPress(button) {
root.handleChipPress(Math.max(0, Math.min(selectedTabIndex, providers.length - 1)), button, null)
}
function triggerRefresh() {
refreshFlash = true
refreshFlashTimer.restart()
usageMain.refreshAll(true)
}
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)) }
function alpha(c, a) { return Qt.rgba(c.r, c.g, c.b, a) }
function cloneObject(value, fallback) {
if (value === undefined || value === null) return fallback
try {
return JSON.parse(JSON.stringify(value))
} catch (e) {
return fallback
}
}
function defaultSettings() {
return {
providers: {
claude: {
enabled: true,
statsPath: "~/.claude/stats-cache.json",
credentialsPath: "~/.claude/.credentials.json",
projectsPath: "~/.claude/projects"
},
codex: { enabled: true }
},
refreshIntervalSec: 900,
syncMode: "Off",
syncDir: "",
syncFileName: "",
syncDeviceId: ""
}
}
function parseSyncEnabledValue(value) {
if (value === true) return true
var text = String(value || "").trim().toLowerCase()
return text === "on" || text === "enabled" || text === "true" || text === "yes" || text === "1"
}
function normalizedSettings(source) {
var defaults = defaultSettings()
var next = cloneObject(source, {}) || {}
if (!next.providers || typeof next.providers !== "object") next.providers = {}
var claude = cloneObject(next.providers.claude, {}) || {}
var codex = cloneObject(next.providers.codex, {}) || {}
if (claude.enabled === undefined || claude.enabled === null) claude.enabled = defaults.providers.claude.enabled
if (codex.enabled === undefined || codex.enabled === null) codex.enabled = defaults.providers.codex.enabled
if (!claude.statsPath) claude.statsPath = defaults.providers.claude.statsPath
if (!claude.credentialsPath) claude.credentialsPath = defaults.providers.claude.credentialsPath
if (!claude.projectsPath) claude.projectsPath = defaults.providers.claude.projectsPath
next.providers.claude = claude
next.providers.codex = codex
var refresh = Number(next.refreshIntervalSec === undefined || next.refreshIntervalSec === null ? defaults.refreshIntervalSec : next.refreshIntervalSec)
next.refreshIntervalSec = Math.round(clamp(isFinite(refresh) ? refresh : defaults.refreshIntervalSec, 30, 3600))
next.syncMode = parseSyncEnabledValue(next.syncMode !== undefined ? next.syncMode : next.syncEnabled) ? "On" : "Off"
next.syncDir = String(next.syncDir || "")
next.syncFileName = String(next.syncFileName || "")
next.syncDeviceId = String(next.syncDeviceId || "")
return next
}
function draftValue(name, fallback) {
var value = draftSettings ? draftSettings[name] : undefined
return value === undefined || value === null ? fallback : value
}
function draftProviderValue(providerId, name, fallback) {
var provider = draftSettings && draftSettings.providers ? draftSettings.providers[providerId] : null
var value = provider ? provider[name] : undefined
return value === undefined || value === null ? fallback : value
}
function setDraftValue(name, value) {
var next = normalizedSettings(draftSettings)
next[name] = value
draftSettings = next
}
function setDraftProviderValue(providerId, name, value) {
var next = normalizedSettings(draftSettings)
if (!next.providers) next.providers = {}
if (!next.providers[providerId]) next.providers[providerId] = {}
next.providers[providerId][name] = value
draftSettings = next
}
function openSettings() {
draftSettings = normalizedSettings(settings)
settingsStatusText = ""
settingsMode = true
popupOpen = true
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
function showUsage() {
settingsMode = false
settingsStatusText = ""
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
function canPersistSettings() {
return !!(bar && bar.shell && typeof bar.shell.updateEntryInline === "function")
}
function saveSettings() {
var next = normalizedSettings(draftSettings)
draftSettings = next
root.settings = next
if (canPersistSettings()) {
bar.shell.updateEntryInline(root.moduleName, next)
settingsStatusText = "Saved to shell.json"
} else {
settingsStatusText = "Saved for this session"
}
usageMain.refreshAll(true)
}
function colorChannelLuminance(value) {
var channel = Number(value)
if (!isFinite(channel)) return 0
return channel <= 0.03928 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4)
}
function colorLuminance(color) {
return 0.2126 * colorChannelLuminance(color.r)
+ 0.7152 * colorChannelLuminance(color.g)
+ 0.0722 * colorChannelLuminance(color.b)
}
function codexIconSource(surfaceColor) {
return colorLuminance(surfaceColor || Color.background) >= 0.5
? Qt.resolvedUrl("assets/codex-light.svg")
: Qt.resolvedUrl("assets/codex.svg")
}
function iconSourceForProvider(provider, surfaceColor) {
if (!provider) return ""
if (provider.providerId === "claude") return Qt.resolvedUrl("assets/claude.svg")
if (provider.providerId === "codex") return codexIconSource(surfaceColor)
return ""
}
function rateLimitLabelIsWeekly(label) {
var text = String(label || "").toLowerCase()
return text.indexOf("week") >= 0 || text.indexOf("7-day") >= 0 || text.indexOf("seven_day") >= 0
}
function usagePercent(provider) {
if (!provider) return -1
var weekly = weeklyUsage(provider)
if (weekly.percent >= 0) return weekly.percent
var values = []
if (provider.rateLimitPercent >= 0) values.push(provider.rateLimitPercent)
if (provider.secondaryRateLimitPercent >= 0) values.push(provider.secondaryRateLimitPercent)
if (values.length === 0) return -1
return Math.max.apply(Math, values)
}
function formatUsagePercent(provider) {
var pct = usagePercent(provider)
return pct < 0 ? "—" : Math.round(pct * 100) + "%"
}
function weeklyUsage(provider) {
if (!provider) return ({ percent: -1, resetAt: "", label: "" })
if (root.rateLimitLabelIsWeekly(provider.rateLimitLabel))
return { percent: provider.rateLimitPercent, resetAt: provider.rateLimitResetAt, label: provider.rateLimitLabel }
if (root.rateLimitLabelIsWeekly(provider.secondaryRateLimitLabel))
return { percent: provider.secondaryRateLimitPercent, resetAt: provider.secondaryRateLimitResetAt, label: provider.secondaryRateLimitLabel }
return ({ percent: -1, resetAt: "", label: "" })
}
function paceInfo(provider) {
var weekly = weeklyUsage(provider)
if (weekly.percent < 0 || !weekly.resetAt) return ({ text: "", detail: "", deficit: false })
var reset = new Date(weekly.resetAt).getTime()
var now = Date.now()
var period = 7 * 24 * 60 * 60 * 1000
var remaining = reset - now
if (remaining <= 0 || remaining > period) return ({ text: "", detail: "", deficit: false })
var elapsed = period - remaining
var expected = root.clamp(elapsed / period, 0, 1)
var used = root.clamp(weekly.percent, 0, 1)
var diff = used - expected
var abs = Math.abs(diff)
var label = abs <= 0.02 ? "On pace" : (diff > 0 ? Math.round(abs * 100) + "% in deficit" : Math.round(abs * 100) + "% in reserve")
var projection = "Lasts until reset"
if (used > 0 && elapsed > 0) {
var eta = elapsed / used * (1 - used)
if (eta < remaining) projection = "Runs out in " + provider.formatResetTime(new Date(now + eta).toISOString())
}
return ({ text: label, detail: "Expected " + Math.round(expected * 100) + "% used · " + projection, deficit: diff > 0 && abs > 0.02 })
}
function tooltipText() {
if (providers.length === 0) return "Model Usage"
var lines = ["Model Usage"]
for (var i = 0; i < providers.length; i++) {
var provider = providers[i]
var line = provider.providerName + ": " + formatUsagePercent(provider) + " used"
var pace = paceInfo(provider)
if (pace.text !== "") line += " · " + pace.text
if (provider.syncEnabled && provider.syncDeviceCount > 1) line += " · " + provider.syncDeviceCount + " devices"
lines.push(line)
}
return lines.join("\n")
}
function syncSummary(provider) {
if (usageMain.syncStatusText !== "") return usageMain.syncStatusText
if (!provider || !provider.syncEnabled) return ""
var count = Number(provider.syncDeviceCount || 0)
if (count <= 0) return "Synced usage"
return "Synced from " + count + " device" + (count === 1 ? "" : "s")
}
function selectTab(index) {
if (providers.length === 0) {
selectedTabIndex = 0
return
}
selectedTabIndex = ((index % providers.length) + providers.length) % providers.length
}
function handleChipPress(index, button, target) {
if (root.bar && target) root.bar.hideTooltip(target)
if (button === Qt.RightButton) {
root.openSettings()
return
}
if (button === Qt.MiddleButton) {
root.triggerRefresh()
return
}
var wasOpen = root.popupOpen
var wasSelected = root.selectedTabIndex === index && !root.settingsMode
root.showUsage()
root.selectTab(index)
if (wasOpen && wasSelected) root.popupOpen = false
else {
root.popupOpen = true
root.triggerRefresh()
}
}
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
onPopupOpenChanged: {
if (popupOpen) {
usageMain.refreshAll()
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
onProvidersChanged: if (selectedTabIndex >= providers.length) selectTab(0)
Main {
id: usageMain
settings: root.settings
}
Timer {
id: refreshFlashTimer
interval: 900
repeat: false
onTriggered: root.refreshFlash = false
}
IpcHandler {
target: "omarchy.model-usage"
function open(): string { root.showUsage(); root.popupOpen = true; return "ok" }
function close(): string { root.close(); return "ok" }
function toggle(): string {
if (root.popupOpen) root.close()
else { root.showUsage(); root.popupOpen = true }
return "ok"
}
function refresh(): string { root.triggerRefresh(); return "ok" }
function settings(): string { root.openSettings(); return "ok" }
function openSettings(): string { root.openSettings(); return "ok" }
}
component UsageChip: Item {
id: chip
required property var modelData
required property int index
readonly property real pct: root.usagePercent(modelData)
readonly property bool compact: root.vertical
readonly property bool tooltipHovered: mouseArea.containsMouse
width: compact ? root.barSize : chipRow.implicitWidth
height: compact ? Math.max(root.barSize, chipColumn.implicitHeight + Style.space(2)) : root.barSize
Row {
id: chipRow
visible: !chip.compact
anchors.centerIn: parent
spacing: 4
Image {
source: root.iconSourceForProvider(chip.modelData, root.bar ? root.bar.background : Color.bar.background)
width: 13
height: 13
sourceSize.width: 13
sourceSize.height: 13
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
opacity: chip.pct >= 0.9 ? 0.75 : 1
}
Text {
text: root.formatUsagePercent(chip.modelData)
color: chip.pct >= 0.9 ? urgent : foreground
font.family: fontFamily
font.pixelSize: 10
font.bold: chip.pct >= 0.9
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
id: chipColumn
visible: chip.compact
anchors.centerIn: parent
spacing: 1
Image {
source: root.iconSourceForProvider(chip.modelData, root.bar ? root.bar.background : Color.bar.background)
width: 13
height: 13
sourceSize.width: 13
sourceSize.height: 13
fillMode: Image.PreserveAspectFit
anchors.horizontalCenter: parent.horizontalCenter
opacity: chip.pct >= 0.9 ? 0.75 : 1
}
Text {
width: root.barSize
text: root.formatUsagePercent(chip.modelData)
color: chip.pct >= 0.9 ? urgent : foreground
font.family: fontFamily
font.pixelSize: 10
font.bold: chip.pct >= 0.9
horizontalAlignment: Text.AlignHCenter
}
}
property var registeredBar: null
function triggerPress(button) {
root.handleChipPress(chip.index, button, chip)
}
function syncClickRegistration() {
if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(chip)
registeredBar = root.bar
if (registeredBar && registeredBar.registerClickTarget) registeredBar.registerClickTarget(chip)
}
Component.onCompleted: syncClickRegistration()
Component.onDestruction: if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(chip)
Connections {
target: root
function onBarChanged() { chip.syncClickRegistration() }
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: if (root.bar) root.bar.showTooltip(chip, root.tooltipText())
onExited: if (root.bar) root.bar.hideTooltip(chip)
onClicked: function(mouse) { root.handleChipPress(chip.index, mouse.button, chip) }
}
}
component EmptyUsageChip: Item {
id: emptyChip
readonly property bool tooltipHovered: emptyMouse.containsMouse
visible: providers.length === 0
width: root.vertical ? root.barSize : emptyLabel.implicitWidth
height: root.barSize
property var registeredBar: null
function triggerPress(button) {
root.handleChipPress(0, button, emptyChip)
}
function syncClickRegistration() {
if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(emptyChip)
registeredBar = root.bar
if (registeredBar && registeredBar.registerClickTarget) registeredBar.registerClickTarget(emptyChip)
}
Component.onCompleted: syncClickRegistration()
Component.onDestruction: if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(emptyChip)
Connections {
target: root
function onBarChanged() { emptyChip.syncClickRegistration() }
}
Text {
id: emptyLabel
anchors.centerIn: parent
text: "AI"
color: dim
font.family: fontFamily
font.pixelSize: 10
font.bold: true
}
MouseArea {
id: emptyMouse
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: if (root.bar) root.bar.showTooltip(emptyChip, "Model Usage")
onExited: if (root.bar) root.bar.hideTooltip(emptyChip)
onClicked: function(mouse) { root.handleChipPress(0, mouse.button, emptyChip) }
}
}
Item {
id: button
anchors.fill: parent
implicitWidth: root.vertical ? root.barSize : barRow.implicitWidth + Style.space(10)
implicitHeight: root.vertical ? barColumn.implicitHeight : root.barSize
Row {
id: barRow
visible: !root.vertical
anchors.centerIn: parent
spacing: Style.space(8)
Repeater {
model: providers
UsageChip { visible: !root.vertical }
}
EmptyUsageChip { visible: !root.vertical && providers.length === 0 }
}
Column {
id: barColumn
visible: root.vertical
anchors.centerIn: parent
spacing: Style.space(2)
Repeater {
model: providers
UsageChip { visible: root.vertical }
}
EmptyUsageChip { visible: root.vertical && providers.length === 0 }
}
}
KeyboardPanel {
id: panel
anchorItem: button
owner: root
bar: root.bar
open: root.popupOpen
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(370))
contentHeight: panel.fittedContentHeight(contentColumn.implicitHeight, Style.space(560))
PanelKeyCatcher {
id: keyCatcher
anchors.fill: parent
blocked: root.settingsMode && settingsContent.editorActive
onMoveRequested: function(dx, dy) {
if (root.settingsMode) {
if (dy !== 0) flick.contentY = root.clamp(flick.contentY + dy * 56, 0, Math.max(0, flick.contentHeight - flick.height))
return
}
if (dx !== 0) root.selectTab(root.selectedTabIndex + dx)
if (dy !== 0) flick.contentY = root.clamp(flick.contentY + dy * 56, 0, Math.max(0, flick.contentHeight - flick.height))
}
onCloseRequested: root.close()
onTextKey: function(t) {
if (t === "r" || t === "R") root.triggerRefresh()
if (t === "s" || t === "S") root.settingsMode ? root.saveSettings() : root.openSettings()
}
ColumnLayout {
anchors.fill: parent
spacing: 10
Header {
visible: !root.settingsMode && !!root.selectedProvider
provider: root.selectedProvider
}
SettingsHeader { visible: root.settingsMode }
PanelSeparator {
Layout.fillWidth: true
foreground: root.foreground
}
Item {
visible: !root.settingsMode && providers.length > 1
Layout.fillWidth: true
Layout.preferredHeight: 30
Row {
anchors.fill: parent
spacing: 6
Repeater {
model: providers
Button {
required property var modelData
required property int index
width: (parent.width - parent.spacing * Math.max(0, providers.length - 1)) / Math.max(1, providers.length)
height: parent.height
text: modelData.providerName
foreground: root.foreground
tooltipBackground: root.background
tooltipForeground: root.foreground
fontFamily: root.fontFamily
fontSize: 11
horizontalPadding: 8
verticalPadding: 5
active: index === root.selectedTabIndex
hasCursor: index === root.selectedTabIndex
onClicked: {
root.selectTab(index)
keyCatcher.forceActiveFocus()
}
}
}
}
}
Flickable {
id: flick
Layout.fillWidth: true
Layout.fillHeight: true
contentWidth: width
contentHeight: contentColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.VerticalFlick
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
ColumnLayout {
id: contentColumn
width: flick.width
spacing: 10
Text {
visible: !root.settingsMode && !root.selectedProvider
Layout.fillWidth: true
Layout.topMargin: 24
text: "No providers enabled. Open Settings to enable Claude or Codex."
color: dim
font.family: fontFamily
font.pixelSize: 11
horizontalAlignment: Text.AlignHCenter
}
StatusCard { provider: root.settingsMode ? null : root.selectedProvider }
RateLimitCard { provider: root.settingsMode ? null : root.selectedProvider }
TodayCard { provider: root.settingsMode ? null : root.selectedProvider }
WeekCard { provider: root.settingsMode ? null : root.selectedProvider }
AllTimeCard { provider: root.settingsMode ? null : root.selectedProvider }
UsageFooter { visible: !root.settingsMode }
SettingsContent {
id: settingsContent
visible: root.settingsMode
}
}
}
}
}
}
component SettingsHeader: RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: "Model Usage Settings"
color: foreground
font.family: fontFamily
font.pixelSize: 15
font.bold: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
Button {
text: "Usage"
foreground: root.foreground
tooltipText: "Back to usage"
tooltipBackground: root.background
tooltipForeground: root.foreground
fontFamily: root.fontFamily
fontSize: 10
horizontalPadding: 8
verticalPadding: 4
onClicked: root.showUsage()
}
Button {
text: "Save"
foreground: root.foreground
tooltipText: "Save settings"
tooltipBackground: root.background
tooltipForeground: root.foreground
fontFamily: root.fontFamily
fontSize: 10
horizontalPadding: 8
verticalPadding: 4
active: true
onClicked: root.saveSettings()
}
}
component UsageFooter: RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
Layout.fillWidth: true
text: {
var sync = root.syncSummary(root.selectedProvider)
return (sync !== "" ? sync + " · " : "") + "←/→ switch · j/k scroll · r refresh · s/settings · esc close"
}
color: dim
font.family: fontFamily
font.pixelSize: 10
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
}
component SettingsContent: ColumnLayout {
id: settingsRoot
Layout.fillWidth: true
spacing: 10
readonly property bool syncOn: root.draftValue("syncMode", "Off") === "On"
readonly property bool claudeOn: root.draftProviderValue("claude", "enabled", true) !== false
readonly property bool editorActive: refreshIntervalField.field.activeFocus
|| claudeStatsField.activeFocus
|| claudeCredentialsField.activeFocus
|| claudeProjectsField.activeFocus
|| syncDirField.activeFocus
|| syncFileNameField.activeFocus
|| syncDeviceIdField.activeFocus
SectionCard {
title: "Providers"
ColumnLayout {
width: parent.width
spacing: 10
Toggle {
Layout.fillWidth: true
label: "Claude Code"
description: checked ? "Show Claude Code usage and rate-limit status" : "Hidden from the bar widget"
checked: root.draftProviderValue("claude", "enabled", true) !== false
foreground: root.foreground
accent: Color.accent
fontFamily: root.fontFamily
onClicked: root.setDraftProviderValue("claude", "enabled", !checked)
}
ColumnLayout {
Layout.fillWidth: true
spacing: 6
enabled: settingsRoot.claudeOn
opacity: enabled ? 1.0 : 0.45
FieldLabel { text: "Claude stats cache" }
TextField {
id: claudeStatsField
Layout.fillWidth: true
text: String(root.draftProviderValue("claude", "statsPath", "~/.claude/stats-cache.json"))
placeholderText: "~/.claude/stats-cache.json"
foreground: root.foreground
onTextChanged: if (text !== root.draftProviderValue("claude", "statsPath", "")) root.setDraftProviderValue("claude", "statsPath", text)
}
FieldLabel { text: "Claude credentials" }
TextField {
id: claudeCredentialsField
Layout.fillWidth: true
text: String(root.draftProviderValue("claude", "credentialsPath", "~/.claude/.credentials.json"))
placeholderText: "~/.claude/.credentials.json"
foreground: root.foreground
onTextChanged: if (text !== root.draftProviderValue("claude", "credentialsPath", "")) root.setDraftProviderValue("claude", "credentialsPath", text)
}
FieldLabel { text: "Claude projects folder" }
TextField {
id: claudeProjectsField
Layout.fillWidth: true
text: String(root.draftProviderValue("claude", "projectsPath", "~/.claude/projects"))
placeholderText: "~/.claude/projects"
foreground: root.foreground
onTextChanged: if (text !== root.draftProviderValue("claude", "projectsPath", "")) root.setDraftProviderValue("claude", "projectsPath", text)
}
}
Toggle {
Layout.fillWidth: true
label: "Codex"
description: checked ? "Show Codex usage and limits" : "Hidden from the bar widget"
checked: root.draftProviderValue("codex", "enabled", true) !== false
foreground: root.foreground
accent: Color.accent
fontFamily: root.fontFamily
onClicked: root.setDraftProviderValue("codex", "enabled", !checked)
}
}
}
SectionCard {
title: "Refresh"
ColumnLayout {
width: parent.width
spacing: 8
NumberField {
id: refreshIntervalField
label: "Refresh interval (seconds)"
value: Number(root.draftValue("refreshIntervalSec", 900))
from: 30
to: 3600
stepSize: 30
fieldWidth: parent.width
foreground: root.foreground
accent: Color.accent
fontFamily: root.fontFamily
onModified: function(value) { root.setDraftValue("refreshIntervalSec", value) }
}
Text {
Layout.fillWidth: true
text: "How often the widget refreshes local usage scans and sync snapshots."
color: dim
font.family: fontFamily
font.pixelSize: 10
wrapMode: Text.WordWrap
}
}
}
SectionCard {
title: "Synced aggregation"
ColumnLayout {
width: parent.width
spacing: 10
Toggle {
Layout.fillWidth: true
label: "Aggregate across devices"
description: checked ? "Write this machine's snapshot and merge every *.json file in the sync folder" : "Only show this machine's local usage"
checked: settingsRoot.syncOn
foreground: root.foreground
accent: Color.accent
fontFamily: root.fontFamily
onClicked: root.setDraftValue("syncMode", checked ? "Off" : "On")
}
ColumnLayout {
Layout.fillWidth: true
spacing: 6
enabled: settingsRoot.syncOn
opacity: enabled ? 1.0 : 0.45
FieldLabel { text: "Sync folder" }
TextField {
id: syncDirField
Layout.fillWidth: true
text: String(root.draftValue("syncDir", ""))
placeholderText: "~/Sync/model-usage"
foreground: root.foreground
onTextChanged: if (text !== root.draftValue("syncDir", "")) root.setDraftValue("syncDir", text)
}
FieldLabel { text: "Snapshot file name" }
TextField {
id: syncFileNameField
Layout.fillWidth: true
text: String(root.draftValue("syncFileName", ""))
placeholderText: "Defaults to <hostname>.json"
foreground: root.foreground
onTextChanged: if (text !== root.draftValue("syncFileName", "")) root.setDraftValue("syncFileName", text)
}
FieldLabel { text: "Device id" }
TextField {
id: syncDeviceIdField
Layout.fillWidth: true
text: String(root.draftValue("syncDeviceId", ""))
placeholderText: "Optional stable name for this machine"
foreground: root.foreground
onTextChanged: if (text !== root.draftValue("syncDeviceId", "")) root.setDraftValue("syncDeviceId", text)
}
}
}
}
Text {
visible: root.settingsStatusText !== ""
Layout.fillWidth: true
text: root.settingsStatusText
color: dim
font.family: fontFamily
font.pixelSize: 10
horizontalAlignment: Text.AlignHCenter
}
Text {
Layout.fillWidth: true
text: "s saves · esc closes"
color: dim
font.family: fontFamily
font.pixelSize: 10
horizontalAlignment: Text.AlignHCenter
}
}
component FieldLabel: Text {
color: dim
font.family: fontFamily
font.pixelSize: 10
font.bold: true
}
component Header: RowLayout {
property var provider: null
visible: !!provider
Layout.fillWidth: true
spacing: 8
Image {
source: root.iconSourceForProvider(provider, root.background)
Layout.preferredWidth: 16
Layout.preferredHeight: 16
sourceSize.width: 16
sourceSize.height: 16
fillMode: Image.PreserveAspectFit
Layout.alignment: Qt.AlignVCenter
}
Text {
text: provider ? provider.providerName + " Usage" : ""
color: foreground
font.family: fontFamily
font.pixelSize: 15
font.bold: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
Button {
visible: provider && String(provider.tierLabel || "") !== ""
text: provider ? provider.tierLabel : ""
foreground: root.foreground
tooltipBackground: root.background
tooltipForeground: root.foreground
fontFamily: root.fontFamily
fontSize: 10
horizontalPadding: 6
verticalPadding: 3
active: true
enabled: false
}
Button {
text: (root.refreshFlash || usageMain.refreshing) ? "Refreshing…" : "Refresh"
foreground: root.foreground
tooltipText: (root.refreshFlash || usageMain.refreshing) ? "Refreshing usage…" : "Refresh usage"
tooltipBackground: root.background
tooltipForeground: root.foreground
fontFamily: root.fontFamily
fontSize: 10
horizontalPadding: 8
verticalPadding: 4
active: root.refreshFlash || usageMain.refreshing
onClicked: {
root.triggerRefresh()
keyCatcher.forceActiveFocus()
}
}
}
component StatusCard: SectionCard {
property var provider: null
visible: !!provider && String(provider.usageStatusText || "") !== ""
titleColor: urgent
title: provider ? provider.usageStatusText : ""
subtitle: provider ? provider.authHelpText : ""
}
component RateLimitCard: SectionCard {
id: rateLimitCard
property var provider: null
visible: !!provider && ((provider.rateLimitPercent >= 0) || (provider.secondaryRateLimitPercent >= 0))
title: "Rate Limit Usage"
ColumnLayout {
width: parent.width
spacing: 10
ProgressRow {
visible: provider && provider.rateLimitPercent >= 0
label: provider ? provider.rateLimitLabel : ""
value: provider ? provider.rateLimitPercent : -1
resetText: provider && provider.rateLimitResetAt ? "Resets in " + provider.formatResetTime(provider.rateLimitResetAt) : ""
}
ProgressRow {
visible: provider && provider.secondaryRateLimitPercent >= 0
label: provider ? provider.secondaryRateLimitLabel : ""
value: provider ? provider.secondaryRateLimitPercent : -1
resetText: provider && provider.secondaryRateLimitResetAt ? "Resets in " + provider.formatResetTime(provider.secondaryRateLimitResetAt) : ""
}
PaceRow { provider: rateLimitCard.provider }
}
}
component TodayCard: SectionCard {
property var provider: null
visible: !!provider && provider.ready && provider.hasLocalStats
title: "Today"
ColumnLayout {
width: parent.width
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 20
StatBlock { value: provider ? String(provider.todayPrompts || 0) : "0"; label: "prompts" }
StatBlock { value: provider ? String(provider.todaySessions || 0) : "0"; label: "sessions" }
}
Repeater {
model: {
var toks = provider ? (provider.todayTokensByModel || {}) : {}
var out = []
for (var k in toks) out.push({ modelId: k, count: toks[k] })
return out
}
delegate: RowLayout {
required property var modelData
Layout.fillWidth: true
Text { text: usageMain.friendlyModelName(modelData.modelId); color: dim; font.family: fontFamily; font.pixelSize: 11 }
Item { Layout.fillWidth: true }
Text { text: usageMain.formatTokenCount(modelData.count) + " tokens"; color: foreground; font.family: fontFamily; font.pixelSize: 11; font.bold: true }
}
}
}
}
component WeekCard: SectionCard {
property var provider: null
visible: !!provider && provider.recentDays && provider.recentDays.length > 0
title: "Last 7 Days"
ColumnLayout {
width: parent.width
spacing: 6
Repeater {
model: provider ? provider.recentDays : []
delegate: RowLayout {
required property var modelData
Layout.fillWidth: true
spacing: 8
readonly property real count: modelData ? Number(modelData.messageCount || 0) : 0
readonly property real maxCount: {
var days = provider ? (provider.recentDays || []) : []
var max = 1
for (var i = 0; i < days.length; i++) if (Number(days[i].messageCount || 0) > max) max = Number(days[i].messageCount || 0)
return max
}
Text {
text: {
var d = modelData.date
if (!d) return ""
var dt = new Date(d + "T00:00:00")
var names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
return names[dt.getDay()] + " " + String(dt.getMonth() + 1).padStart(2, "0") + "/" + String(dt.getDate()).padStart(2, "0")
}
color: dim
font.family: fontFamily
font.pixelSize: 10
Layout.preferredWidth: 48
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 10
color: track
radius: Math.max(1, Style.cornerRadius / 3)
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * (count / maxCount)
color: root.alpha(foreground, 0.78)
radius: Math.max(1, Style.cornerRadius / 3)
Behavior on width { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
}
}
Text {
text: usageMain.formatTokenCount(count)
color: foreground
font.family: fontFamily
font.pixelSize: 10
font.bold: true
horizontalAlignment: Text.AlignRight
Layout.preferredWidth: 48
}
}
}
}
}
component AllTimeCard: SectionCard {
property var provider: null
visible: {
var usage = provider ? (provider.modelUsage || {}) : {}
return Object.keys(usage).length > 0
}
title: "All-Time"
ColumnLayout {
width: parent.width
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 20
StatBlock { value: provider ? usageMain.formatTokenCount(provider.totalPrompts || 0) : "0"; label: "messages" }
StatBlock { value: provider ? String(provider.totalSessions || 0) : "0"; label: "sessions" }
}
PanelSeparator { Layout.fillWidth: true; foreground: root.foreground; strength: 0.18 }
Repeater {
model: {
var usage = provider ? (provider.modelUsage || {}) : {}
var out = []
for (var k in usage) out.push({ modelId: k, data: usage[k] })
return out
}
delegate: ColumnLayout {
required property var modelData
Layout.fillWidth: true
spacing: 4
Text { text: usageMain.friendlyModelName(modelData.modelId); color: foreground; font.family: fontFamily; font.pixelSize: 11; font.bold: true }
GridLayout {
Layout.leftMargin: 10
columns: 2
columnSpacing: 18
rowSpacing: 2
DetailPair { name: "Input"; value: usageMain.formatTokenCount(modelData.data.inputTokens || 0) }
DetailPair { name: "Output"; value: usageMain.formatTokenCount(modelData.data.outputTokens || 0) }
DetailPair { name: "Cache Read"; value: usageMain.formatTokenCount(modelData.data.cacheReadInputTokens || 0) }
DetailPair { name: "Cache Write"; value: usageMain.formatTokenCount(modelData.data.cacheCreationInputTokens || 0) }
}
}
}
}
}
component SectionCard: BorderSurface {
id: section
property string title: ""
property string subtitle: ""
property color titleColor: foreground
default property alias content: body.data
Layout.fillWidth: true
color: card
borderSpec: Border.flat(Qt.rgba(foreground.r, foreground.g, foreground.b, 0.05), 1)
padding: 12
radius: Style.cornerRadius
implicitHeight: body.implicitHeight + contentTopInset + contentBottomInset
ColumnLayout {
id: body
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: section.contentTopInset
anchors.rightMargin: section.contentRightInset
anchors.bottomMargin: section.contentBottomInset
anchors.leftMargin: section.contentLeftInset
spacing: 8
PanelSectionHeader {
visible: section.title !== ""
Layout.fillWidth: true
text: section.title
foreground: section.titleColor
fontFamily: root.fontFamily
fontSize: 11
}
Text {
visible: section.subtitle !== ""
Layout.fillWidth: true
text: section.subtitle
color: dim
font.family: fontFamily
font.pixelSize: 10
wrapMode: Text.WordWrap
}
}
}
component PaceRow: ColumnLayout {
property var provider: null
readonly property var pace: root.paceInfo(provider)
visible: pace.text !== ""
spacing: 2
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
Text {
text: "Pace"
color: dim
font.family: fontFamily
font.pixelSize: 10
}
Item { Layout.fillWidth: true }
Text {
text: pace.text
color: pace.deficit ? urgent : foreground
font.family: fontFamily
font.pixelSize: 10
font.bold: true
}
}
Text {
Layout.fillWidth: true
text: pace.detail
color: dim
font.family: fontFamily
font.pixelSize: 10
horizontalAlignment: Text.AlignRight
}
}
component ProgressRow: ColumnLayout {
property string label: ""
property real value: -1
property string resetText: ""
spacing: 5
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
Text { text: label; color: dim; font.family: fontFamily; font.pixelSize: 11 }
Item { Layout.fillWidth: true }
Text {
text: value < 0 ? "—" : Math.round(value * 100) + "%"
color: value >= 0.9 ? urgent : foreground
font.family: fontFamily
font.pixelSize: 11
font.bold: true
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 8
color: track
radius: Math.max(1, Style.cornerRadius / 3)
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * root.clamp(value, 0, 1)
color: value >= 0.9 ? root.alpha(urgent, 0.72) : root.alpha(foreground, 0.78)
radius: Math.max(1, Style.cornerRadius / 3)
Behavior on width { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
}
}
Text { visible: resetText !== ""; text: resetText; color: dim; font.family: fontFamily; font.pixelSize: 10 }
}
component StatBlock: ColumnLayout {
property string value: "0"
property string label: ""
spacing: 2
Text { text: value; color: foreground; font.family: fontFamily; font.pixelSize: 18; font.bold: true }
Text { text: label; color: dim; font.family: fontFamily; font.pixelSize: 10 }
}
component DetailPair: RowLayout {
property string name: ""
property string value: ""
Text { text: name; color: dim; font.family: fontFamily; font.pixelSize: 10; Layout.preferredWidth: 76 }
Text { text: value; color: foreground; font.family: fontFamily; font.pixelSize: 10; font.bold: true }
}
}