Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
}

@MainActor
private final class ConfiguredMenuBarActionBox: NSObject {
let action: CmuxResolvedConfigAction

init(action: CmuxResolvedConfigAction) {
self.action = action
}
}

private final class MainWindowController: NSWindowController, NSWindowDelegate {
var onClose: (() -> Void)?
var shouldClose: (() -> Bool)?
Expand Down Expand Up @@ -688,6 +697,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private var shortcutMonitor: Any?
private var shortcutDefaultsObserver: NSObjectProtocol?
private var menuBarVisibilityObserver: NSObjectProtocol?
private var configuredMenuBarObserverTokens: [NSObjectProtocol] = []
private var configuredMenuBarTopLevelItems: [NSMenuItem] = []
private var configuredMenuBarActionBoxes: [ConfiguredMenuBarActionBox] = []
private var configuredMenuBarRefreshScheduled = false
private var splitButtonTooltipRefreshScheduled = false
private struct PendingConfiguredShortcutChord {
let firstStroke: ShortcutStroke
Expand Down Expand Up @@ -1595,6 +1608,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
installLifecycleSnapshotObserversIfNeeded()
prepareStartupSessionSnapshotIfNeeded()
startSessionAutosaveTimerIfNeeded()
installConfiguredMenuBarObserversIfNeeded()
#if DEBUG
setupJumpUnreadUITestIfNeeded()
setupTerminalCmdClickUITestIfNeeded()
Expand Down Expand Up @@ -6236,6 +6250,153 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
}

func configuredMenuBarMenusForCommands(preferredWindow: NSWindow? = nil) -> [CmuxResolvedMenuBarMenu] {
guard let store = preferredRegisteredMainWindowContext(preferredWindow: preferredWindow)?.cmuxConfigStore else {
return []
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep custom menus available when no main window exists

When all main windows are closed (a normal state in this app, especially with menu-bar-only usage), configuredMenuBarMenusForCommands returns [] because it requires an existing window context. The next refresh path removes previously injected top-level custom menus and then exits early, so global ui.menuBar commands become inaccessible until a new main window is created through some other entrypoint. This regresses menu-bar command availability in no-window sessions.

Useful? React with 👍 / 👎.

return store.menuBarMenus
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

@discardableResult
func performConfiguredMenuBarAction(
_ action: CmuxResolvedConfigAction,
preferredWindow: NSWindow? = nil
) -> Bool {
let context = preferredRegisteredMainWindowContext(preferredWindow: preferredWindow)
?? createMainWindowContextForConfiguredMenuBarAction()
guard let context else {
NSSound.beep()
return false
}
let preferredWindow = resolvedWindow(for: context) ?? preferredWindow ?? NSApp.keyWindow ?? NSApp.mainWindow
guard executeConfiguredCmuxAction(action, context: context, preferredWindow: preferredWindow) else {
NSSound.beep()
return false
}
return true
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

private func createMainWindowContextForConfiguredMenuBarAction() -> MainWindowContext? {
guard mainWindowContexts.isEmpty else {
return preferredMainWindowContextForWorkspaceCreation(debugSource: "menuBar.configured")
}
let windowId = createMainWindow()
return mainWindowContexts.values.first { $0.windowId == windowId }
}

private func installConfiguredMenuBarObserversIfNeeded() {
guard configuredMenuBarObserverTokens.isEmpty else { return }
let names: [Notification.Name] = [
.cmuxConfigStoreDidChange,
.mainWindowContextsDidChange,
NSWindow.didBecomeKeyNotification,
NSWindow.didBecomeMainNotification,
]
configuredMenuBarObserverTokens = names.map { name in
NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
Task { @MainActor [weak self] in
self?.scheduleConfiguredMenuBarRefresh()
}
}
}
scheduleConfiguredMenuBarRefresh()
}

private func scheduleConfiguredMenuBarRefresh() {
guard !configuredMenuBarRefreshScheduled else { return }
configuredMenuBarRefreshScheduled = true
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.configuredMenuBarRefreshScheduled = false
self.refreshConfiguredMenuBar()
}
}

private func refreshConfiguredMenuBar() {
guard let mainMenu = NSApp.mainMenu else { return }

for item in configuredMenuBarTopLevelItems {
let index = mainMenu.index(of: item)
if index >= 0 {
mainMenu.removeItem(at: index)
}
}
configuredMenuBarTopLevelItems.removeAll()
configuredMenuBarActionBoxes.removeAll()

let menus = configuredMenuBarMenusForCommands(preferredWindow: NSApp.keyWindow ?? NSApp.mainWindow)
guard !menus.isEmpty else { return }

var insertionIndex = configuredMenuBarInsertionIndex(in: mainMenu)
for menu in menus {
let item = NSMenuItem(title: menu.title, action: nil, keyEquivalent: "")
item.submenu = configuredMenuBarMenu(from: menu)
mainMenu.insertItem(item, at: insertionIndex)
configuredMenuBarTopLevelItems.append(item)
insertionIndex += 1
}
}

private func configuredMenuBarInsertionIndex(in mainMenu: NSMenu) -> Int {
let notificationsTitle = String(localized: "menu.notifications.title", defaultValue: "Notifications")
if let notificationsIndex = mainMenu.items.lastIndex(where: { $0.title == notificationsTitle }) {
return notificationsIndex + 1
}
#if DEBUG
if let debugIndex = mainMenu.items.lastIndex(where: { $0.title == "Debug" }) {
return debugIndex
}
#endif
return min(mainMenu.items.count, max(1, mainMenu.items.count - 1))
}

private func configuredMenuBarMenu(from menu: CmuxResolvedMenuBarMenu) -> NSMenu {
let nsMenu = NSMenu(title: menu.title)
for item in menu.items {
switch item {
case .separator:
if !nsMenu.items.isEmpty, nsMenu.items.last?.isSeparatorItem == false {
nsMenu.addItem(.separator())
}
case .submenu(let submenu):
let item = NSMenuItem(title: submenu.title, action: nil, keyEquivalent: "")
item.submenu = configuredMenuBarMenu(from: submenu)
nsMenu.addItem(item)
case .action(let menuAction):
let item = NSMenuItem(
title: menuAction.title,
action: #selector(performConfiguredMenuBarMenuItem(_:)),
keyEquivalent: ""
)
item.target = self
let box = ConfiguredMenuBarActionBox(action: menuAction.action)
configuredMenuBarActionBoxes.append(box)
item.representedObject = box
item.toolTip = menuAction.tooltip
item.image = configuredMenuBarImage(for: menuAction.icon ?? menuAction.action.icon)
nsMenu.addItem(item)
}
}

while nsMenu.items.last?.isSeparatorItem == true {
nsMenu.removeItem(at: nsMenu.items.count - 1)
}
return nsMenu
}

private func configuredMenuBarImage(for icon: CmuxButtonIcon?) -> NSImage? {
guard case .some(.symbol(let symbolName)) = icon else { return nil }
return NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
}

@objc private func performConfiguredMenuBarMenuItem(_ sender: NSMenuItem) {
guard let box = sender.representedObject as? ConfiguredMenuBarActionBox else {
NSSound.beep()
return
}
performConfiguredMenuBarAction(box.action, preferredWindow: NSApp.keyWindow ?? NSApp.mainWindow)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Extract configurable menu-bar construction/dispatch out of AppDelegate.

This block adds more UI menu rendering + action-routing responsibility to an already cross-cutting file. Please move this feature into a focused coordinator (e.g., ConfiguredMenuBarController) and keep AppDelegate as wiring-only.

As per coding guidelines: "Flag Swift files that mix UI rendering, state ownership, persistence, networking, parsing, subprocess/socket protocol, and platform bridge code in one place."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AppDelegate.swift` around lines 6253 - 6398, Move all configured
menu-bar logic out of AppDelegate into a new ConfiguredMenuBarController class:
create ConfiguredMenuBarController and copy the private functions and state that
begin with configuredMenuBar* plus configuredMenuBarMenusForCommands(_:),
performConfiguredMenuBarAction(_:preferredWindow:),
configuredMenuBarImage(for:), configuredMenuBarMenu(from:),
performConfiguredMenuBarMenuItem(_:),
installConfiguredMenuBarObserversIfNeeded(), scheduleConfiguredMenuBarRefresh(),
refreshConfiguredMenuBar(), configuredMenuBarInsertionIndex(in:), and
createMainWindowContextForConfiguredMenuBarAction() and the arrays
configuredMenuBarTopLevelItems/configuredMenuBarActionBoxes/configuredMenuBarObserverTokens/configuredMenuBarRefreshScheduled
into it; make them instance members and resolve dependencies by injecting
required collaborators from AppDelegate (e.g., a CmuxConfigStore provider /
configured menus accessor, mainWindowContexts or a resolver function
preferredRegisteredMainWindowContext(preferredWindow:), createMainWindow(),
resolvedWindow(for:), executeConfiguredCmuxAction(_:context:preferredWindow:)),
and NSApp/main menu access; update the `@objc` selector
performConfiguredMenuBarMenuItem(_:)'s target to the new controller; in
AppDelegate replace the copied implementations with a single stored instance of
ConfiguredMenuBarController, instantiate it during setup, and forward or pass
injected closures/properties as needed so AppDelegate becomes wiring-only.


/// Shows the "Open Folder" panel and creates a workspace for the selected directory.
/// Called from both the SwiftUI menu and `handleCustomShortcut`.
func showOpenFolderPanel() {
Expand Down
Loading
Loading