import AppKit final class StatusItemController { struct Actions { let onLeftClick: () -> Void let onToggleStatusBar: () -> Void let onToggleAppWindow: () -> Void let onRefresh: () -> Void let onQuit: () -> Void } private var statusItem: NSStatusItem? private let actions: Actions private var stateStatusBarVisible: Bool = true private var stateAppWindowVisible: Bool = false init(actions: Actions) { self.actions = actions createStatusItemIfNeeded() } func setState(statusBarVisible: Bool, appWindowVisible: Bool) { self.stateStatusBarVisible = statusBarVisible self.stateAppWindowVisible = appWindowVisible } func hideStatusItem() { if let item = statusItem { NSStatusBar.system.removeStatusItem(item) statusItem = nil } } func showStatusItem() { createStatusItemIfNeeded() } var isVisible: Bool { statusItem != nil } private func createStatusItemIfNeeded() { guard statusItem == nil else { return } let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) statusItem = item if let button = item.button { // Use a proper template image so it always tints/appears in light/dark menu bar button.image = StatusItemController.makeTemplateGlyphImage("P") button.imagePosition = .imageOnly button.target = self button.action = #selector(handleClick) // Only respond to left click; right click intentionally does nothing button.sendAction(on: [.leftMouseUp]) button.toolTip = "Prole Tools — Click for menu" button.appearsDisabled = false } } @objc private func handleClick(_ sender: Any?) { // Always open the context menu on left click showMenu() } private func showMenu() { guard let button = statusItem?.button else { return } let menu = NSMenu(title: "ProleStatus") let statusBarTitle = stateStatusBarVisible ? "Hide Status Bar Icon" : "Show Status Bar Icon" let appWindowTitle = stateAppWindowVisible ? "Hide Main Window" : "Show Main Window" let sbItem = NSMenuItem(title: statusBarTitle, action: #selector(toggleStatusBar), keyEquivalent: "") sbItem.target = self menu.addItem(sbItem) let winItem = NSMenuItem(title: appWindowTitle, action: #selector(toggleAppWindow), keyEquivalent: "") winItem.target = self menu.addItem(winItem) menu.addItem(.separator()) let refreshItem = NSMenuItem(title: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r") refreshItem.target = self menu.addItem(refreshItem) menu.addItem(.separator()) let quitItem = NSMenuItem(title: "Quit Prole Tools", action: #selector(quitApp), keyEquivalent: "q") quitItem.target = self menu.addItem(quitItem) let point = NSPoint(x: 0, y: button.bounds.minY - 3) menu.popUp(positioning: nil, at: point, in: button) } @objc private func toggleStatusBar() { actions.onToggleStatusBar() } @objc private func toggleAppWindow() { actions.onToggleAppWindow() } @objc private func refreshNow() { actions.onRefresh() } @objc private func quitApp() { actions.onQuit() } } // MARK: - Icon drawing extension StatusItemController { /// Create a monochrome template image from a single-character glyph to use as a status bar icon. /// The image is marked as template so macOS tints it appropriately. fileprivate static func makeTemplateGlyphImage(_ char: Character) -> NSImage? { let size = NSSize(width: 18, height: 18) let image = NSImage(size: size) image.lockFocus() defer { image.unlockFocus() } // Clear background (transparent) NSColor.clear.set() NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill() // Draw the glyph centered let string = String(char) let font = NSFont.monospacedSystemFont(ofSize: 14, weight: .bold) let attrs: [NSAttributedString.Key: Any] = [ .font: font, .foregroundColor: NSColor.black // template ignores color; use black base ] let attributed = NSAttributedString(string: string, attributes: attrs) let textSize = attributed.size() let rect = NSRect( x: (size.width - textSize.width) / 2.0, y: (size.height - textSize.height) / 2.0, width: textSize.width, height: textSize.height ) attributed.draw(in: rect) image.isTemplate = true return image } }