windows: cfg-gate the tray badge so the Linux build has no dead code

The spend badge is a second tray icon carrying the number as its bitmap, which
only the Tauri tray backend provides; Linux runs its own SNI tray and has no
equivalent. The module was compiled there anyway, so every item in it - and the
two tray ids in lib.rs - tripped dead_code under clippy -D warnings on the
ubuntu leg.

- `mod tray_badge` and TRAY_ID / BADGE_TRAY_ID are now cfg(not(linux)), so the
  code is absent on Linux rather than present and unused.
- `set_tray_badge` reports the badge as unsupported on Linux instead of
  returning a success that never happened.
- The frontend hides the control wherever it is unsupported, behind
  TRAY_BADGE_SUPPORTED in lib/platform.ts - one constant, three call sites
  (the settings row, the footer menu item, and the effect that would otherwise
  invoke the command).
- AppState's `linux_tray` field is dropped: it was written and never read, which
  is the same lint one file over. init_tray_linux already owns the handle.

Checked rather than reasoned: `cargo clippy --all-targets -- -D warnings` passes
on macOS, and a scratch copy of the crate with the linux/macos cfg arms swapped
(so a normal macOS clippy selects everything a Linux build would keep, and drops
everything it would drop) also passes. Reinstating the ungated `mod tray_badge`
in that copy reproduces the exact CI failure, so the check is real. It stubs
tray_linux.rs, whose ksni and png deps do not build here - that file is still
only covered by CI's ubuntu leg.
This commit is contained in:
iamtoruk 2026-08-18 04:37:27 -07:00
parent 562bab65ba
commit 76c63cbfd1
6 changed files with 44 additions and 23 deletions

View file

@ -8,6 +8,12 @@ Linux (ksni / AppIndicator) support is compiled and kept working for dev, but it
**experimental and unreleased** - Linux users should use the GNOME extension in `../gnome/`.
The releases this repo cuts from here are Windows only.
Not everything crosses over: the spend badge is a second tray icon carrying the number as its
bitmap, which only the Windows notification area provides. `tray_badge` is compiled out on
Linux, the `set_tray_badge` command reports it as unsupported there, and the frontend hides
the control behind `TRAY_BADGE_SUPPORTED` in `src/lib/platform.ts`. Anything else that is
Windows-only must be cfg-gated the same way, or the ubuntu leg of CI fails on dead code.
## Architecture
```

View file

@ -3,6 +3,10 @@ mod cli;
mod config;
mod fx;
mod plan;
/// The spend-in-the-tray badge is a second tray icon, which only the Tauri tray backend
/// provides; Linux runs its own SNI tray (`tray_linux`) and has no equivalent, so the
/// whole module is compiled out there rather than sitting unused.
#[cfg(not(target_os = "linux"))]
mod tray_badge;
#[cfg(target_os = "linux")]
mod tray_linux;
@ -26,9 +30,11 @@ use crate::cli::CodeburnCli;
use crate::config::CurrencyConfig;
use crate::fx::FxCache;
#[cfg(not(target_os = "linux"))]
const TRAY_ID: &str = "codeburn-tray";
/// Second tray icon that carries today's spend as text, sitting next to the logo. The
/// closest Windows and Linux panels get to the macOS menubar title.
/// closest the Windows notification area gets to the macOS menubar title.
#[cfg(not(target_os = "linux"))]
const BADGE_TRAY_ID: &str = "codeburn-badge";
const POPOVER_LABEL: &str = "popover";
@ -41,8 +47,6 @@ pub struct AppState {
pub config: Mutex<CurrencyConfig>,
pub fx: FxCache,
pub plan: plan::PlanClient,
#[cfg(target_os = "linux")]
pub linux_tray: tray_linux::LinuxTrayHandle,
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@ -51,24 +55,18 @@ pub fn run() {
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| {
#[cfg(target_os = "linux")]
let linux_tray = tray_linux::LinuxTrayHandle::empty();
let state = AppState {
app.manage(AppState {
cli: Mutex::new(CodeburnCli::resolve()),
config: Mutex::new(CurrencyConfig::load_or_default()),
fx: FxCache::new(),
plan: plan::PlanClient::new(),
#[cfg(target_os = "linux")]
linux_tray: linux_tray.clone(),
};
app.manage(state);
});
#[cfg(not(target_os = "linux"))]
build_tray_tauri(app.handle())?;
#[cfg(target_os = "linux")]
init_tray_linux(app.handle().clone(), linux_tray);
init_tray_linux(app.handle().clone(), tray_linux::LinuxTrayHandle::empty());
if let Some(window) = app.get_webview_window(POPOVER_LABEL) {
let _ = window.hide();
@ -466,6 +464,14 @@ mod commands {
/// `text` is a short spend string ("$87", "142", "1.2K"); `None` hides the badge icon.
#[tauri::command]
pub fn set_tray_badge(app: AppHandle, text: Option<String>) -> Result<(), String> {
#[cfg(target_os = "linux")]
{
// Unreachable from the UI: the frontend hides the control wherever the badge is
// unsupported (lib/platform.ts). Saying so beats reporting a success that never
// happened.
let _ = (app, text);
Err("the tray spend badge needs a second tray icon, which the Linux SNI tray does not provide".to_string())
}
#[cfg(not(target_os = "linux"))]
{
let Some(badge) = app.tray_by_id(super::BADGE_TRAY_ID) else {
@ -487,12 +493,8 @@ mod commands {
badge.set_visible(false).map_err(|e| e.to_string())?;
}
}
Ok(())
}
#[cfg(target_os = "linux")]
{
let _ = (app, text);
}
Ok(())
}
#[tauri::command]

View file

@ -8,6 +8,7 @@ import { USD, formatCurrency, trayBadgeText } from './lib/currency'
import { PayloadCache } from './lib/cache'
import { relativePast } from './lib/dates'
import { applyTheme, currentTheme, readSetting, writeSetting } from './lib/settings'
import { TRAY_BADGE_SUPPORTED } from './lib/platform'
import { AgentTabStrip, detectedProviders } from './components/AgentTabStrip'
import type { Provider } from './components/AgentTabStrip'
import { ModelsSection } from './components/ModelsSection'
@ -65,7 +66,7 @@ export function App() {
const [version, setVersion] = useState('')
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
const [theme, setTheme] = useState(() => currentTheme())
const [trayBadge, setTrayBadge] = useState(() => readSetting('trayBadge') !== 'off')
const [trayBadge, setTrayBadge] = useState(() => TRAY_BADGE_SUPPORTED && readSetting('trayBadge') !== 'off')
const [showSettings, setShowSettings] = useState(false)
// The window starts hidden and is shown by a tray click, which emits `codeburn://shown`.
const [popoverVisible, setPopoverVisible] = useState(false)
@ -216,6 +217,7 @@ export function App() {
}, [todayCost, currency])
useEffect(() => {
if (!TRAY_BADGE_SUPPORTED) return
const text = trayBadge && todayCost !== null ? trayBadgeText(todayCost, currency) : null
invoke('set_tray_badge', { text }).catch(err => setError(`Tray badge: ${String(err)}`))
}, [todayCost, currency, trayBadge])

View file

@ -1,5 +1,6 @@
import type { CurrencyState } from '../lib/currency'
import { CURRENCY_CODES } from '../lib/currency'
import { TRAY_BADGE_SUPPORTED } from '../lib/platform'
import { DropMenu } from './DropMenu'
import { CoinIcon, DownloadIcon, EllipsisIcon, RefreshIcon, TerminalIcon } from './Icons'
@ -64,7 +65,9 @@ export function FooterBar({
className="dropmenu-more"
items={[
{ id: 'settings', label: settingsOpen ? 'Back to overview' : 'Settings…' },
{ id: 'badge', label: "Show today's cost in tray", checked: trayBadge, separatorBefore: true },
...(TRAY_BADGE_SUPPORTED
? [{ id: 'badge', label: "Show today's cost in tray", checked: trayBadge, separatorBefore: true }]
: []),
{ id: 'theme', label: themeLabel },
{ id: 'quit', label: 'Quit CodeBurn', separatorBefore: true },
]}

View file

@ -3,7 +3,7 @@ import { invoke } from '@tauri-apps/api/core'
import { openUrl } from '@tauri-apps/plugin-opener'
import type { CurrencyState } from '../lib/currency'
import { CURRENCY_CODES } from '../lib/currency'
import { homePath } from '../lib/platform'
import { homePath, TRAY_BADGE_SUPPORTED } from '../lib/platform'
import type { CliStatus } from './SetupState'
import { DropMenu } from './DropMenu'
import { ChevronDown, ChevronRight } from './Icons'
@ -70,9 +70,11 @@ export function SettingsPanel({
<Toggle on={loginItem === true} disabled={loginItem === null} onToggle={toggleLogin} />
</Row>
{loginError && <div className="settings-error">{loginError}</div>}
<Row label="Show today's cost in the tray" hint="A second tray icon carrying the number, next to the logo.">
<Toggle on={trayBadge} onToggle={() => onTrayBadge(!trayBadge)} />
</Row>
{TRAY_BADGE_SUPPORTED && (
<Row label="Show today's cost in the tray" hint="A second tray icon carrying the number, next to the logo.">
<Toggle on={trayBadge} onToggle={() => onTrayBadge(!trayBadge)} />
</Row>
)}
</div>
<div className="settings-group">

View file

@ -8,3 +8,9 @@ const SEP = IS_WINDOWS ? '\\' : '/'
export function homePath(...parts: string[]): string {
return [HOME, ...parts].join(SEP)
}
/// Today's spend in the tray is a second tray icon carrying the number as its bitmap. Only
/// the Windows notification area gives us one; the Linux SNI tray has no equivalent, and
/// macOS ships the Swift menubar instead. Where this is false the control is hidden and the
/// Rust command is never called.
export const TRAY_BADGE_SUPPORTED = IS_WINDOWS