From dcfb3038a396498d6a791b96a086547e4007024c Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Tue, 28 Apr 2026 22:30:57 +0200 Subject: [PATCH 1/2] Add display-mode setting (dock / menu bar / hidden) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New "Show app in" picker in Settings: - Dock — current behavior, regular activation policy. - Menu Bar — accessory mode with an NSStatusItem that mirrors the Dock menu (Settings, Open Config, Check for Updates, Quit). - Hidden — accessory mode with no status item or Dock icon. Re-launching cmdcmd.app reopens Settings via applicationShouldHandleReopen. Mode is persisted to config.json via the in-place patcher and applied live without restart. Removes the unconditional setActivationPolicy(.regular) at launch so config wins on cold start. Hidden uses .accessory rather than .prohibited so single-instance relaunch behavior keeps working. Co-Authored-By: plyght Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/b4ebf24f.md | 5 ++++ Sources/cmdcmd/Config.swift | 10 ++++++- Sources/cmdcmd/SettingsWindow.swift | 22 +++++++++++++-- Sources/cmdcmd/main.swift | 43 ++++++++++++++++++++++++++++- 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 .changeset/b4ebf24f.md diff --git a/.changeset/b4ebf24f.md b/.changeset/b4ebf24f.md new file mode 100644 index 0000000..922ba02 --- /dev/null +++ b/.changeset/b4ebf24f.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Add a display-mode setting: dock, menu bar, or hidden. diff --git a/Sources/cmdcmd/Config.swift b/Sources/cmdcmd/Config.swift index 8351675..f43d1cd 100644 --- a/Sources/cmdcmd/Config.swift +++ b/Sources/cmdcmd/Config.swift @@ -1,15 +1,23 @@ import Foundation +enum DisplayMode: String, Codable, CaseIterable { + case dock + case menuBar = "menu-bar" + case hidden +} + struct Config: Codable { var animations: Bool var trigger: String? var bindings: [String: Action] var livePreviews: Bool? + var displayMode: DisplayMode? var triggerSpec: String { trigger ?? "cmd-cmd" } var livePreviewsEnabled: Bool { livePreviews ?? true } + var displayModeOrDefault: DisplayMode { displayMode ?? .dock } - static let `default` = Config(animations: true, trigger: nil, bindings: [:], livePreviews: nil) + static let `default` = Config(animations: true, trigger: nil, bindings: [:], livePreviews: nil, displayMode: nil) static var fileURL: URL { URL(fileURLWithPath: NSHomeDirectory()) diff --git a/Sources/cmdcmd/SettingsWindow.swift b/Sources/cmdcmd/SettingsWindow.swift index a985207..a750b08 100644 --- a/Sources/cmdcmd/SettingsWindow.swift +++ b/Sources/cmdcmd/SettingsWindow.swift @@ -11,7 +11,7 @@ final class SettingsWindowController: NSWindowController { init(config: Config) { model = SettingsModel(config: config) let window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 460, height: 320), + contentRect: NSRect(x: 0, y: 0, width: 460, height: 400), styleMask: [.titled, .closable, .miniaturizable], backing: .buffered, defer: false @@ -29,6 +29,7 @@ final class SettingsWindowController: NSWindowController { private final class SettingsModel: ObservableObject { @Published var animations: Bool { didSet { save() } } @Published var livePreviews: Bool { didSet { save() } } + @Published var displayMode: DisplayMode { didSet { save() } } private var base: Config @Published var status: String = "" var onSave: ((Config) -> Void)? @@ -36,6 +37,7 @@ private final class SettingsModel: ObservableObject { init(config: Config) { animations = config.animations livePreviews = config.livePreviewsEnabled + displayMode = config.displayModeOrDefault base = config } @@ -43,10 +45,12 @@ private final class SettingsModel: ObservableObject { var config = base config.animations = animations config.livePreviews = livePreviews + config.displayMode = displayMode do { try Config.patchOnDisk([ ("animations", animations ? "true" : "false"), ("livePreviews", livePreviews ? "true" : "false"), + ("displayMode", "\"\(displayMode.rawValue)\""), ]) base = config onSave?(config) @@ -96,6 +100,20 @@ private struct SettingsRootView: View { } .toggleStyle(.switch) + VStack(alignment: .leading, spacing: 6) { + Text("Show app in").font(.system(size: 13, weight: .medium)) + Picker("", selection: $model.displayMode) { + Text("Dock").tag(DisplayMode.dock) + Text("Menu Bar").tag(DisplayMode.menuBar) + Text("Hidden").tag(DisplayMode.hidden) + } + .labelsHidden() + .pickerStyle(.segmented) + Text("Hidden mode keeps the app running with no Dock or menu bar UI. Re-launch cmdcmd.app to bring Settings back.") + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer(minLength: 0) HStack(spacing: 10) { @@ -108,6 +126,6 @@ private struct SettingsRootView: View { } } .padding(24) - .frame(minWidth: 420, minHeight: 280) + .frame(minWidth: 420, minHeight: 360) } } diff --git a/Sources/cmdcmd/main.swift b/Sources/cmdcmd/main.swift index 414c428..7d6c588 100644 --- a/Sources/cmdcmd/main.swift +++ b/Sources/cmdcmd/main.swift @@ -16,7 +16,6 @@ if let i = args.firstIndex(of: "--render-iconset"), i + 1 < args.count { } let app = NSApplication.shared -app.setActivationPolicy(.regular) app.applicationIconImage = AppIcon.makePlaceholder() final class AppDelegate: NSObject, NSApplicationDelegate { @@ -28,8 +27,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { var settingsFactory: (() -> SettingsWindowController)? private var settingsController: SettingsWindowController? + private var statusItem: NSStatusItem? func applicationDockMenu(_ sender: NSApplication) -> NSMenu? { + return buildAppMenu() + } + + private func buildAppMenu() -> NSMenu { let menu = NSMenu() let settingsItem = NSMenuItem(title: "Settings…", action: #selector(openSettings), keyEquivalent: "") settingsItem.target = self @@ -42,9 +46,44 @@ final class AppDelegate: NSObject, NSApplicationDelegate { keyEquivalent: "") checkItem.target = updaterController menu.addItem(checkItem) + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: "Quit cmdcmd", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) return menu } + func applyDisplayMode(_ mode: DisplayMode) { + switch mode { + case .dock: + removeStatusItem() + NSApp.setActivationPolicy(.regular) + case .menuBar: + NSApp.setActivationPolicy(.accessory) + installStatusItem() + case .hidden: + removeStatusItem() + NSApp.setActivationPolicy(.accessory) + } + } + + private func installStatusItem() { + if statusItem == nil { + let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) + let icon = AppIcon.makePlaceholder() + icon.size = NSSize(width: 18, height: 18) + icon.isTemplate = false + item.button?.image = icon + item.menu = buildAppMenu() + statusItem = item + } + } + + private func removeStatusItem() { + if let s = statusItem { + NSStatusBar.system.removeStatusItem(s) + } + statusItem = nil + } + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { if !flag { openSettings() } return true @@ -73,6 +112,7 @@ app.finishLaunching() _ = try? Config.ensureExists() var appConfig = Config.load() +appDelegate.applyDisplayMode(appConfig.displayModeOrDefault) let tracker = SpaceTracker() let overlay = Overlay(tracker: tracker, config: appConfig) var trigger: AnyObject? @@ -82,6 +122,7 @@ appDelegate.settingsFactory = { controller.onSave = { newConfig in appConfig = newConfig overlay.updateConfig(newConfig) + appDelegate.applyDisplayMode(newConfig.displayModeOrDefault) } return controller } From a8e89dae50f7fa17e34ae10155c2f8a9f8c88ba7 Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Tue, 28 Apr 2026 22:33:38 +0200 Subject: [PATCH 2/2] Use a template SF Symbol for the menu-bar icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The colorful ⌘⌘ app icon doesn't follow the standard monochrome template-image convention. Switched to the system "command" SF Symbol (template = true) so the system tints it correctly across light/dark and accent-color menu bars. Co-Authored-By: Claude Opus 4.7 (1M context) --- Sources/cmdcmd/main.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/cmdcmd/main.swift b/Sources/cmdcmd/main.swift index 7d6c588..7c37e06 100644 --- a/Sources/cmdcmd/main.swift +++ b/Sources/cmdcmd/main.swift @@ -68,9 +68,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private func installStatusItem() { if statusItem == nil { let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) - let icon = AppIcon.makePlaceholder() - icon.size = NSSize(width: 18, height: 18) - icon.isTemplate = false + let icon = NSImage(systemSymbolName: "command", accessibilityDescription: "cmdcmd") + icon?.isTemplate = true item.button?.image = icon item.menu = buildAppMenu() statusItem = item