The GNOME AppIndicator extension delivers Activate with (0, 0) instead of
real screen coordinates, which made the popover land in the top-left corner
because the centering math treated the bogus origin as a legitimate anchor.
Treat (0, 0) as no-anchor and reuse the top-right fallback the non-Linux
path already uses, so the popover lands near the StatusNotifier area on
hosts that don't pass coordinates.
Also cfg-gate the Emitter import since it is only used in the non-Linux
tray menu path.
Tauri's Linux tray uses libappindicator, which by design never fires
left-click events to the app and exposes no icon screen position. That
forced a menu-first UX on GNOME and made anchoring the popover near the
icon impossible (tauri-apps/tauri#7283, closed not-planned).
Drop libappindicator on Linux and talk StatusNotifierItem directly via
the ksni crate:
* new src-tauri/src/tray_linux.rs implements ksni::Tray. activate(x, y)
and secondary_activate(x, y) arrive with real screen coordinates. The
implementation exports no menu on purpose, so SNI hosts (notably the
gnome-shell-extension-appindicator) call Activate on left click instead
of opening a context menu.
* LinuxTrayHandle wraps the async ksni::Handle with a sync Mutex so the
Tauri command handler can push title updates without naming the
generic Handle<CodeburnTray> across module boundaries.
* lib.rs gates TrayIconBuilder behind cfg(not(target_os = "linux")) and
spawns ksni on the Tokio runtime Tauri already owns. Tray click events
go through codeburn://tray-activate with {x, y} so the positioning logic
can anchor the popover directly below the click, clamped to the monitor.
* new set_tray_title command pushes the hero cost to the tray label on
every payload fetch. On Linux that's the SNI title next to the icon;
on other platforms it's the TrayIcon::set_title.
* new quit_app command + popover-footer × button gives Linux users a way
to exit without a tray menu.
* empty-state copy already landed in a previous commit. Combined with the
footer Quit button, the popover is now self-contained on Linux.
Windows is unaffected: TrayIconBuilder there fires left-click events
correctly and set_title/tooltip work out of the box.
Known limitation: on vanilla GNOME without AppIndicator support, there is
no tray to click. Documentation will link to the AppIndicator extension
install, same caveat Slack, Discord and 1Password ship with.
window.outer_size() returns (0, 0) on the first show, so the previous
positioning snapped to top-left when the window had not been rendered yet.
Derive the target x from the popover width declared in tauri.conf.json and
scale margins by the monitor's scale factor so HiDPI displays land in the
right spot.
Linux window managers ignore tray icon screen coordinates, so we cannot
attach the popover to the icon the way macOS does. Snap it to the top-right
of the primary monitor on every show so it feels anchored to the tray area
on GNOME, KDE and Unity, which all host StatusNotifier icons at the top
right.
Linux tray implementations often do not deliver a distinct left-click event
through the AppIndicator path, so the popover could not be opened on GNOME.
Add an explicit Show Dashboard menu item that calls toggle_popover so the
UI is reachable via the tray menu on every platform.
Also render a friendly empty state in the popover when no session data is
found, pointing at the supported source paths instead of showing a bare
$0 hero with an empty activity list.
desktop/Scripts/provision-linux.sh: one-shot apt + Node 20 + Rust stable
+ codeburn CLI + repo clone + npm install. Leaves the user one command
away from `npm run tauri dev`.
desktop/Scripts/autoinstall/: cloud-init user-data + meta-data plus a
README for building the CIDATA ISO. Mount that ISO alongside the Ubuntu
Server ISO in UTM and the installer runs without any prompts. Default
test credentials codeburn/codeburn; README calls out changing them for
non-throwaway VMs.
Adds desktop/ with a native tray app that mirrors the macOS popover via
a shared tokens.json and the same codeburn status --format menubar-json
data source. Same security posture as the Swift app: argv-validated CLI
spawn, O_NOFOLLOW cache writes, flock on config.json, FX rate clamping
to [0.0001, 1_000_000].
Stack:
- Tauri 2.x (Rust) for tray + window lifecycle, shells out to the CLI
- React + TypeScript + Vite for the popover UI
- libayatana-appindicator on Linux, system tray on Windows
- Produces .deb / .rpm / .AppImage on ubuntu-latest, .msi on
windows-latest. Both workflows run on free GitHub Actions minutes.
Rust modules (src-tauri/src/):
- lib.rs: tray icon, menu events, popover toggle, state wiring
- cli.rs: CodeburnCli with argv allowlist and bounded pipe drain
(20 MB stdout / 256 KB stderr / 60 s wall time)
- config.rs: flock-guarded read-modify-write of ~/.config/codeburn/config.json
- fx.rs: Frankfurter fetch with 24 h disk cache, bounds check
Frontend:
- App.tsx with agent tabs, period switcher, hero cost, activity rows,
optimize findings CTA, footer (currency picker / refresh / Open
Full Report). Listens for `codeburn://refresh` tray events.
- lib/payload.ts mirrors the CLI's MenubarPayload shape
- lib/currency.ts mirrors the Swift Double.asCurrency helpers
- styles.css with design tokens as CSS custom properties
CLI:
- `codeburn menubar` now platform-dispatches: macOS (.app zip),
Linux (.AppImage into ~/.local/bin), Windows (.msi via msiexec).
macOS behaviour preserved exactly.
Release workflow:
- .github/workflows/release-desktop-linux.yml triggers on `linux-v*`
tags, builds all three Linux formats, uploads to GitHub Releases.
Scaffold verified:
- cargo check -> clean
- tsc --noEmit -> clean
- npm run build (CLI) -> 205 KB
- Existing test suite: 230 / 230 still pass