diff --git a/.github/actions/build-linux-desktop/action.yml b/.github/actions/build-linux-desktop/action.yml
index 3d1b38cd54..60ca88e56f 100644
--- a/.github/actions/build-linux-desktop/action.yml
+++ b/.github/actions/build-linux-desktop/action.yml
@@ -25,7 +25,7 @@ runs:
shell: bash
run: |
sudo apt-get update
- sudo apt-get install -y qt6-base-dev qt6-declarative-dev qt6-wayland libqt6svg6 qml6-module-qtquick qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-window qml6-module-qtquick-templates qml6-module-qtqml-workerscript
+ sudo apt-get install -y qt6-base-dev qt6-declarative-dev qt6-svg-dev qt6-wayland libqt6svg6 qml6-module-qtquick qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-window qml6-module-qtquick-templates qml6-module-qtqml-workerscript
- name: Build and test
shell: bash
env:
diff --git a/Integrations/Linux/DesktopController.cpp b/Integrations/Linux/DesktopController.cpp
index 018bfce0d0..e4791a6037 100644
--- a/Integrations/Linux/DesktopController.cpp
+++ b/Integrations/Linux/DesktopController.cpp
@@ -98,7 +98,7 @@ bool DesktopController::validate(QVariantMap &values) {
!QStringList{"meters", "icon"}.contains(values.value("trayStyle").toString())) {
m_configError = "Unsupported display preference."; return false;
}
- for (const auto &key : {"allAccounts", "showIdentity", "showCosts", "showStatus", "notifications", "showTray", "refreshOnOpen", "showPace", "warningColors", "followOmarchyTheme"})
+ for (const auto &key : {"allAccounts", "showIdentity", "showCosts", "showStatus", "notifications", "showTray", "compactQuickView", "refreshOnOpen", "showPace", "warningColors", "followOmarchyTheme"})
values[key] = values.value(key).toBool();
return true;
}
@@ -106,7 +106,7 @@ bool DesktopController::validate(QVariantMap &values) {
void DesktopController::loadSettings(const QString &cliOverride) {
m_settings = {{"executable", "codexbar"}, {"provider", "codex"}, {"source", "auto"},
{"refreshSeconds", 300}, {"accountIndex", 0}, {"notifyThreshold", 10}, {"allAccounts", false},
- {"showIdentity", false}, {"showCosts", true}, {"showStatus", true}, {"notifications", false}, {"showTray", true}, {"refreshOnOpen", false}, {"providerOrder", QStringList{}},
+ {"showIdentity", false}, {"showCosts", true}, {"showStatus", true}, {"notifications", false}, {"showTray", true}, {"compactQuickView", false}, {"refreshOnOpen", false}, {"providerOrder", QStringList{}},
{"quotaDisplay", "remaining"}, {"resetDisplay", "countdown"}, {"showPace", true},
{"warningColors", true}, {"trayStyle", "meters"}, {"followOmarchyTheme", false}};
m_configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/codexbar/linux.json";
@@ -288,7 +288,9 @@ void DesktopController::copySummary() {
void DesktopController::showWindow(const QString &page) {
if (page == "spending" && QDateTime::currentMSecsSinceEpoch() - m_costUpdated > 300000) refreshCosts();
- if (page == "usage" && m_settings.value("refreshOnOpen").toBool()) refresh();
+ if (page == "quick-view" && m_settings.value("showCosts").toBool() &&
+ QDateTime::currentMSecsSinceEpoch() - m_costUpdated > 300000) refreshCosts();
+ if ((page == "usage" || page == "dashboard" || page == "quick-view") && m_settings.value("refreshOnOpen").toBool()) refresh();
emit windowRequested(page);
}
@@ -356,7 +358,7 @@ bool DesktopController::listen(const QString &socketPath) {
response = {{"ok", ok}, {"enabled", launchAtLogin()}};
}
else if (command == "refresh") { refresh(); refreshCosts(); }
- else if (command == "settings" || command == "usage" || command == "spending") showWindow(command);
+ else if (command == "settings" || command == "usage" || command == "dashboard" || command == "spending" || command == "quick-view") showWindow(command);
else if (command == "configure" && request.value("settings").isObject()) {
const bool ok = saveSettings(request.value("settings").toObject().toVariantMap());
response = {{"ok", ok}, {"error", m_configError}};
diff --git a/Integrations/Linux/README.md b/Integrations/Linux/README.md
index 5fa4672753..2ea87e74e7 100644
--- a/Integrations/Linux/README.md
+++ b/Integrations/Linux/README.md
@@ -1,7 +1,7 @@
# CodexBar for Linux
-A Qt 6 desktop app with separate Usage & Spend and Settings windows, an optional
-system tray icon, and a launcher entry. The Swift `codexbar` CLI owns provider
+A Qt 6 desktop app with an opt-in compact Quick View, separate Usage & Spend and Settings
+windows, an optional system tray icon, and a launcher entry. The Swift `codexbar` CLI owns provider
fetching and authentication. The desktop owns polling, settings, notifications,
and a private local socket for desktop adapters. No HTTP server is needed.
@@ -185,8 +185,14 @@ The app does not read terminal output or store credentials. Finish the flow, the
refresh usage. These controls manage the active CLI session; browser imports,
token-account editing and Mac managed profiles are not implemented here.
-Usage displays used or remaining quota, reset times, pace, credits, status, generic provider
-details, and charts. Unknown values stay unknown. Identity is hidden by default. Display preferences control reset countdowns,
+Ordinary launch and `--usage` open the full Usage & Spend window. Quick View opens
+with `--quick-view`, from the tray menu, or by clicking the tray icon after enabling
+"Use compact Quick View from the tray" in Settings (off by default). It presents
+an Overview and provider tabs with the same provider icons used by the macOS menu, compact quota meters,
+reset times, pace, and optional local cost. The footer opens the full Usage & Spend
+window, Settings, About, or Quit. `--dashboard` also opens the full Usage & Spend window
+directly; `--spending` opens its Spending tab. The full Usage & Spend window retains
+generic provider details and charts. Unknown values stay unknown. Identity is hidden by default. Display preferences control reset countdowns,
absolute times, pace visibility, and low-quota colors. The tray can show two quota
meters for the first displayed provider or a static icon. Unknown meters remain
empty tracks. The tooltip identifies the displayed providers and stale data.
@@ -203,7 +209,7 @@ are not invoices. Opening spending scans independently of quota polling, with a
five-minute cache; Refresh forces a new scan.
Quota polling defaults to five minutes. Optional refresh-on-open updates usage
-when its window opens. Refresh and Ctrl+R update the selected tab independently;
+when Quick View or the full usage window opens. Refresh and Ctrl+R update the selected tab independently;
Ctrl+, opens Settings, and Ctrl+Q quits. Queries never overlap within each stream,
stop after 60 seconds, and cap output at 8 MiB. Failed refreshes retain previous
results with a stale indicator. Changing selection rejects old in-flight results.
@@ -211,7 +217,7 @@ Optional notifications use the desktop's D-Bus notification service for remainin
quota threshold crossings, observed resets, and service-status transitions.
Startup, provider errors, and ambiguous multi-account results stay silent.
-Closing a window leaves the backend running. Quit from the usage window or tray,
+Closing a window leaves the backend running. Quit from Quick View or the tray,
or use `codexbar-linux --quit`. Launching again opens the existing process.
Preferences live in `$XDG_CONFIG_HOME/codexbar/linux.json` (normally `~/.config`),
written atomically with user-only permissions. Invalid files are never overwritten:
@@ -222,6 +228,8 @@ fix or remove the file and restart. Authentication remains in the CLI's stores.
```sh
codexbar-linux --background
codexbar-linux --usage
+codexbar-linux --quick-view
+codexbar-linux --dashboard
codexbar-linux --settings
codexbar-linux --spending
codexbar-linux --refresh
diff --git a/Integrations/Linux/codexbar-linux.pro b/Integrations/Linux/codexbar-linux.pro
index eadc7a5083..b1daba0cd9 100644
--- a/Integrations/Linux/codexbar-linux.pro
+++ b/Integrations/Linux/codexbar-linux.pro
@@ -1,10 +1,10 @@
-QT += core gui widgets quick qml quickcontrols2 network dbus
+QT += core gui widgets quick qml quickcontrols2 network dbus svg
CONFIG += c++17 console
CONFIG -= app_bundle
TARGET = codexbar-linux
SOURCES += main.cpp DesktopController.cpp
HEADERS += DesktopController.h
-RESOURCES += desktop.qrc
+RESOURCES += desktop.qrc provider-icons.qrc
QMAKE_CXXFLAGS += -Wall -Wextra
target.path = $$PREFIX/bin
isEmpty(PREFIX): target.path = /usr/local/bin
diff --git a/Integrations/Linux/desktop.qrc b/Integrations/Linux/desktop.qrc
index 7fbc924eaa..003659babb 100644
--- a/Integrations/Linux/desktop.qrc
+++ b/Integrations/Linux/desktop.qrc
@@ -2,6 +2,8 @@
Shared/Usage.js
Shared/Notifications.js
qml/Main.qml
+ qml/QuickView.qml
+ qml/MenuMetric.qml
qml/Dashboard.qml
qml/SettingsWindow.qml
qml/UsageCard.qml
diff --git a/Integrations/Linux/main.cpp b/Integrations/Linux/main.cpp
index 3f3d1e0a71..c696339254 100644
--- a/Integrations/Linux/main.cpp
+++ b/Integrations/Linux/main.cpp
@@ -1,12 +1,14 @@
#include "DesktopController.h"
#include
+#include
#include
#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -15,13 +17,41 @@
#include
#include
#include
+#include
#include
#include
+#include
#include
#include
#include
#include
+// The shared provider SVGs are white template assets on macOS. Tint them using the
+// active Qt palette so they stay legible in both light and dark Linux appearances.
+class ProviderIconProvider final : public QQuickImageProvider {
+public:
+ ProviderIconProvider() : QQuickImageProvider(QQuickImageProvider::Image) {}
+
+ QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override {
+ const auto provider = id.section('?', 0, 0);
+ if (!QRegularExpression("^[a-z0-9-]{1,80}$").match(provider).hasMatch()) return {};
+ QFile icon(":/logos/" + provider + ".svg");
+ if (!icon.open(QIODevice::ReadOnly)) return {};
+ QSvgRenderer svg(icon.readAll());
+ if (!svg.isValid()) return {};
+ const auto color = QColor("#" + id.section("color=", 1, 1).section('&', 0, 0));
+ const auto dimensions = requestedSize.isValid() ? requestedSize : QSize(36, 36);
+ QImage image(dimensions, QImage::Format_ARGB32_Premultiplied);
+ image.fill(Qt::transparent);
+ QPainter painter(&image);
+ svg.render(&painter);
+ painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
+ painter.fillRect(image.rect(), color.isValid() ? color : QColor(Qt::black));
+ if (size) *size = dimensions;
+ return image;
+ }
+};
+
static QByteArray request(const QString &socketPath, const QJsonObject &message) {
QLocalSocket socket;
socket.connectToServer(socketPath);
@@ -49,14 +79,14 @@ int main(int argc, char **argv) {
QCommandLineParser parser;
parser.setApplicationDescription("CodexBar desktop for Linux · Qt windows and optional tray");
parser.addHelpOption(); parser.addVersionOption();
- for (const auto &name : {"snapshot", "refresh", "settings", "spending", "usage", "quit", "background", "no-tray"})
+ for (const auto &name : {"snapshot", "refresh", "settings", "spending", "dashboard", "usage", "quick-view", "quit", "background", "no-tray"})
parser.addOption(QCommandLineOption(name, QString("%1 the running desktop app").arg(name)));
parser.addOption(QCommandLineOption("autostart", "Set login startup: enable, disable, status", "action"));
parser.addOption(QCommandLineOption("configure", "Update desktop settings through local IPC", "json"));
parser.addOption(QCommandLineOption("cli", "CodexBar CLI executable for a new instance", "path"));
parser.process(*application);
QString command = "usage";
- for (const auto &name : {"background", "usage", "settings", "spending", "refresh", "snapshot", "quit", "configure", "autostart"})
+ for (const auto &name : {"background", "usage", "quick-view", "settings", "spending", "dashboard", "refresh", "snapshot", "quit", "configure", "autostart"})
if (parser.isSet(name)) command = name;
const bool clientOnly = QStringList{"snapshot", "refresh", "quit", "configure", "autostart"}.contains(command);
const bool noTray = parser.isSet("no-tray");
@@ -139,20 +169,23 @@ int main(int argc, char **argv) {
QObject::connect(&controller, &DesktopController::settingsChanged, &app, updateTheme);
themeTimer.start(10000); updateTheme();
QQmlApplicationEngine engine;
+ engine.addImageProvider("provider-icon", new ProviderIconProvider);
QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QCoreApplication::quit);
engine.rootContext()->setContextProperty("desktop", &controller);
engine.load(QUrl("qrc:/qml/Main.qml"));
if (engine.rootObjects().isEmpty()) return 1;
QSystemTrayIcon tray(QIcon(":/icon.svg"));
QMenu menu;
- menu.addAction("Usage & Spend…", &controller, [&controller] { controller.showWindow("usage"); });
+ menu.addAction("Quick View", &controller, [&controller] { controller.showWindow("quick-view"); });
+ menu.addAction("Usage & Spend…", &controller, [&controller] { controller.showWindow("dashboard"); });
menu.addAction("Settings…", &controller, [&controller] { controller.showWindow("settings"); });
menu.addSeparator();
menu.addAction("Refresh", &controller, [&controller] { controller.refresh(); controller.refreshCosts(); });
menu.addAction("Quit CodexBar", &app, &QCoreApplication::quit);
tray.setContextMenu(&menu);
QObject::connect(&tray, &QSystemTrayIcon::activated, &controller, [&controller](QSystemTrayIcon::ActivationReason reason) {
- if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick) controller.showWindow("usage");
+ if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick)
+ controller.showWindow(controller.settings().value("compactQuickView").toBool() ? "quick-view" : "usage");
});
auto updateTray = [&] {
tray.setVisible(!noTray && controller.settings().value("showTray").toBool());
diff --git a/Integrations/Linux/provider-icons.qrc b/Integrations/Linux/provider-icons.qrc
new file mode 100644
index 0000000000..ddf86a758b
--- /dev/null
+++ b/Integrations/Linux/provider-icons.qrc
@@ -0,0 +1,84 @@
+
+
+ ../../Sources/CodexBar/Resources/ProviderIcon-abacus.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-aiand.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-alibaba.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-amp.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-antigravity.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-atlascloud.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-augment.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-bedrock.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-bifrost.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-chutes.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-claude.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-clawrouter.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-clinepass.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-codebuff.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-coderabbit.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-codex.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-commandcode.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-copilot.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-cursor.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-deepgram.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-deepinfra.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-deepseek.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-devin.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-devpass.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-doubao.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-elevenlabs.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-factory.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-fireworks.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-gemini.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-gitkraken.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-grok.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-groq.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-helmcode.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-huggingface.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-hyper.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-ibmbob.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-jetbrains.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-kilo.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-kimi.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-kiro.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-litellm.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-llmman.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-llmproxy.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-longcat.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-manus.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-mimo.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-minimax.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-mistral.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-muse.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-neuralwatt.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-notion.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-nous.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-ollama.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-opencode.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-opencodego.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-openrouter.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-perplexity.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-pi.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-poe.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-qoder.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-qwencloud.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-replicate.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-sakana.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-stepfun.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-sub2api.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-synthetic.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-t3chat.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-typesafe.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-v0.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-venice.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-vercel.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-vertexai.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-warp.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-wayfinder.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-windsurf.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-xai.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-zai.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-zed.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-zenmux.svg
+ ../../Sources/CodexBar/Resources/ProviderIcon-zoommate.svg
+
+
diff --git a/Integrations/Linux/qml/Dashboard.qml b/Integrations/Linux/qml/Dashboard.qml
index 9c6c3f50c0..d5b31dbaf6 100644
--- a/Integrations/Linux/qml/Dashboard.qml
+++ b/Integrations/Linux/qml/Dashboard.qml
@@ -43,7 +43,7 @@ ApplicationWindow {
TabBar {
Layout.fillWidth: true
currentIndex: window.selectedTab
- TabButton { text: "Usage"; onClicked: { window.selectedTab = 0; desktop.showWindow("usage"); } }
+ TabButton { text: "Usage"; onClicked: { window.selectedTab = 0; desktop.showWindow("dashboard"); } }
TabButton { text: "Spending"; onClicked: { window.selectedTab = 1; desktop.showWindow("spending"); } }
}
Label {
diff --git a/Integrations/Linux/qml/Main.qml b/Integrations/Linux/qml/Main.qml
index dc0d86c46f..041fb34123 100644
--- a/Integrations/Linux/qml/Main.qml
+++ b/Integrations/Linux/qml/Main.qml
@@ -2,13 +2,16 @@ import QtQuick
QtObject {
id: root
+ property QuickView quickView: QuickView {}
property Dashboard dashboard: Dashboard {}
property SettingsWindow preferences: SettingsWindow {}
property Connections routing: Connections {
target: desktop
function onWindowRequested(page) {
- var target = page === "settings" ? root.preferences : root.dashboard;
- if (page !== "settings") root.dashboard.selectedTab = page === "spending" ? 1 : 0;
+ var target = page === "settings" ? root.preferences :
+ page === "quick-view" ? root.quickView : root.dashboard;
+ if (page === "usage" || page === "dashboard" || page === "spending")
+ root.dashboard.selectedTab = page === "spending" ? 1 : 0;
target.show();
target.raise();
target.requestActivate();
diff --git a/Integrations/Linux/qml/MenuMetric.qml b/Integrations/Linux/qml/MenuMetric.qml
new file mode 100644
index 0000000000..f6fbf2b928
--- /dev/null
+++ b/Integrations/Linux/qml/MenuMetric.qml
@@ -0,0 +1,84 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import "../Shared/Usage.js" as Usage
+
+ColumnLayout {
+ id: root
+ required property var metric
+ property string quotaDisplay: "used"
+ property string resetDisplay: "countdown"
+ property bool showPace: true
+ property bool warningColors: true
+ property int notifyThreshold: 20
+ property color accent
+ property color primaryText
+ property color secondaryText
+ property color trackColor
+ readonly property int amount: Usage.quotaValue(metric.remaining, quotaDisplay)
+ readonly property bool warning: warningColors && metric.remaining <= notifyThreshold
+ spacing: 6
+ Layout.fillWidth: true
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 8
+ Label {
+ text: root.metric.label
+ color: root.primaryText
+ font.pixelSize: 15
+ font.weight: Font.DemiBold
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ textFormat: Text.PlainText
+ }
+ Label {
+ text: Usage.resetText(root.metric.resetsAt, clock.now, root.resetDisplay)
+ color: root.secondaryText
+ font.pixelSize: 12
+ horizontalAlignment: Text.AlignRight
+ wrapMode: Text.Wrap
+ Layout.maximumWidth: root.width * 0.48
+ textFormat: Text.PlainText
+ }
+ }
+ Rectangle {
+ id: track
+ Layout.fillWidth: true
+ height: 7
+ radius: height / 2
+ color: root.trackColor
+ Accessible.role: Accessible.ProgressBar
+ Accessible.name: root.metric.label + ": " + root.amount + "% " +
+ (root.quotaDisplay === "used" ? "used" : "left")
+ Rectangle {
+ width: track.width * Math.max(0, Math.min(100, root.amount)) / 100
+ height: parent.height
+ radius: parent.radius
+ color: root.warning ? "#d9874a" : root.accent
+ }
+ }
+ Label {
+ Layout.fillWidth: true
+ text: root.amount + "% " + (root.quotaDisplay === "used" ? "used" : "left")
+ color: root.primaryText
+ font.pixelSize: 13
+ }
+ Label {
+ Layout.fillWidth: true
+ visible: root.showPace && text !== ""
+ text: root.metric.pace || ""
+ color: root.secondaryText
+ font.pixelSize: 12
+ wrapMode: Text.Wrap
+ textFormat: Text.PlainText
+ }
+ Timer {
+ id: clock
+ property double now: Date.now()
+ interval: 30000
+ running: root.visible
+ repeat: true
+ onTriggered: now = Date.now()
+ }
+}
diff --git a/Integrations/Linux/qml/QuickView.qml b/Integrations/Linux/qml/QuickView.qml
new file mode 100644
index 0000000000..f1e2c9817a
--- /dev/null
+++ b/Integrations/Linux/qml/QuickView.qml
@@ -0,0 +1,443 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import "../Shared/Usage.js" as Usage
+
+ApplicationWindow {
+ id: window
+ title: "CodexBar"
+ width: 420
+ height: selectedIndex === -1 ? Math.min(650, 405 + desktop.entries.length * 54) : 680
+ minimumWidth: 360; minimumHeight: 400
+ property int selectedIndex: 0 // -1 is Overview; entries may include multiple accounts.
+ readonly property var selectedEntry: selectedIndex >= 0 && selectedIndex < desktop.entries.length ?
+ desktop.entries[selectedIndex] : null
+ readonly property var selectedCost: selectedEntry ? costFor(selectedEntry.provider) : null
+ readonly property color primaryText: nativePalette.windowText
+ readonly property color secondaryText: Qt.rgba(primaryText.r, primaryText.g, primaryText.b, 0.65)
+ readonly property color accent: nativePalette.highlight
+ readonly property color meterColor: "#c98757"
+ readonly property color trackColor: Qt.rgba(primaryText.r, primaryText.g, primaryText.b, 0.14)
+ readonly property color dividerColor: Qt.rgba(primaryText.r, primaryText.g, primaryText.b, 0.17)
+ readonly property color hoverColor: Qt.rgba(primaryText.r, primaryText.g, primaryText.b, 0.08)
+
+ function costFor(provider) {
+ for (var i = 0; i < desktop.spending.length; ++i) {
+ if (desktop.spending[i].provider === provider) return desktop.spending[i];
+ }
+ return null;
+ }
+ function openPage(page) { hide(); desktop.showWindow(page); }
+
+ onClosing: function(event) { event.accepted = false; hide(); }
+ Shortcut { sequence: "Ctrl+R"; onActivated: desktop.refresh() }
+ Shortcut { sequence: "Ctrl+,"; onActivated: window.openPage("settings") }
+ Shortcut { sequence: "Ctrl+Q"; onActivated: Qt.quit() }
+ Shortcut { sequence: "Escape"; onActivated: window.hide() }
+ Connections {
+ target: desktop
+ function onChanged() {
+ if (desktop.entries.length && window.selectedIndex >= desktop.entries.length)
+ window.selectedIndex = 0;
+ }
+ }
+ SystemPalette { id: nativePalette }
+ background: Rectangle { color: nativePalette.window }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.leftMargin: 20; Layout.rightMargin: 14
+ Layout.topMargin: 12; Layout.bottomMargin: 8
+ Label {
+ text: "CodexBar"
+ color: window.primaryText
+ font.pixelSize: 14
+ font.weight: Font.DemiBold
+ Layout.fillWidth: true
+ }
+ ToolButton {
+ text: "↻"
+ enabled: !desktop.busy
+ Accessible.name: "Refresh usage"
+ onClicked: desktop.refresh()
+ ToolTip.visible: hovered
+ ToolTip.text: "Refresh usage"
+ contentItem: Label {
+ text: "↻"
+ color: window.primaryText
+ font.pixelSize: 19
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+ background: Rectangle {
+ radius: 6
+ color: parent.hovered ? window.hoverColor : "transparent"
+ }
+ }
+ }
+
+ Flickable {
+ id: switcher
+ Layout.fillWidth: true
+ Layout.preferredHeight: 69
+ clip: true
+ contentWidth: tabs.implicitWidth + 32
+ contentHeight: height
+ flickableDirection: Flickable.HorizontalFlick
+ boundsBehavior: Flickable.StopAtBounds
+ Row {
+ id: tabs
+ x: 16
+ spacing: 3
+ height: parent.height - 6
+ Item {
+ width: 72; height: parent.height
+ Rectangle {
+ anchors.fill: parent; anchors.bottomMargin: 4
+ radius: 9
+ color: window.selectedIndex === -1 ? window.accent : "transparent"
+ }
+ Label {
+ anchors.centerIn: parent
+ anchors.verticalCenterOffset: -7
+ text: "▦"
+ color: window.selectedIndex === -1 ? "white" : window.secondaryText
+ font.pixelSize: 17
+ }
+ Label {
+ anchors.bottom: parent.bottom; anchors.bottomMargin: 13
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: "Overview"
+ color: window.selectedIndex === -1 ? "white" : window.secondaryText
+ font.pixelSize: 11
+ }
+ MouseArea {
+ anchors.fill: parent
+ activeFocusOnTab: true
+ Accessible.role: Accessible.Button
+ Accessible.name: "Overview"
+ onClicked: window.selectedIndex = -1
+ Keys.onReturnPressed: window.selectedIndex = -1
+ Keys.onSpacePressed: window.selectedIndex = -1
+ }
+ }
+ Repeater {
+ model: desktop.entries
+ Item {
+ required property var modelData
+ required property int index
+ width: 72; height: tabs.height
+ readonly property bool selected: window.selectedIndex === index
+ readonly property var firstWindow: modelData.windows.length ? modelData.windows[0] : null
+ Rectangle {
+ anchors.fill: parent; anchors.bottomMargin: 4
+ radius: 9
+ color: selected ? window.accent : "transparent"
+ }
+ Image {
+ id: providerIcon
+ source: "image://provider-icon/" + modelData.provider + "?color=" +
+ (selected ? "ffffff" : window.secondaryText.toString().slice(1))
+ width: 18; height: 18
+ sourceSize.width: 36; sourceSize.height: 36
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 5
+ visible: status === Image.Ready
+ }
+ Label {
+ visible: providerIcon.status !== Image.Ready
+ text: Usage.providerName(modelData.provider).slice(0, 2).toUpperCase()
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: 5
+ font.pixelSize: 12; font.bold: true
+ color: selected ? "white" : window.secondaryText
+ }
+ Label {
+ text: Usage.providerName(modelData.provider)
+ width: parent.width - 6
+ x: 3; y: 28
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight
+ font.pixelSize: 11
+ color: selected ? "white" : window.secondaryText
+ textFormat: Text.PlainText
+ }
+ Rectangle {
+ x: 10; width: parent.width - 20; height: 3
+ y: parent.height - 10; radius: 2
+ color: selected ? Qt.rgba(1, 1, 1, 0.4) : window.trackColor
+ Rectangle {
+ width: parent.width * (firstWindow ? 100 - firstWindow.remaining : 0) / 100
+ height: parent.height; radius: parent.radius
+ color: selected ? "white" : window.meterColor
+ }
+ }
+ MouseArea {
+ anchors.fill: parent
+ activeFocusOnTab: true
+ Accessible.role: Accessible.Button
+ Accessible.name: Usage.providerName(modelData.provider) +
+ (modelData.accountLabel ? " " + modelData.accountLabel : "")
+ onClicked: window.selectedIndex = index
+ Keys.onReturnPressed: window.selectedIndex = index
+ Keys.onSpacePressed: window.selectedIndex = index
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle { Layout.fillWidth: true; height: 1; color: window.dividerColor }
+
+ Flickable {
+ id: bodyScroll
+ Layout.fillWidth: true; Layout.fillHeight: true
+ Layout.minimumHeight: 0
+ clip: true
+ contentWidth: width
+ contentHeight: bodyContent.height
+ boundsBehavior: Flickable.StopAtBounds
+ Column {
+ id: bodyContent
+ width: bodyScroll.width
+ spacing: 14
+ Item { height: 2 }
+
+ Label {
+ visible: desktop.entries.length === 0
+ text: desktop.busy ? "Loading usage…" : desktop.error || "No providers to show. Enable one in Settings."
+ x: 20; width: parent.width - 40
+ color: window.secondaryText
+ wrapMode: Text.Wrap
+ }
+
+ ColumnLayout {
+ visible: window.selectedIndex === -1 && desktop.entries.length > 0
+ x: 20; width: parent.width - 40
+ spacing: 12
+ Label {
+ text: "Overview"
+ color: window.primaryText
+ font.pixelSize: 20; font.weight: Font.DemiBold
+ }
+ Label {
+ text: desktop.busy ? "Refreshing usage…" : desktop.stale ?
+ "Usage may be out of date" : desktop.updated ? "Updated " + desktop.updated : "Waiting for usage…"
+ color: window.secondaryText; font.pixelSize: 12
+ }
+ Repeater {
+ model: desktop.entries
+ Rectangle {
+ required property var modelData
+ required property int index
+ Layout.fillWidth: true
+ Layout.preferredHeight: 54
+ radius: 10
+ color: window.hoverColor
+ ColumnLayout {
+ id: overviewContent
+ anchors.fill: parent; anchors.margins: 11
+ spacing: 6
+ RowLayout {
+ Layout.fillWidth: true
+ Label {
+ text: Usage.providerName(modelData.provider)
+ color: window.primaryText
+ font.pixelSize: 14; font.weight: Font.DemiBold
+ Layout.fillWidth: true; elide: Text.ElideRight
+ }
+ Label {
+ text: modelData.windows.length ?
+ Usage.quotaValue(modelData.windows[0].remaining, desktop.settings.quotaDisplay) + "% " +
+ (desktop.settings.quotaDisplay === "used" ? "used" : "left") : "—"
+ color: window.secondaryText; font.pixelSize: 12
+ }
+ }
+ Rectangle {
+ Layout.fillWidth: true; height: 5; radius: 3; color: window.trackColor
+ Rectangle {
+ width: parent.width * (modelData.windows.length ?
+ Usage.quotaValue(modelData.windows[0].remaining, desktop.settings.quotaDisplay) : 0) / 100
+ height: parent.height; radius: parent.radius; color: window.meterColor
+ }
+ }
+ }
+ MouseArea {
+ anchors.fill: parent
+ activeFocusOnTab: true
+ Accessible.role: Accessible.Button
+ Accessible.name: "Show " + Usage.providerName(modelData.provider)
+ onClicked: window.selectedIndex = index
+ Keys.onReturnPressed: window.selectedIndex = index
+ Keys.onSpacePressed: window.selectedIndex = index
+ }
+ }
+ }
+ }
+
+ Column {
+ visible: window.selectedEntry !== null
+ x: 20; width: parent.width - 40
+ spacing: 14
+ RowLayout {
+ width: parent.width
+ Label {
+ text: window.selectedEntry ? Usage.providerName(window.selectedEntry.provider) : ""
+ color: window.primaryText
+ font.pixelSize: 21; font.weight: Font.DemiBold
+ Layout.fillWidth: true; elide: Text.ElideRight
+ }
+ Label {
+ text: window.selectedEntry ? window.selectedEntry.accountLabel : ""
+ visible: text !== ""
+ color: window.secondaryText
+ font.pixelSize: 12; elide: Text.ElideMiddle
+ Layout.maximumWidth: 150
+ textFormat: Text.PlainText
+ }
+ }
+ RowLayout {
+ width: parent.width
+ Label {
+ text: desktop.busy ? "Refreshing usage…" : desktop.stale ?
+ "Usage may be out of date" : desktop.updated ? "Updated " + desktop.updated : "Waiting for usage…"
+ color: window.secondaryText; font.pixelSize: 12
+ Layout.fillWidth: true; elide: Text.ElideRight
+ }
+ Label {
+ text: window.selectedEntry ? window.selectedEntry.plan : ""
+ visible: text !== ""
+ color: window.secondaryText; font.pixelSize: 12
+ textFormat: Text.PlainText
+ }
+ }
+ Label {
+ visible: text !== ""
+ text: window.selectedEntry ? window.selectedEntry.error || window.selectedEntry.status : ""
+ color: window.selectedEntry && window.selectedEntry.error ? "#cf7443" : window.secondaryText
+ font.pixelSize: 12; wrapMode: Text.Wrap
+ width: parent.width; textFormat: Text.PlainText
+ }
+ Rectangle { width: parent.width; height: 1; color: window.dividerColor }
+ Repeater {
+ model: window.selectedEntry ? window.selectedEntry.windows : []
+ MenuMetric {
+ required property var modelData
+ width: parent.width
+ metric: modelData
+ quotaDisplay: desktop.settings.quotaDisplay
+ resetDisplay: desktop.settings.resetDisplay
+ showPace: desktop.settings.showPace
+ warningColors: desktop.settings.warningColors
+ notifyThreshold: desktop.settings.notifyThreshold
+ accent: window.meterColor
+ primaryText: window.primaryText
+ secondaryText: window.secondaryText
+ trackColor: window.trackColor
+ }
+ }
+ ColumnLayout {
+ visible: window.selectedEntry && window.selectedEntry.credits !== null &&
+ window.selectedEntry.credits !== undefined
+ width: parent.width
+ spacing: 9
+ Rectangle { Layout.fillWidth: true; height: 1; color: window.dividerColor }
+ Label { text: "Credits"; color: window.primaryText; font.pixelSize: 15; font.weight: Font.DemiBold }
+ Label {
+ text: window.selectedEntry ? String(window.selectedEntry.credits) + " remaining" : ""
+ color: window.secondaryText; font.pixelSize: 13
+ }
+ }
+ ColumnLayout {
+ visible: desktop.settings.showCosts && (window.selectedCost !== null || desktop.costBusy)
+ width: parent.width
+ spacing: 8
+ Rectangle { Layout.fillWidth: true; height: 1; color: window.dividerColor }
+ Label { text: "Cost across accounts"; color: window.primaryText; font.pixelSize: 15; font.weight: Font.DemiBold }
+ Label {
+ text: window.selectedCost ? "Today " + Usage.money(window.selectedCost.today) +
+ " · Last 30 days " + Usage.money(window.selectedCost.month) : "Reading local history…"
+ color: window.primaryText; font.pixelSize: 13
+ Layout.fillWidth: true; wrapMode: Text.Wrap
+ }
+ Label {
+ visible: window.selectedCost !== null
+ text: window.selectedCost ? Usage.count(window.selectedCost.tokens) + " tokens · " +
+ Usage.provenance(window.selectedCost.provenance) : ""
+ color: window.secondaryText; font.pixelSize: 12
+ Layout.fillWidth: true; wrapMode: Text.Wrap
+ }
+ }
+ Repeater {
+ model: window.selectedEntry ? window.selectedEntry.details : []
+ ColumnLayout {
+ required property var modelData
+ width: parent.width
+ spacing: 6
+ Rectangle { Layout.fillWidth: true; height: 1; color: window.dividerColor }
+ Label {
+ text: modelData.title
+ visible: text !== ""
+ color: window.primaryText
+ font.pixelSize: 14; font.weight: Font.DemiBold
+ Layout.fillWidth: true; wrapMode: Text.Wrap; textFormat: Text.PlainText
+ }
+ Repeater {
+ model: modelData.rows
+ Label {
+ required property var modelData
+ text: modelData.label + ": " + modelData.value +
+ (modelData.secondaryValue ? " · " + modelData.secondaryValue : "")
+ color: window.secondaryText
+ font.pixelSize: 12
+ Layout.fillWidth: true; wrapMode: Text.Wrap; textFormat: Text.PlainText
+ }
+ }
+ }
+ }
+ }
+ Item { height: 6 }
+ }
+ }
+
+ Rectangle { Layout.fillWidth: true; height: 1; color: window.dividerColor }
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.leftMargin: 12; Layout.rightMargin: 12
+ Layout.topMargin: 6; Layout.bottomMargin: 8
+ spacing: 1
+ ActionRow { text: "Usage & Spend…"; onClicked: window.openPage("dashboard") }
+ ActionRow { text: "Settings…"; onClicked: window.openPage("settings") }
+ ActionRow { text: "About CodexBar"; onClicked: about.open() }
+ ActionRow { text: "Quit"; onClicked: Qt.quit() }
+ }
+ }
+ Dialog {
+ id: about
+ anchors.centerIn: parent
+ modal: true
+ title: "About CodexBar"
+ standardButtons: Dialog.Ok
+ Label { text: "CodexBar Linux " + Qt.application.version; padding: 14 }
+ }
+ component ActionRow: Button {
+ Layout.fillWidth: true
+ implicitHeight: 31
+ flat: true
+ contentItem: Label {
+ text: parent.text
+ color: window.primaryText
+ font.pixelSize: 13
+ verticalAlignment: Text.AlignVCenter
+ leftPadding: 10
+ }
+ background: Rectangle {
+ radius: 6
+ color: parent.hovered || parent.down ? window.hoverColor : "transparent"
+ }
+ }
+}
diff --git a/Integrations/Linux/qml/SettingsWindow.qml b/Integrations/Linux/qml/SettingsWindow.qml
index 7ed3700bf0..47273134d2 100644
--- a/Integrations/Linux/qml/SettingsWindow.qml
+++ b/Integrations/Linux/qml/SettingsWindow.qml
@@ -11,6 +11,18 @@ ApplicationWindow {
property string feedback: ""
property int section: 0
property var providerOrder: []
+ SystemPalette { id: nativePalette }
+ palette.window: nativePalette.window
+ palette.windowText: nativePalette.windowText
+ palette.base: nativePalette.base
+ palette.text: nativePalette.text
+ palette.button: nativePalette.button
+ palette.buttonText: nativePalette.buttonText
+ palette.highlight: nativePalette.highlight
+ palette.highlightedText: nativePalette.highlightedText
+ background: Rectangle { color: nativePalette.window }
+ readonly property color secondaryText: Qt.rgba(palette.windowText.r, palette.windowText.g,
+ palette.windowText.b, 0.65)
function moveProvider(index, offset) {
var order = providerOrder.slice();
var item = order.splice(index, 1)[0]; order.splice(index + offset, 0, item);
@@ -29,7 +41,7 @@ ApplicationWindow {
reset.currentIndex = reset.model.indexOf(s.resetDisplay);
trayStyle.currentIndex = trayStyle.model.indexOf(s.trayStyle);
theme.checked = s.followOmarchyTheme; pace.checked = s.showPace; warnings.checked = s.warningColors;
- refreshOnOpen.checked = s.refreshOnOpen;
+ refreshOnOpen.checked = s.refreshOnOpen; compactQuickView.checked = s.compactQuickView;
tray.checked = s.showTray; executable.text = s.executable; feedback = "";
}
Shortcut { sequence: "Escape"; onActivated: window.hide() }
@@ -39,141 +51,158 @@ ApplicationWindow {
}
ColumnLayout {
anchors.fill: parent; anchors.margins: 16; spacing: 12
- TabBar {
- Layout.fillWidth: true
- currentIndex: window.section
- TabButton { text: "General"; onClicked: window.section = 0 }
- TabButton { text: "Providers"; onClicked: window.section = 1 }
- TabButton { text: "Advanced"; onClicked: window.section = 2 }
- }
- ScrollView {
- id: scroll
+ RowLayout {
Layout.fillWidth: true; Layout.fillHeight: true
- contentWidth: availableWidth; clip: true
- ColumnLayout {
- width: scroll.availableWidth; spacing: 20
- GroupBox {
- title: "Provider"; Layout.fillWidth: true; visible: window.section === 1
- ColumnLayout {
- anchors.fill: parent; spacing: 10
- Label { text: "Provider" }
- ComboBox {
- id: provider; Layout.fillWidth: true; editable: true
- model: ["codex", "claude", "both", "enabled", "cursor", "gemini", "copilot", "antigravity", "custom", "all"]
- validator: RegularExpressionValidator { regularExpression: /^[a-z0-9-]{1,80}$/ }
- }
+ spacing: 14
+ Rectangle {
+ Layout.preferredWidth: 172; Layout.fillHeight: true
+ color: Qt.rgba(palette.windowText.r, palette.windowText.g, palette.windowText.b, 0.06)
+ radius: 10
+ ColumnLayout {
+ anchors.fill: parent; anchors.margins: 8
+ spacing: 3
+ Label {
+ text: "PREFERENCES"
+ color: window.secondaryText
+ font.pixelSize: 11; font.weight: Font.DemiBold
+ Layout.leftMargin: 9; Layout.topMargin: 8; Layout.bottomMargin: 5
+ }
+ NavigationButton { text: "General"; destination: 0 }
+ NavigationButton { text: "Providers"; destination: 1 }
+ NavigationButton { text: "Advanced"; destination: 2 }
+ Item { Layout.fillHeight: true }
+ }
+ }
+ ScrollView {
+ id: scroll
+ Layout.fillWidth: true; Layout.fillHeight: true
+ contentWidth: availableWidth; clip: true
+ ColumnLayout {
+ width: scroll.availableWidth; spacing: 20
+ SettingSection {
+ title: "Provider"; Layout.fillWidth: true; visible: window.section === 1
ColumnLayout {
- visible: provider.editText === "custom"
- Layout.fillWidth: true
- RowLayout {
- Layout.fillWidth: true
- ComboBox { id: available; model: desktop.providers; textRole: "displayName"; Layout.fillWidth: true }
- Button {
- text: "Add"
- enabled: available.currentIndex >= 0
- onClicked: {
- var id = desktop.providers[available.currentIndex].provider;
- if (window.providerOrder.indexOf(id) < 0) window.providerOrder = window.providerOrder.concat([id]);
- }
- }
+ anchors.fill: parent; spacing: 10
+ Label { text: "Provider" }
+ ComboBox {
+ id: provider; Layout.fillWidth: true; editable: true
+ model: ["codex", "claude", "both", "enabled", "cursor", "gemini", "copilot", "antigravity", "custom", "all"]
+ validator: RegularExpressionValidator { regularExpression: /^[a-z0-9-]{1,80}$/ }
}
- Repeater {
- model: window.providerOrder
+ ColumnLayout {
+ visible: provider.editText === "custom"
+ Layout.fillWidth: true
RowLayout {
- required property string modelData
- required property int index
Layout.fillWidth: true
- Label { text: Usage.providerName(modelData); Layout.fillWidth: true; wrapMode: Text.Wrap }
- Button { text: "↑"; Accessible.name: "Move " + modelData + " up"; enabled: index > 0; onClicked: window.moveProvider(index, -1) }
- Button { text: "↓"; Accessible.name: "Move " + modelData + " down"; enabled: index < window.providerOrder.length - 1; onClicked: window.moveProvider(index, 1) }
- Button { text: "Remove"; onClicked: { var order = window.providerOrder.slice(); order.splice(index, 1); window.providerOrder = order; } }
+ ComboBox { id: available; model: desktop.providers; textRole: "displayName"; Layout.fillWidth: true }
+ Button {
+ text: "Add"
+ enabled: available.currentIndex >= 0
+ onClicked: {
+ var id = desktop.providers[available.currentIndex].provider;
+ if (window.providerOrder.indexOf(id) < 0) window.providerOrder = window.providerOrder.concat([id]);
+ }
+ }
+ }
+ Repeater {
+ model: window.providerOrder
+ RowLayout {
+ required property string modelData
+ required property int index
+ Layout.fillWidth: true
+ Label { text: Usage.providerName(modelData); Layout.fillWidth: true; wrapMode: Text.Wrap }
+ Button { text: "↑"; Accessible.name: "Move " + modelData + " up"; enabled: index > 0; onClicked: window.moveProvider(index, -1) }
+ Button { text: "↓"; Accessible.name: "Move " + modelData + " down"; enabled: index < window.providerOrder.length - 1; onClicked: window.moveProvider(index, 1) }
+ Button { text: "Remove"; onClicked: { var order = window.providerOrder.slice(); order.splice(index, 1); window.providerOrder = order; } }
+ }
}
}
+ Label { text: "Choose custom to select and order providers, or enabled to follow your CLI configuration."; Layout.fillWidth: true; wrapMode: Text.Wrap; opacity: 0.65 }
+ RowLayout {
+ Layout.fillWidth: true
+ Label { text: "Source"; Layout.fillWidth: true }
+ ComboBox { id: source; model: ["auto", "oauth", "cli", "api", "web"] }
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ Label { text: "Account (0 = default)"; Layout.fillWidth: true; wrapMode: Text.Wrap }
+ SpinBox { enabled: !allAccounts.checked && ["custom", "both", "all", "enabled"].indexOf(provider.editText) < 0; id: account; from: 0; to: 999; editable: true }
+ }
+ Option { enabled: ["custom", "both", "all", "enabled"].indexOf(provider.editText) < 0; id: allAccounts; text: "All accounts" }
+ Option { id: identity; text: "Show account identity" }
+ RowLayout {
+ Layout.fillWidth: true
+ ComboBox { id: loginProvider; model: ["codex", "claude"]; Layout.fillWidth: true }
+ Button { text: "Sign in…"; onClicked: { if (desktop.accountAction(loginProvider.currentText, "login")) window.feedback = "Finish signing in in the terminal, then refresh usage."; } }
+ Button { text: "Sign out…"; onClicked: signOut.open() }
+ }
+ Label { text: "Sign-in opens your provider CLI in a terminal. Account selection above only changes displayed usage."; Layout.fillWidth: true; wrapMode: Text.Wrap; opacity: 0.65 }
}
- Label { text: "Choose custom to select and order providers, or enabled to follow your CLI configuration."; Layout.fillWidth: true; wrapMode: Text.Wrap; opacity: 0.65 }
- RowLayout {
- Layout.fillWidth: true
- Label { text: "Source"; Layout.fillWidth: true }
- ComboBox { id: source; model: ["auto", "oauth", "cli", "api", "web"] }
- }
- RowLayout {
- Layout.fillWidth: true
- Label { text: "Account (0 = default)"; Layout.fillWidth: true; wrapMode: Text.Wrap }
- SpinBox { enabled: !allAccounts.checked && ["custom", "both", "all", "enabled"].indexOf(provider.editText) < 0; id: account; from: 0; to: 999; editable: true }
- }
- Option { enabled: ["custom", "both", "all", "enabled"].indexOf(provider.editText) < 0; id: allAccounts; text: "All accounts" }
- Option { id: identity; text: "Show account identity" }
- RowLayout {
- Layout.fillWidth: true
- ComboBox { id: loginProvider; model: ["codex", "claude"]; Layout.fillWidth: true }
- Button { text: "Sign in…"; onClicked: { if (desktop.accountAction(loginProvider.currentText, "login")) window.feedback = "Finish signing in in the terminal, then refresh usage."; } }
- Button { text: "Sign out…"; onClicked: signOut.open() }
- }
- Label { text: "Sign-in opens your provider CLI in a terminal. Account selection above only changes displayed usage."; Layout.fillWidth: true; wrapMode: Text.Wrap; opacity: 0.65 }
}
- }
- GroupBox {
- title: "Updates"; Layout.fillWidth: true; visible: window.section === 0
- ColumnLayout {
- anchors.fill: parent; spacing: 10
- RowLayout {
- Layout.fillWidth: true
- Label { text: "Refresh every (seconds)"; Layout.fillWidth: true; wrapMode: Text.Wrap }
- SpinBox { id: interval; from: 60; to: 3600; stepSize: 60; editable: true }
- }
- Option { id: refreshOnOpen; text: "Refresh when opening usage" }
- Option { id: notices; text: "Notify about low quota, resets, and outages" }
- RowLayout {
- Layout.fillWidth: true; enabled: notices.checked
- Label { text: "Remaining quota threshold (%)"; Layout.fillWidth: true; wrapMode: Text.Wrap }
- SpinBox { id: threshold; from: 1; to: 99; editable: true }
+ SettingSection {
+ title: "Updates"; Layout.fillWidth: true; visible: window.section === 0
+ ColumnLayout {
+ anchors.fill: parent; spacing: 10
+ RowLayout {
+ Layout.fillWidth: true
+ Label { text: "Refresh every (seconds)"; Layout.fillWidth: true; wrapMode: Text.Wrap }
+ SpinBox { id: interval; from: 60; to: 3600; stepSize: 60; editable: true }
+ }
+ Option { id: refreshOnOpen; text: "Refresh when opening usage" }
+ Option { id: notices; text: "Notify about low quota, resets, and outages" }
+ RowLayout {
+ Layout.fillWidth: true; enabled: notices.checked
+ Label { text: "Remaining quota threshold (%)"; Layout.fillWidth: true; wrapMode: Text.Wrap }
+ SpinBox { id: threshold; from: 1; to: 99; editable: true }
+ }
+ Option { id: status; text: "Service status" }
}
- Option { id: status; text: "Service status" }
}
- }
- GroupBox {
- title: "Desktop"; Layout.fillWidth: true; visible: window.section === 0
- ColumnLayout {
- anchors.fill: parent; spacing: 10
- Button {
- text: desktop.launchAtLogin ? "Disable start at login" : "Enable start at login"
- onClicked: desktop.setLaunchAtLogin(!desktop.launchAtLogin)
- }
- Option { id: tray; text: "Tray icon" }
- Label { text: "Optional with the Omarchy widget. CodexBar is also available in the application launcher."; Layout.fillWidth: true; wrapMode: Text.Wrap; opacity: 0.65 }
- RowLayout {
- Layout.fillWidth: true
- Label { text: "Tray style"; Layout.fillWidth: true }
- ComboBox { id: trayStyle; model: ["meters", "icon"] }
+ SettingSection {
+ title: "Desktop"; Layout.fillWidth: true; visible: window.section === 0
+ ColumnLayout {
+ anchors.fill: parent; spacing: 10
+ Button {
+ text: desktop.launchAtLogin ? "Disable start at login" : "Enable start at login"
+ onClicked: desktop.setLaunchAtLogin(!desktop.launchAtLogin)
+ }
+ Option { id: tray; text: "Tray icon" }
+ Option { id: compactQuickView; text: "Use compact Quick View from the tray" }
+ Label { text: "Optional with the Omarchy widget. CodexBar is also available in the application launcher."; Layout.fillWidth: true; wrapMode: Text.Wrap; opacity: 0.65 }
+ RowLayout {
+ Layout.fillWidth: true
+ Label { text: "Tray style"; Layout.fillWidth: true }
+ ComboBox { id: trayStyle; model: ["meters", "icon"] }
+ }
+ Option { id: costs; text: "Local spending" }
}
- Option { id: costs; text: "Local spending" }
}
- }
- GroupBox {
- title: "Display"; Layout.fillWidth: true; visible: window.section === 0
- ColumnLayout {
- anchors.fill: parent; spacing: 10
- RowLayout {
- Layout.fillWidth: true
- Label { text: "Quota"; Layout.fillWidth: true }
- ComboBox { id: quota; model: ["remaining", "used"] }
- }
- RowLayout {
- Layout.fillWidth: true
- Label { text: "Reset time"; Layout.fillWidth: true }
- ComboBox { id: reset; model: ["countdown", "absolute", "both"] }
+ SettingSection {
+ title: "Display"; Layout.fillWidth: true; visible: window.section === 0
+ ColumnLayout {
+ anchors.fill: parent; spacing: 10
+ RowLayout {
+ Layout.fillWidth: true
+ Label { text: "Quota"; Layout.fillWidth: true }
+ ComboBox { id: quota; model: ["remaining", "used"] }
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ Label { text: "Reset time"; Layout.fillWidth: true }
+ ComboBox { id: reset; model: ["countdown", "absolute", "both"] }
+ }
+ Option { id: theme; text: "Follow Omarchy theme colors" }
+ Option { id: pace; text: "Show pace" }
+ Option { id: warnings; text: "Highlight low quota" }
}
- Option { id: theme; text: "Follow Omarchy theme colors" }
- Option { id: pace; text: "Show pace" }
- Option { id: warnings; text: "Highlight low quota" }
}
- }
- GroupBox {
- title: "CLI"; Layout.fillWidth: true; visible: window.section === 2
- ColumnLayout {
- anchors.fill: parent; spacing: 10
- Label { text: "Executable" }
- TextField { id: executable; Layout.fillWidth: true; selectByMouse: true; placeholderText: "codexbar" }
+ SettingSection {
+ title: "CLI"; Layout.fillWidth: true; visible: window.section === 2
+ ColumnLayout {
+ anchors.fill: parent; spacing: 10
+ Label { text: "Executable" }
+ TextField { id: executable; Layout.fillWidth: true; selectByMouse: true; placeholderText: "codexbar" }
+ }
}
}
}
@@ -192,7 +221,8 @@ ApplicationWindow {
notifyThreshold: threshold.value, refreshSeconds: interval.value,
providerOrder: window.providerOrder, quotaDisplay: quota.currentText, resetDisplay: reset.currentText,
followOmarchyTheme: theme.checked, showPace: pace.checked, warningColors: warnings.checked, trayStyle: trayStyle.currentText,
- refreshOnOpen: refreshOnOpen.checked, showTray: tray.checked, executable: executable.text.trim()})) window.feedback = "Settings saved";
+ refreshOnOpen: refreshOnOpen.checked, showTray: tray.checked, compactQuickView: compactQuickView.checked,
+ executable: executable.text.trim()})) window.feedback = "Settings saved";
}
}
}
@@ -217,4 +247,43 @@ ApplicationWindow {
wrapMode: Text.Wrap
}
}
+ component NavigationButton: Button {
+ required property int destination
+ Layout.fillWidth: true
+ implicitHeight: 36
+ flat: true
+ contentItem: Label {
+ text: parent.text
+ color: window.section === parent.destination ? "white" : palette.windowText
+ font.pixelSize: 13
+ font.weight: window.section === parent.destination ? Font.DemiBold : Font.Normal
+ verticalAlignment: Text.AlignVCenter
+ leftPadding: 12
+ }
+ background: Rectangle {
+ radius: 7
+ color: window.section === parent.destination ? palette.highlight :
+ parent.hovered ? Qt.rgba(palette.windowText.r, palette.windowText.g, palette.windowText.b, 0.08) :
+ "transparent"
+ }
+ onClicked: window.section = destination
+ }
+ component SettingSection: GroupBox {
+ Layout.fillWidth: true
+ padding: 14
+ label: Label {
+ text: parent.title
+ color: palette.windowText
+ font.pixelSize: 15
+ font.weight: Font.DemiBold
+ leftPadding: 4
+ }
+ background: Rectangle {
+ y: 8
+ height: parent.height - 8
+ radius: 10
+ color: Qt.rgba(palette.windowText.r, palette.windowText.g, palette.windowText.b, 0.035)
+ border.color: Qt.rgba(palette.windowText.r, palette.windowText.g, palette.windowText.b, 0.13)
+ }
+ }
}
diff --git a/Integrations/Linux/tests/test_desktop.py b/Integrations/Linux/tests/test_desktop.py
index 3e39f165c7..f1f6c35ce0 100644
--- a/Integrations/Linux/tests/test_desktop.py
+++ b/Integrations/Linux/tests/test_desktop.py
@@ -199,12 +199,19 @@ def test_failed_refresh_preserves_previous_usage(self):
self.assertTrue(value['stale'])
self.assertEqual(value['summary'], 'CX 60%')
- def test_refresh_on_open_is_optional_and_does_not_scan_spending(self):
+ def test_quick_view_is_explicit_and_scans_cost_only_when_opened(self):
calls = self.root / 'calls.jsonl'
+ self.assertFalse((self.root / 'config/codexbar/linux.json').exists())
self.client('--usage')
- time.sleep(0.15)
- self.assertEqual(len(calls.read_text().splitlines()), 1)
- self.client('--configure', '{"refreshOnOpen":true}')
+ self.assertEqual([json.loads(line)['args'][0] for line in calls.read_text().splitlines()], ['usage'])
+ self.client('--quick-view')
+ self.wait_for(lambda value: value.get('costProviders') == 1 and not value['costBusy'])
+ self.assertEqual([json.loads(line)['args'][0] for line in calls.read_text().splitlines()],
+ ['usage', 'cost'])
+ self.client('--configure', '{"compactQuickView":true,"refreshOnOpen":true}')
+ self.assertTrue(json.loads((self.root / 'config/codexbar/linux.json').read_text())['compactQuickView'])
+ self.client('--configure', '{"showCosts":false}')
+ self.client('--configure', '{"showCosts":true}')
self.wait_for(lambda value: bool(value.get('entries')) and not value['busy'])
previous = len(calls.read_text().splitlines())
self.client('--usage')
@@ -212,8 +219,11 @@ def test_refresh_on_open_is_optional_and_does_not_scan_spending(self):
while len(calls.read_text().splitlines()) == previous and time.monotonic() < end:
time.sleep(0.05)
self.assertEqual(len(calls.read_text().splitlines()), previous + 1)
- for line in calls.read_text().splitlines():
- self.assertEqual(json.loads(line)['args'][0], 'usage')
+ self.assertEqual(json.loads(calls.read_text().splitlines()[-1])['args'][0], 'usage')
+ self.assertNotIn('cost', [json.loads(line)['args'][0] for line in calls.read_text().splitlines()[previous:]])
+ self.client('--quick-view')
+ self.wait_for(lambda value: value.get('costProviders') == 1 and not value['costBusy'])
+ self.assertIn('cost', [json.loads(line)['args'][0] for line in calls.read_text().splitlines()[previous + 1:]])
def test_failed_spending_preserves_previous_scan(self):
self.client('--spending')
diff --git a/docs/screenshots/linux-quick-view-1x-popos.png b/docs/screenshots/linux-quick-view-1x-popos.png
new file mode 100644
index 0000000000..14cb990619
Binary files /dev/null and b/docs/screenshots/linux-quick-view-1x-popos.png differ
diff --git a/docs/screenshots/linux-quick-view-after-popos.png b/docs/screenshots/linux-quick-view-after-popos.png
new file mode 100644
index 0000000000..8ae9f6267a
Binary files /dev/null and b/docs/screenshots/linux-quick-view-after-popos.png differ
diff --git a/docs/screenshots/linux-quick-view-before-popos-v061.png b/docs/screenshots/linux-quick-view-before-popos-v061.png
new file mode 100644
index 0000000000..473593ddbb
Binary files /dev/null and b/docs/screenshots/linux-quick-view-before-popos-v061.png differ
diff --git a/docs/screenshots/linux-quick-view-dark-popos.png b/docs/screenshots/linux-quick-view-dark-popos.png
new file mode 100644
index 0000000000..9f5e99fd67
Binary files /dev/null and b/docs/screenshots/linux-quick-view-dark-popos.png differ
diff --git a/docs/screenshots/linux-settings-after-popos.png b/docs/screenshots/linux-settings-after-popos.png
new file mode 100644
index 0000000000..25f923125d
Binary files /dev/null and b/docs/screenshots/linux-settings-after-popos.png differ
diff --git a/docs/screenshots/linux-settings-before-popos-v061.png b/docs/screenshots/linux-settings-before-popos-v061.png
new file mode 100644
index 0000000000..05b23b7df8
Binary files /dev/null and b/docs/screenshots/linux-settings-before-popos-v061.png differ
diff --git a/docs/screenshots/linux-settings-dark-popos.png b/docs/screenshots/linux-settings-dark-popos.png
new file mode 100644
index 0000000000..fd2d060fd8
Binary files /dev/null and b/docs/screenshots/linux-settings-dark-popos.png differ
diff --git a/docs/screenshots/linux-usage-default-after-popos.png b/docs/screenshots/linux-usage-default-after-popos.png
new file mode 100644
index 0000000000..fe748436ec
Binary files /dev/null and b/docs/screenshots/linux-usage-default-after-popos.png differ