feat(menubar): opt-in compact mode with variable-width status item

UserDefaults key CodeBurnMenubarCompact toggles a tighter menubar
display: no decimals, no leading space, variable width that hugs
the rendered text instead of the fixed 130pt slot.

Closes #129
This commit is contained in:
iamtoruk 2026-04-22 04:33:35 -07:00
parent bc54f85e34
commit e8f8ccc94a
3 changed files with 28 additions and 8 deletions

View file

@ -211,6 +211,14 @@ npx codeburn menubar
One command: downloads the latest `.app`, installs into `~/Applications`, and launches it. Re-run with `--force` to reinstall. Native Swift + SwiftUI app lives in `mac/` (see `mac/README.md` for build details). Shows today's cost with a flame icon, opens a popover with agent tabs, period switcher (Today / 7 Days / 30 Days / Month / All), Trend / Forecast / Pulse / Stats / Plan insights, activity and model breakdowns, optimize findings, and CSV/JSON export. Refreshes live via FSEvents plus a 15-second poll.
**Compact mode** shrinks the menubar item to fit the text, dropping decimals (e.g. `$110` instead of `$110.20`). Opt in with:
```bash
defaults write CodeBurnMenubar CodeBurnMenubarCompact -bool true
```
Relaunch the app to apply. To revert: `defaults delete CodeBurnMenubar CodeBurnMenubarCompact`.
## What it tracks
**13 task categories** classified from tool usage patterns and user message keywords. No LLM calls, fully deterministic.

View file

@ -301,6 +301,11 @@ extension Double {
let state = CurrencyState.shared
return String(format: "\(state.symbol)%.2f", self * state.rate)
}
func asCompactCurrencyWhole() -> String {
let state = CurrencyState.shared
return "\(state.symbol)\(Int((self * state.rate).rounded()))"
}
}
extension Int {

View file

@ -7,6 +7,7 @@ private let nanosPerSecond: UInt64 = 1_000_000_000
private let refreshIntervalNanos: UInt64 = refreshIntervalSeconds * nanosPerSecond
/// Fixed so the popover's anchor point doesn't shift each time today's cost changes.
private let statusItemFixedWidth: CGFloat = 130
private let statusItemCompactWidth: CGFloat = NSStatusItem.variableLength
private let popoverWidth: CGFloat = 360
private let popoverHeight: CGFloat = 660
private let menubarTitleFontSize: CGFloat = 13
@ -120,10 +121,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
// MARK: - Status Item
private var isCompact: Bool {
UserDefaults.standard.bool(forKey: "CodeBurnMenubarCompact")
}
private func setupStatusItem() {
// Fixed width so the popover anchor (and thus popover position) doesn't shift
// every time today's cost or findings badge changes.
statusItem = NSStatusBar.system.statusItem(withLength: statusItemFixedWidth)
let width = isCompact ? statusItemCompactWidth : statusItemFixedWidth
statusItem = NSStatusBar.system.statusItem(withLength: width)
guard let button = statusItem.button else { return }
button.target = self
button.action = #selector(handleButtonClick(_:))
@ -152,20 +156,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
let attachment = NSTextAttachment()
attachment.image = flame
if let size = flame?.size {
// Nudge the image down ~2pt so its visual centre sits on the text baseline mid-line
// rather than riding high. Exact value tuned against SF Pro Display 13pt.
attachment.bounds = CGRect(x: 0, y: -2, width: size.width, height: size.height)
attachment.bounds = CGRect(x: 0, y: -3, width: size.width, height: size.height)
}
let hasPayload = store.todayPayload != nil
let valueText = " " + (store.todayPayload?.current.cost.asCompactCurrency() ?? "$—")
let compact = isCompact
let fallback = compact ? "$-" : "$—"
let formatted = store.todayPayload?.current.cost
let valueText = compact
? (formatted?.asCompactCurrencyWhole() ?? fallback)
: " " + (formatted?.asCompactCurrency() ?? fallback)
let color: NSColor = hasPayload ? .labelColor : .secondaryLabelColor
let composed = NSMutableAttributedString()
composed.append(NSAttributedString(attachment: attachment))
composed.append(NSAttributedString(
string: valueText,
attributes: [.font: font, .foregroundColor: color]
attributes: [.font: font, .foregroundColor: color, .baselineOffset: -1.0]
))
button.attributedTitle = composed
// Force immediate redraw. NSStatusItem sometimes defers the status bar paint for an