import AppKit import SwiftUI enum ProviderIconAsset { static func name(for provider: ProviderID) -> String { switch provider { case .cursor: "ProviderCursor" case .claude: "ProviderClaude" } } } enum MenuBarIconRenderer { /// MenuBarExtra ignores SwiftUI layout and NSImage.size — rasterize to exact points. static func sizedImage(for provider: ProviderID, height: CGFloat = MenuBarMetrics.iconHeight) -> NSImage { let source = loadProviderImage(for: provider) guard let source else { return NSImage(size: NSSize(width: height, height: height)) } var sourceSize = source.size if sourceSize.width <= 0 || sourceSize.height <= 0 { sourceSize = NSSize(width: 24, height: 24) } let aspect = sourceSize.width / sourceSize.height let pointSize = NSSize(width: height * aspect, height: height) let scale = NSScreen.main?.backingScaleFactor ?? 2 let pixelWidth = max(Int((pointSize.width * scale).rounded(.up)), 1) let pixelHeight = max(Int((pointSize.height * scale).rounded(.up)), 1) guard let rep = NSBitmapImageRep( bitmapDataPlanes: nil, pixelsWide: pixelWidth, pixelsHigh: pixelHeight, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0 ) else { return NSImage(size: pointSize) } rep.size = pointSize NSGraphicsContext.saveGraphicsState() if let context = NSGraphicsContext(bitmapImageRep: rep) { NSGraphicsContext.current = context context.imageInterpolation = .high let dest = NSRect(origin: .zero, size: pointSize) let src = NSRect(origin: .zero, size: sourceSize) source.draw( in: dest, from: src, operation: .sourceOver, fraction: 1.0, respectFlipped: false, hints: [.interpolation: NSImageInterpolation.high] ) } NSGraphicsContext.restoreGraphicsState() let image = NSImage(size: pointSize) image.addRepresentation(rep) image.isTemplate = true return image } private static func loadProviderImage(for provider: ProviderID) -> NSImage? { var image: NSImage? NSApp.effectiveAppearance.performAsCurrentDrawingAppearance { image = NSImage(named: ProviderIconAsset.name(for: provider)) } return image } } /// Settings and other in-app UI — keep full size. struct ProviderBrandIcon: View { let provider: ProviderID var size: CGFloat = 18 var body: some View { Image(ProviderIconAsset.name(for: provider)) .resizable() .interpolation(.high) .antialiased(true) .aspectRatio(contentMode: .fit) .frame(width: size, height: size) } } enum MenuBarMetrics { static let iconHeight: CGFloat = 18 static let textSize: CGFloat = 11 static let providerSpacing: CGFloat = 8 static let itemSpacing: CGFloat = 4 static let labelHeight: CGFloat = 20 } struct ProviderMenuBarIconView: View { let providerID: ProviderID let state: ProviderDisplayState? let settings: AppSettings @Environment(\.colorScheme) private var colorScheme private var selectedMetrics: [UsageMetric] { settings.settings(for: providerID).menuBarMetrics } private var badgeText: String? { guard let snapshot = state?.snapshot else { return nil } return snapshot.menuBarDisplay(for: selectedMetrics) } /// Always show icon + text — this app has no Dock icon. private var statusText: String { if state?.isLoading == true { return "…" } if let badgeText { return badgeText } if state?.errorMessage != nil { return "!" } return "—" } var body: some View { HStack(spacing: MenuBarMetrics.itemSpacing) { Image(nsImage: MenuBarIconRenderer.sizedImage(for: providerID)) .renderingMode(.template) .foregroundStyle(.primary) .frame(width: MenuBarMetrics.iconHeight, height: MenuBarMetrics.iconHeight) .opacity(state?.errorMessage != nil ? 0.45 : 1.0) Text(statusText) .font(.system(size: MenuBarMetrics.textSize, weight: .medium)) .foregroundStyle(state?.errorMessage != nil ? .red : .primary) .lineLimit(1) } .fixedSize(horizontal: true, vertical: false) .frame(height: MenuBarMetrics.labelHeight) .id(colorScheme) .help(helpText) } private var helpText: String { if let error = state?.errorMessage { return "\(providerID.displayName): \(error)" } if statusText != "—", statusText != "…" { return "\(providerID.displayName): \(statusText)" } return providerID.displayName } }