Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/build-linux-desktop/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 6 additions & 4 deletions Integrations/Linux/DesktopController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,15 @@ 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;
}

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";
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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}};
Expand Down
20 changes: 14 additions & 6 deletions Integrations/Linux/README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -203,15 +209,15 @@ 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.
Optional notifications use the desktop's D-Bus notification service for remaining
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:
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Integrations/Linux/codexbar-linux.pro
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions Integrations/Linux/desktop.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<file>Shared/Usage.js</file>
<file>Shared/Notifications.js</file>
<file>qml/Main.qml</file>
<file>qml/QuickView.qml</file>
<file>qml/MenuMetric.qml</file>
<file>qml/Dashboard.qml</file>
<file>qml/SettingsWindow.qml</file>
<file>qml/UsageCard.qml</file>
Expand Down
41 changes: 37 additions & 4 deletions Integrations/Linux/main.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#include "DesktopController.h"

#include <QApplication>
#include <QColor>
#include <QCommandLineParser>
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <QFileInfo>
#include <QIcon>
#include <QImage>
#include <QJsonDocument>
#include <QLocalSocket>
#include <QLockFile>
Expand All @@ -15,13 +17,41 @@
#include <QRegularExpression>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickImageProvider>
#include <QQuickStyle>
#include <QStandardPaths>
#include <QSvgRenderer>
#include <QSystemTrayIcon>
#include <cstdio>
#include <memory>
#include <unistd.h>

// 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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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());
Expand Down
Loading