From ac3fc231a6644a427d5dbf5ab2b151b50362e838 Mon Sep 17 00:00:00 2001 From: imaznation Date: Thu, 21 May 2026 12:39:34 -0700 Subject: [PATCH] menu bar status item (accessory activation policy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kiosk-mode users typically don't want EdgeControl in the Dock or Cmd-Tab — the dashboard window lives on a secondary display and the app is otherwise headless from the main Mac's perspective. Without a menu-bar affordance, though, there's no path to Settings or to quit the app cleanly (the regular menu bar belongs to whatever app is in front, not EdgeControl). Changes: - Switch NSApp activation policy from .regular to .accessory: no Dock icon, no Cmd-Tab entry, no top-of-screen menu bar. - Install an NSStatusItem in the menu bar with a Settings… / Quit menu. Icon is square.grid.2x2.fill at 16pt .semibold — reads instantly as "dashboard" and matches the visual weight of common status items. - Keep the (now unused) buildMainMenu in place so existing Cmd-Q wiring continues to compile; downstream forks that want to switch back to .regular don't have to re-derive the menu structure. Co-Authored-By: Claude Opus 4.7 (1M context) --- Sources/EdgeControl/App/EdgeControlApp.swift | 51 +++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/Sources/EdgeControl/App/EdgeControlApp.swift b/Sources/EdgeControl/App/EdgeControlApp.swift index 2af62ed..f5915e3 100644 --- a/Sources/EdgeControl/App/EdgeControlApp.swift +++ b/Sources/EdgeControl/App/EdgeControlApp.swift @@ -64,6 +64,7 @@ final class EdgeControlAppDelegate: NSObject, NSApplicationDelegate { private let history: MetricsHistory private let pluginManager: PluginManager private let dashboardWindowController = DashboardWindowController() + private var statusItem: NSStatusItem? init(model: AppModel, layoutEngine: LayoutEngine, registry: WidgetRegistry, history: MetricsHistory, pluginManager: PluginManager) { self.model = model @@ -74,8 +75,12 @@ final class EdgeControlAppDelegate: NSObject, NSApplicationDelegate { } func applicationDidFinishLaunching(_ notification: Notification) { - NSApp.setActivationPolicy(.regular) - NSApp.mainMenu = buildMainMenu() + // Accessory policy: no Dock icon, no Cmd-Tab entry. EdgeControl + // is a kiosk dashboard living on a secondary display; the + // menu-bar status item below is the primary user-facing + // affordance on the main Mac. + NSApp.setActivationPolicy(.accessory) + installMenuBarItem() // Pre-configure SettingsWindowController with all dependencies SettingsWindowController.shared.configure( model: model, @@ -119,6 +124,48 @@ final class EdgeControlAppDelegate: NSObject, NSApplicationDelegate { mainMenu.addItem(appMenuItem) return mainMenu } + + // MARK: - Menu bar status item + + private func installMenuBarItem() { + let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + if let button = item.button { + // square.grid.2x2.fill reads instantly as "dashboard" and + // doesn't clash visually with chart-flavored neighbors in + // the menu bar. Explicit 16pt + .semibold matches the + // visual weight of common status items (filled glyphs in + // a 16pt frame would otherwise render small). + let config = NSImage.SymbolConfiguration( + pointSize: 16, + weight: .semibold + ) + let image = NSImage( + systemSymbolName: "square.grid.2x2.fill", + accessibilityDescription: "EdgeControl" + )?.withSymbolConfiguration(config) + image?.isTemplate = true + button.image = image + } + item.menu = buildStatusMenu() + statusItem = item + } + + private func buildStatusMenu() -> NSMenu { + let menu = NSMenu() + menu.addItem(NSMenuItem( + title: "Settings…", + action: #selector(openSettings(_:)), + keyEquivalent: "," + )) + menu.addItem(.separator()) + let quit = NSMenuItem(title: "Quit EdgeControl", action: #selector(quitApp(_:)), keyEquivalent: "q") + menu.addItem(quit) + return menu + } + + @objc private func openSettings(_ sender: Any?) { + SettingsWindowController.shared.show() + } } @main