import AppKit import SwiftUI import Combine @MainActor public class StatusBarController: NSObject, NSPopoverDelegate { private var statusItem: NSStatusItem private var popover: NSPopover private var store: UsageStore private var cancellables = Set() private var contextMenu: NSMenu! private var hostingController: NSHostingController? private var lastCloseTime: Date? public init(store: UsageStore) { self.store = store self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) self.popover = NSPopover() super.init() setupPopover() setupContextMenu() setupStatusItem() bindStore() } private func setupPopover() { popover.behavior = .transient popover.animates = true popover.appearance = NSAppearance(named: .darkAqua) popover.delegate = self } public func popoverDidClose(_ notification: Notification) { lastCloseTime = Date() popover.contentViewController = nil hostingController = nil } private func setupContextMenu() { contextMenu = NSMenu() let refreshItem = NSMenuItem(title: "Refresh Now", action: #selector(menuRefresh), keyEquivalent: "r") refreshItem.target = self contextMenu.addItem(refreshItem) let openConfigItem = NSMenuItem(title: "Open config.json", action: #selector(menuOpenConfig), keyEquivalent: "c") openConfigItem.target = self contextMenu.addItem(openConfigItem) let openAuthItem = NSMenuItem(title: "Open auth.json", action: #selector(menuOpenAuth), keyEquivalent: "o") openAuthItem.target = self contextMenu.addItem(openAuthItem) contextMenu.addItem(NSMenuItem.separator()) let quitItem = NSMenuItem(title: "Quit UsageTray", action: #selector(menuQuit), keyEquivalent: "q") quitItem.target = self contextMenu.addItem(quitItem) } private func setupStatusItem() { guard let button = statusItem.button else { return } let config = NSImage.SymbolConfiguration(pointSize: 13, weight: .medium) if let image = NSImage(systemSymbolName: "gauge.open.with.lines.needle.33percent", accessibilityDescription: "Usage")?.withSymbolConfiguration(config) { image.isTemplate = true button.image = image } if let font = NSFont(name: "Comic Mono", size: 12) { button.font = font } button.imagePosition = .imageLeading button.action = #selector(handleClick(_:)) button.target = self button.sendAction(on: [.leftMouseUp, .rightMouseUp]) } private func bindStore() { store.$harmonized .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } self.statusItem.button?.title = self.store.statusItemTitle } .store(in: &cancellables) } @objc private func handleClick(_ sender: AnyObject?) { guard let button = statusItem.button else { return } let event = NSApp.currentEvent if event?.type == .rightMouseUp { contextMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button) } else { togglePopover(sender) } } @objc public func togglePopover(_ sender: AnyObject?) { guard let button = statusItem.button else { return } if popover.isShown { popover.performClose(sender) return } if let lastClose = lastCloseTime, Date().timeIntervalSince(lastClose) < 0.25 { return } Task { await store.refresh(force: false) } let hc = NSHostingController(rootView: PopoverContentView(store: store)) self.hostingController = hc popover.contentViewController = hc let v = hc.view v.layoutSubtreeIfNeeded() let fit = v.fittingSize popover.contentSize = NSSize(width: 360, height: max(320, fit.height)) popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) hc.view.window?.makeKey() } @objc private func menuRefresh() { Task { await store.refresh(force: true) } } @objc private func menuOpenConfig() { NSWorkspace.shared.open(AppConfig.configURL) } @objc private func menuOpenAuth() { NSWorkspace.shared.open(AuthStorage.authURL) } @objc private func menuQuit() { NSApplication.shared.terminate(nil) } }