Includes provider integrations, settings, NSStatusItem UI, tests, Makefile, and README with screenshot. Co-authored-by: Cursor <cursoragent@cursor.com>
114 lines
3.6 KiB
Swift
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
|
|
}
|
|
}
|