macos-ai-menubar-usage/AIMenubarUsage/Services/MenuBarController.swift
Felipe M. 00c4e33171
Initial commit: AI Menubar Usage — macOS menu bar app for Cursor and Claude.
Includes provider integrations, settings, NSStatusItem UI, tests, Makefile, and README with screenshot.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 20:41:40 +02:00

114 lines
3.6 KiB
Swift

import AppKit
import SwiftUI
@MainActor
final class MenuBarController: NSObject {
static let shared = MenuBarController()
private var statusItem: NSStatusItem?
private var popover: NSPopover?
private var labelHostingView: NSHostingView<MenuBarLabelView>?
private let settings = AppSettings.shared
private let refreshService = UsageRefreshService.shared
func install() {
guard statusItem == nil else { return }
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem = item
guard let button = item.button else { return }
button.image = nil
button.title = ""
button.imagePosition = .noImage
let labelView = MenuBarLabelView(refreshService: refreshService, settings: settings)
let hostingView = NSHostingView(rootView: labelView)
hostingView.translatesAutoresizingMaskIntoConstraints = false
labelHostingView = hostingView
button.addSubview(hostingView)
NSLayoutConstraint.activate([
hostingView.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 2),
hostingView.trailingAnchor.constraint(equalTo: button.trailingAnchor, constant: -2),
hostingView.topAnchor.constraint(equalTo: button.topAnchor),
hostingView.bottomAnchor.constraint(equalTo: button.bottomAnchor),
])
hostingView.layoutSubtreeIfNeeded()
button.target = self
button.action = #selector(statusItemClicked(_:))
button.sendAction(on: [.leftMouseUp, .rightMouseUp])
let popover = NSPopover()
popover.behavior = .transient
popover.contentViewController = NSHostingController(
rootView: PopoverView(refreshService: refreshService, settings: settings)
)
self.popover = popover
}
@objc private func statusItemClicked(_ sender: NSStatusBarButton) {
guard let event = NSApp.currentEvent else { return }
if event.type == .rightMouseUp {
showContextMenu(on: sender)
return
}
togglePopover(on: sender)
}
private func togglePopover(on button: NSStatusBarButton) {
guard let popover else { return }
if popover.isShown {
popover.performClose(nil)
return
}
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
popover.contentViewController?.view.window?.makeKey()
}
private func showContextMenu(on button: NSStatusBarButton) {
let menu = NSMenu()
menu.addItem(
withTitle: "Settings…",
action: #selector(openSettings),
keyEquivalent: ","
)
menu.addItem(.separator())
menu.addItem(
withTitle: "Quit AI Menubar Usage",
action: #selector(quitApp),
keyEquivalent: "q"
)
menu.items.forEach { $0.target = self }
statusItem?.menu = menu
button.performClick(nil)
statusItem?.menu = nil
}
@objc private func openSettings() {
popover?.performClose(nil)
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
}
@objc private func quitApp() {
NSApplication.shared.terminate(nil)
}
}
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
MenuBarController.shared.install()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
false
}
}