import AppKit import CodexBarCore import SwiftUI @MainActor struct MenuContent: View { @Bindable var store: UsageStore @Bindable var settings: SettingsStore let account: AccountInfo let updater: UpdaterProviding let provider: UsageProvider? let actions: MenuActions var body: some View { let descriptor = MenuDescriptor.build( provider: self.provider, store: self.store, settings: self.settings, account: self.account, updateReady: self.updater.updateStatus.isUpdateReady) VStack(alignment: .leading, spacing: 8) { ForEach(Array(descriptor.sections.enumerated()), id: \.offset) { index, section in VStack(alignment: .leading, spacing: 4) { ForEach(Array(section.entries.enumerated()), id: \.offset) { _, entry in self.row(for: entry) } } if index < descriptor.sections.count - 1 { Divider() } } } .padding(.horizontal, 10) .padding(.vertical, 6) .frame(minWidth: 260, alignment: .leading) } @ViewBuilder private func row(for entry: MenuDescriptor.Entry) -> some View { switch entry { case let .text(text, style): switch style { case .headline: Text(text).font(.headline) .accessibilityLabel(text) case .primary: Text(text) .accessibilityLabel(text) case .secondary: Text(text).foregroundStyle(.secondary).font(.footnote) .accessibilityLabel(text) } case let .action(title, action): Button { self.perform(action) } label: { if let icon = self.iconName(for: action) { HStack(spacing: 8) { Image(systemName: icon) .imageScale(.medium) .frame(width: 18, alignment: .center) Text(title) } .foregroundStyle(.primary) .accessibilityElement(children: .combine) .accessibilityLabel(title) } else { Text(title) .accessibilityLabel(title) } } .buttonStyle(.plain) case let .unavailable(title, tooltip): Text(title) .foregroundStyle(.secondary) .help(tooltip ?? "") case let .submenu(title, systemImageName, submenuItems): VStack(alignment: .leading, spacing: 4) { HStack(spacing: 8) { if let systemImageName { Image(systemName: systemImageName) } Text(title).font(.headline) } .accessibilityElement(children: .combine) .accessibilityLabel(title) ForEach(Array(submenuItems.enumerated()), id: \.offset) { _, submenuItem in HStack(spacing: 8) { if submenuItem.isChecked { Image(systemName: "checkmark") .imageScale(.small) .frame(width: 18, alignment: .center) } else { Spacer().frame(width: 18) } Text(submenuItem.title) .foregroundStyle(submenuItem.isEnabled ? .primary : .secondary) } .accessibilityElement(children: .combine) .accessibilityLabel(submenuItem.title) } } case .divider: Divider() } } private func iconName(for action: MenuDescriptor.MenuAction) -> String? { action.systemImageName } private func perform(_ action: MenuDescriptor.MenuAction) { switch action { case .refresh: self.actions.refresh() case .refreshAugmentSession: self.actions.refreshAugmentSession() case .installUpdate: self.actions.installUpdate() case .dashboard: self.actions.openDashboard() case .statusPage: self.actions.openStatusPage() case .changelog: self.actions.openChangelog() case .addCodexAccount: self.actions.addCodexAccount() case .requestCodexSystemPromotion: return case let .addProviderAccount(provider): self.actions.switchAccount(provider) case let .switchAccount(provider): self.actions.switchAccount(provider) case let .openTerminal(command): self.actions.openTerminal(command) case let .loginToProvider(url): if let urlObj = URL(string: url) { NSWorkspace.shared.open(urlObj) } case .settings: self.actions.openSettings() case .about: self.actions.openAbout() case .quit: self.actions.quit() case let .copyError(message): self.actions.copyError(message) case .focusAgentSession: return } } } struct MenuActions { let installUpdate: () -> Void let refresh: () -> Void let refreshAugmentSession: () -> Void let openDashboard: () -> Void let openStatusPage: () -> Void let openChangelog: () -> Void let addCodexAccount: () -> Void let switchAccount: (UsageProvider) -> Void let openTerminal: (String) -> Void let openSettings: () -> Void let openAbout: () -> Void let quit: () -> Void let copyError: (String) -> Void } struct PersistentRefreshRowMetrics: Equatable { static let defaults = Self( rowHeight: 24, selectionHorizontalInset: 5, selectionVerticalInset: 0, selectionCornerRadius: 7, // Align the custom row's image/title frames with native NSMenuItem columns. leadingPadding: 15, trailingPadding: 8, iconWidth: 16, iconSymbolPointSize: 16, iconSymbolWeight: .regular, iconTitleSpacing: 4.5, shortcutFontSize: 13, shortcutXOffset: -9.5, shortcutYOffset: 0) let rowHeight: CGFloat let selectionHorizontalInset: CGFloat let selectionVerticalInset: CGFloat let selectionCornerRadius: CGFloat let leadingPadding: CGFloat let trailingPadding: CGFloat let iconWidth: CGFloat let iconSymbolPointSize: CGFloat let iconSymbolWeight: NSFont.Weight let iconTitleSpacing: CGFloat let shortcutFontSize: CGFloat let shortcutXOffset: CGFloat let shortcutYOffset: CGFloat } @MainActor struct StatusIconView: View { @Bindable var store: UsageStore let provider: UsageProvider var body: some View { Image(nsImage: self.icon) .renderingMode(.template) .interpolation(.none) .accessibilityLabel(self.accessibilityLabel) .accessibilityValue(self.accessibilityValue) } private var accessibilityLabel: String { let descriptor = ProviderDescriptorRegistry.descriptor(for: self.provider) return descriptor.metadata.displayName } private var accessibilityValue: String { let snapshot = self.store.snapshot(for: self.provider) guard let snap = snapshot else { return L("No data") } let remaining = IconRemainingResolver.resolvedRemaining( snapshot: snap, style: self.store.style(for: self.provider)) let primary = remaining.primary let percent = primary.map(Self.accessibilityPercentRemaining) ?? L("Unknown") let stale = self.store.isStale(provider: self.provider) return stale ? "\(percent), \(L("stale data"))" : percent } static func accessibilityPercentRemaining(_ remaining: Double) -> String { String(format: L("%d percent remaining"), Int(remaining.rounded())) } private var icon: NSImage { let now = Date() let snapshot = self.store.snapshot(for: self.provider) let remaining = snapshot.map { IconRemainingResolver.resolvedRemaining( snapshot: $0, style: self.store.style(for: self.provider), now: now) } let creditsProjection = self.store.codexConsumerProjectionIfNeeded( for: self.provider, surface: .menuBar, snapshotOverride: snapshot, now: now) let creditsRemaining = creditsProjection?.menuBarFallback == .creditsBalance ? self.store.codexMenuBarCreditsRemaining( snapshotOverride: snapshot, now: now) : nil return IconRenderer.makeIcon( primaryRemaining: remaining?.primary, weeklyRemaining: remaining?.secondary, creditsRemaining: creditsRemaining, stale: self.store.isStale(provider: self.provider), style: self.store.style(for: self.provider), statusIndicator: self.store.statusIndicator(for: self.provider), hideCritters: self.store.settings.menuBarHidesCritters) } }