import AppKit import ApplicationServices import Combine import CoreGraphics import Foundation import MacToolsPluginKit // MARK: - MenuBarHiddenManager // // Central coordinator. Owns the divider, item enumerator, event synthesiser // and icon cache. Three concerns: // // 1. `isEnabled` — toggles the divider; permission-free. The hide mechanism // is purely the expanding NSStatusItem (Thaw / Ice approach). // 2. Layout drag — moves items between sections by synthesising Cmd+drag // events. Requires accessibility + screen recording. // 3. Click forwarding — temporarily moves one hidden item into the visible // section, clicks it, then returns it after its interface closes. // Requires accessibility + screen recording. @MainActor final class MenuBarHiddenManager: ObservableObject { @Published private(set) var snapshot: MenuBarHiddenSnapshot = .empty @Published private(set) var permissions = MenuBarHiddenPermissionsStatus( hasAccessibility: false, hasScreenRecording: false ) let iconCache: MenuBarHiddenIconCache private let store: MenuBarHiddenStore private let divider: MenuBarHiddenDivider private let alwaysHiddenDivider: MenuBarHiddenDivider private let enumerator: MenuBarHiddenItemEnumerator private let events: MenuBarHiddenEventSynthesis private let localization: PluginLocalization private let permissionProvider: () -> MenuBarHiddenPermissionsStatus private struct PendingClickRequest { let item: MenuBarItem let button: CGMouseButton } private var isActive = false private var settingsVisible = false private var hiddenIconsPanelVisible = false private var isDraggingMenuBarItem = false private var currentDragTarget: MenuBarHiddenMenuBarDragTarget = .unknown private var isRecoveringControlItemOrder = false private var shouldRestoreHiddenAfterDrag = false private var dragSessionID = 0 private var currentDisplayID = CGMainDisplayID() private var moveTask: Task? private var clickTask: Task? private var pendingClickRequests: [PendingClickRequest] = [] private var controlItemRecoveryTask: Task? private var controlItemRecoveryPollTask: Task? private var hiddenRestoreTask: Task? private var alwaysHiddenRestoreTask: Task? private var layoutRestoreTask: Task? private var menuBarDragCommitTask: Task? private var menuBarDragCommitSessionID: Int? private var settingsSettleRefreshTask: Task? private var temporaryRehideTask: Task? private var temporaryRehideCancellable: AnyCancellable? private var temporarilyShownItemContexts: [TemporarilyShownItemContext] = [] var hostStatusItemFrameProvider: (() -> NSRect?)? var resetHostStatusItemPosition: (() -> Void)? init( store: MenuBarHiddenStore, localization: PluginLocalization = PluginLocalization(bundle: .main), permissionProvider: @escaping () -> MenuBarHiddenPermissionsStatus = { MenuBarHiddenPermissionsStatus( hasAccessibility: AXIsProcessTrusted(), hasScreenRecording: MenuBarHiddenScreenRecordingPermission.isGranted() ) } ) { self.store = store self.divider = MenuBarHiddenDivider(kind: .hidden) self.alwaysHiddenDivider = MenuBarHiddenDivider(kind: .alwaysHidden) self.enumerator = MenuBarHiddenItemEnumerator() self.events = MenuBarHiddenEventSynthesis() self.iconCache = MenuBarHiddenIconCache() self.localization = localization self.permissionProvider = permissionProvider divider.onFrameChange = { [weak self] in self?.refresh(reason: .dividerMoved) } alwaysHiddenDivider.onFrameChange = { [weak self] in self?.refresh(reason: .dividerMoved) } } #if DEBUG func replaceSnapshotForTesting( visibleItems: [MenuBarItem] = [], hiddenItems: [MenuBarItem] = [], alwaysHiddenItems: [MenuBarItem] = [], permissions: MenuBarHiddenPermissionsStatus = MenuBarHiddenPermissionsStatus( hasAccessibility: true, hasScreenRecording: true ) ) { snapshot = MenuBarHiddenSnapshot( visibleItems: visibleItems, hiddenItems: hiddenItems, alwaysHiddenItems: alwaysHiddenItems, permissions: permissions ) } #endif // MARK: - Lifecycle func activate() { guard !isActive else { return } isActive = true refreshPermissions() if store.isEnabled { installAndExpand() } else { installAndShow() } updateAlwaysHiddenDividerVisibility() rebuildSnapshot() recoverControlItemOrderIfNeeded(sessionID: nil) scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(700)) scheduleAlwaysHiddenItemsRestoreIfNeeded(delay: .milliseconds(600)) } func deactivate() { guard isActive else { return } isActive = false moveTask?.cancel(); moveTask = nil clickTask?.cancel(); clickTask = nil pendingClickRequests.removeAll() controlItemRecoveryTask?.cancel(); controlItemRecoveryTask = nil controlItemRecoveryPollTask?.cancel(); controlItemRecoveryPollTask = nil hiddenRestoreTask?.cancel(); hiddenRestoreTask = nil alwaysHiddenRestoreTask?.cancel(); alwaysHiddenRestoreTask = nil layoutRestoreTask?.cancel(); layoutRestoreTask = nil cancelMenuBarDragCommit() settingsSettleRefreshTask?.cancel(); settingsSettleRefreshTask = nil temporaryRehideTask?.cancel(); temporaryRehideTask = nil temporaryRehideCancellable?.cancel(); temporaryRehideCancellable = nil temporarilyShownItemContexts.removeAll() isDraggingMenuBarItem = false currentDragTarget = .unknown isRecoveringControlItemOrder = false shouldRestoreHiddenAfterDrag = false invalidateDragSession() divider.uninstall() alwaysHiddenDivider.uninstall() } // MARK: - Toggle (Function 1, no permissions required) var isEnabled: Bool { get { store.isEnabled } set { guard store.isEnabled != newValue else { return } hiddenRestoreTask?.cancel() hiddenRestoreTask = nil layoutRestoreTask?.cancel() layoutRestoreTask = nil shouldRestoreHiddenAfterDrag = false store.isEnabled = newValue if newValue { installAndExpand() } else { if !divider.isInstalled { divider.install() } divider.showSection(isDragging: false) } rebuildSnapshot() scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(500)) } } var isAlwaysHiddenEnabled: Bool { get { store.isAlwaysHiddenEnabled } set { guard store.isAlwaysHiddenEnabled != newValue else { return } alwaysHiddenRestoreTask?.cancel() alwaysHiddenRestoreTask = nil guard permissions.canManageItems || !newValue else { store.isAlwaysHiddenEnabled = false updateAlwaysHiddenDividerVisibility() rebuildSnapshot() refreshIconsIfUIVisible() return } store.isAlwaysHiddenEnabled = newValue updateAlwaysHiddenDividerVisibility() rebuildSnapshot() if newValue { scheduleAlwaysHiddenItemsRestoreIfNeeded(delay: .milliseconds(500)) } scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(500)) refreshIconsIfUIVisible() } } var showsHiddenIconsInPanel: Bool { get { store.showsHiddenIconsInPanel } set { guard store.showsHiddenIconsInPanel != newValue else { return } guard permissions.canManageItems || !newValue else { store.showsHiddenIconsInPanel = false return } store.showsHiddenIconsInPanel = newValue } } var canShowHiddenIconsInPanel: Bool { permissions.canManageItems && store.showsHiddenIconsInPanel } // MARK: - Refresh func refresh(reason: MenuBarHiddenRefreshReason) { guard isActive else { return } MenuBarHiddenLog.plugin.debug("refresh: \(reason.description)") refreshPermissions() rebuildSnapshot() if !isMenuBarDragCommitPending { scheduleAlwaysHiddenItemsRestoreIfNeeded() scheduleStoredLayoutRestoreIfNeeded() } refreshIconsIfUIVisible() } func setSettingsVisible(_ visible: Bool) { settingsVisible = visible settingsSettleRefreshTask?.cancel() settingsSettleRefreshTask = nil if visible { refresh(reason: .settingsAppeared) recoverControlItemOrderIfNeeded(sessionID: nil) settingsSettleRefreshTask = Task { [weak self] in // Matches Thaw's settling delay before cache refresh: freshly // shown NSStatusItem windows can report stale bounds briefly. try? await Task.sleep(for: .milliseconds(500)) guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } self?.refresh(reason: .settingsAppeared) } } } } func setHiddenIconsPanelVisible(_ visible: Bool) { hiddenIconsPanelVisible = visible if visible { refresh(reason: .hiddenIconsPanelAppeared) } } func setDraggingMenuBarItem(_ dragging: Bool, startLocation: NSPoint?) { guard isDraggingMenuBarItem != dragging else { return } isDraggingMenuBarItem = dragging if dragging { let shouldRestoreHidden = shouldRestoreHiddenAfterDrag || store.isEnabled invalidateDragSession() let sessionID = dragSessionID currentDragTarget = dragTarget(at: startLocation) cancelMenuBarDragCommit() hiddenRestoreTask?.cancel() hiddenRestoreTask = nil layoutRestoreTask?.cancel() layoutRestoreTask = nil controlItemRecoveryTask?.cancel() controlItemRecoveryTask = nil isRecoveringControlItemOrder = false shouldRestoreHiddenAfterDrag = shouldRestoreHidden if store.isEnabled { store.isEnabled = false } installAndShow() divider.showSection(isDragging: true) updateAlwaysHiddenDividerVisibility() refresh(reason: .dragStarted) rebuildSnapshot() startControlItemOrderRecoveryPolling(sessionID: sessionID) } else { let sessionID = dragSessionID stopControlItemOrderRecoveryPolling() scheduleMenuBarDragCommit(target: currentDragTarget, sessionID: sessionID) currentDragTarget = .unknown } } // MARK: - Move item (Function 2, requires both permissions) func moveItem( id: MenuBarItemTag, to section: MenuBarHiddenSection, placement: MenuBarHiddenMovePlacement ) { guard permissions.canManageItems else { MenuBarHiddenLog.plugin.debug("moveItem ignored — missing permissions") return } guard let item = snapshot.allItems.first(where: { $0.tag == id }) else { return } guard !MenuBarHiddenLayoutPolicy.shouldRejectMoveToSection(item: item, section: section) else { recoverHostIconIfNeeded() MenuBarHiddenLog.plugin.debug("moveItem ignored — item cannot move to \(section.rawValue)") return } alwaysHiddenRestoreTask?.cancel() alwaysHiddenRestoreTask = nil layoutRestoreTask?.cancel() layoutRestoreTask = nil moveTask?.cancel() moveTask = Task { [weak self] in guard let self else { return } let moved = await self.performMove(item: item, toSection: section, placement: placement) guard moved else { return } self.updateStoredLayoutAfterMove( for: item, movedItem: self.snapshotItem(matching: item, in: section), destination: section ) } } // MARK: - Click forwarding (Function 3, requires both permissions) func clickItem(_ item: MenuBarItem, button: CGMouseButton) { guard permissions.canManageItems else { return } pendingClickRequests.append(PendingClickRequest(item: item, button: button)) startClickProcessingIfNeeded() } private func startClickProcessingIfNeeded() { guard clickTask == nil else { return } clickTask = Task { [weak self] in await self?.processPendingClickRequests() } } private func processPendingClickRequests() async { while !Task.isCancelled { guard !pendingClickRequests.isEmpty else { clickTask = nil return } let request = pendingClickRequests.removeFirst() await performClick(item: request.item, button: request.button) } pendingClickRequests.removeAll() clickTask = nil } // MARK: - Permissions func refreshPermissions() { let current = currentPermissions() guard permissions != current else { return } permissions = current if !current.canManageItems { if store.isAlwaysHiddenEnabled { store.isAlwaysHiddenEnabled = false updateAlwaysHiddenDividerVisibility() rebuildSnapshot() } if store.showsHiddenIconsInPanel { store.showsHiddenIconsInPanel = false } } } func currentPermissions() -> MenuBarHiddenPermissionsStatus { permissionProvider() } // MARK: - Private private func installAndExpand() { if !divider.isInstalled { divider.install() } if visibleControlItemStoredPositionNeedsRecovery() { divider.showSection(isDragging: false) scheduleHostIconRecovery(sessionID: nil) return } if isDraggingMenuBarItem { divider.showSection(isDragging: true) } else { divider.hideSection() } updateAlwaysHiddenDividerVisibility() } private func installAndShow() { if !divider.isInstalled { divider.install() } divider.showSection(isDragging: false) updateAlwaysHiddenDividerVisibility() } private func updateAlwaysHiddenDividerVisibility() { guard isActive else { return } if store.isAlwaysHiddenEnabled { if !alwaysHiddenDivider.isInstalled { alwaysHiddenDivider.install() } alwaysHiddenDivider.hideSection() recoverAlwaysHiddenDividerIfNeeded(sessionID: nil) } else { alwaysHiddenDivider.uninstall() } } private func invalidateDragSession() { dragSessionID &+= 1 } private func rebuildSnapshot() { divider.refreshWindowID() alwaysHiddenDivider.refreshWindowID() let result = enumerateMenuBarItems( hiddenDividerWindowID: divider.isInstalled ? divider.windowID : nil, hiddenDividerFrame: divider.isInstalled ? divider.screenFrame : nil, alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil, alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil, excludedWindowIDs: excludedControlWindowIDs() ) let allItems = result.items currentDisplayID = result.displayID guard let hiddenDividerBounds = result.hiddenDividerBounds else { snapshot = MenuBarHiddenSnapshot( visibleItems: [], hiddenItems: [], alwaysHiddenItems: [], permissions: permissions ) return } let layoutItems = MenuBarHiddenLayoutPolicy.layoutEditorItems(from: allItems) let alwaysHiddenDividerBounds = store.isAlwaysHiddenEnabled ? result.alwaysHiddenDividerBounds : nil let visibleItems = MenuBarHiddenLayoutPolicy.visibleItems( from: layoutItems, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) let hiddenItems = MenuBarHiddenLayoutPolicy.hiddenItems( from: layoutItems, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) let alwaysHiddenItems = MenuBarHiddenLayoutPolicy.alwaysHiddenItems( from: layoutItems, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) snapshot = MenuBarHiddenSnapshot( visibleItems: visibleItems, hiddenItems: hiddenItems, alwaysHiddenItems: alwaysHiddenItems, permissions: permissions ) } private func refreshIconsIfUIVisible() { guard settingsVisible || hiddenIconsPanelVisible else { return } guard permissions.canManageItems else { return } iconCache.refresh( groups: [snapshot.visibleItems, snapshot.hiddenItems, snapshot.alwaysHiddenItems], displayID: currentDisplayID ) } private func excludedControlWindowIDs() -> Set { var ids = Set() if divider.isInstalled { ids.formUnion(divider.hiddenControlWindowIDs) } if alwaysHiddenDivider.isInstalled { ids.formUnion(alwaysHiddenDivider.hiddenControlWindowIDs) } return ids } private func enumerateMenuBarItems( hiddenDividerWindowID: CGWindowID?, hiddenDividerFrame: NSRect?, alwaysHiddenDividerWindowID: CGWindowID?, alwaysHiddenDividerFrame: NSRect?, excludedWindowIDs: Set ) -> MenuBarHiddenEnumeratorResult { enumerator.enumerate( hiddenDividerWindowID: hiddenDividerWindowID, hiddenDividerFrame: hiddenDividerFrame, alwaysHiddenDividerWindowID: alwaysHiddenDividerWindowID, alwaysHiddenDividerFrame: alwaysHiddenDividerFrame, excludedWindowIDs: excludedWindowIDs, canResolveSourcePIDs: MenuBarHiddenSourcePIDResolutionPolicy.canResolve( permissions: permissions ) ) } private func localizedDescription(for error: Error) -> String { if let eventError = error as? MenuBarHiddenEventError { return eventError.localizedDescription(localization: localization) } return error.localizedDescription } // MARK: - Move private func performMove( item: MenuBarItem, toSection: MenuBarHiddenSection, placement: MenuBarHiddenMovePlacement ) async -> Bool { await events.waitForUserInputPause() let live = enumerateMenuBarItems( hiddenDividerWindowID: divider.windowID, hiddenDividerFrame: divider.screenFrame, alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil, alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil, excludedWindowIDs: excludedControlWindowIDs() ) guard let liveItem = live.items.first(where: { $0.tag == item.tag }), let target = computeMoveTarget( section: toSection, placement: placement, items: live.items, hiddenDividerBounds: live.hiddenDividerBounds, alwaysHiddenDividerBounds: live.alwaysHiddenDividerBounds ) else { MenuBarHiddenLog.plugin.warning("performMove: could not resolve live item or target X") return false } do { try await events.move(item: liveItem, to: target) } catch { MenuBarHiddenLog.plugin.error("performMove failed: \(self.localizedDescription(for: error))") return false } try? await Task.sleep(for: .milliseconds(300)) rebuildSnapshot() recoverHostIconIfNeeded() refreshIconsIfUIVisible() return snapshotItem(matching: item, in: toSection) != nil } private func snapshotItem(matching item: MenuBarItem, in section: MenuBarHiddenSection) -> MenuBarItem? { let items: [MenuBarItem] switch section { case .visible: items = snapshot.visibleItems case .hidden: items = snapshot.hiddenItems case .alwaysHidden: items = snapshot.alwaysHiddenItems } return items.first { $0.windowID == item.windowID } ?? items.first { $0.tag.matchesIgnoringWindowID(item.tag) && ($0.sourcePID ?? $0.ownerPID) == (item.sourcePID ?? item.ownerPID) } } private func updateStoredLayoutAfterMove( for originalItem: MenuBarItem, movedItem: MenuBarItem?, destination: MenuBarHiddenSection ) { switch destination { case .alwaysHidden: store.recordAlwaysHiddenItem((movedItem ?? originalItem).tag) case .visible, .hidden: if store.isAlwaysHiddenEnabled { store.removeAlwaysHiddenItem(originalItem.tag) if let movedItem { store.removeAlwaysHiddenItem(movedItem.tag) } } } recordCurrentVisibleHiddenLayout() scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(500)) } private func prepareStoredVisibleHiddenLayoutIfNeeded() { guard permissions.canManageItems else { return } guard !snapshot.allItems.isEmpty else { return } guard !isDraggingMenuBarItem, !isRecoveringControlItemOrder else { return } guard temporarilyShownItemContexts.isEmpty else { return } if store.hasVisibleHiddenLayout { appendNewItemsToStoredVisibleHiddenLayout() } else { recordCurrentVisibleHiddenLayout() } } private func appendNewItemsToStoredVisibleHiddenLayout() { let layout = storedLayout() let next = MenuBarHiddenStoredLayoutPolicy.appendNewItemsToHiddenByDefault( items: snapshot.allItems, storedLayout: layout ) guard next.visibleItemStableKeys != store.visibleItemStableKeys || next.hiddenItemStableKeys != store.hiddenItemStableKeys else { return } store.recordVisibleHiddenLayout( visibleKeys: next.visibleItemStableKeys, hiddenKeys: next.hiddenItemStableKeys ) } private func recordCurrentVisibleHiddenLayout() { let next = MenuBarHiddenStoredLayoutPolicy.visibleHiddenLayout( visibleItems: snapshot.visibleItems, hiddenItems: snapshot.hiddenItems, alwaysHiddenItems: snapshot.alwaysHiddenItems, previousVisibleItemStableKeys: store.visibleItemStableKeys, previousHiddenItemStableKeys: store.hiddenItemStableKeys ) store.recordVisibleHiddenLayout( visibleKeys: next.visibleItemStableKeys, hiddenKeys: next.hiddenItemStableKeys ) } private func storedLayout() -> MenuBarHiddenStoredLayout { MenuBarHiddenStoredLayout( visibleItemStableKeys: store.visibleItemStableKeys, hiddenItemStableKeys: store.hiddenItemStableKeys, alwaysHiddenItemStableKeys: store.alwaysHiddenItemStableKeys, isAlwaysHiddenEnabled: store.isAlwaysHiddenEnabled ) } private func scheduleStoredLayoutRestoreIfNeeded(delay: Duration = .milliseconds(300)) { prepareStoredVisibleHiddenLayoutIfNeeded() guard canRestoreStoredVisibleHiddenLayout else { layoutRestoreTask?.cancel() layoutRestoreTask = nil return } guard !storedVisibleHiddenRestoreCandidates().isEmpty else { layoutRestoreTask?.cancel() layoutRestoreTask = nil return } layoutRestoreTask?.cancel() layoutRestoreTask = Task { [weak self] in try? await Task.sleep(for: delay) guard !Task.isCancelled else { return } guard let self else { return } await self.restoreStoredVisibleHiddenLayout() } } private func restoreStoredVisibleHiddenLayout() async { guard canRestoreStoredVisibleHiddenLayout else { layoutRestoreTask = nil return } let candidates = storedVisibleHiddenRestoreCandidates() guard !candidates.isEmpty else { layoutRestoreTask = nil return } var restoredCount = 0 for candidate in candidates { guard !Task.isCancelled else { layoutRestoreTask = nil return } guard canRestoreStoredVisibleHiddenLayout else { scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(600)) return } let placement = storedVisibleHiddenPlacement( for: candidate.item, in: candidate.section ) if await performMove(item: candidate.item, toSection: candidate.section, placement: placement) { restoredCount += 1 } try? await Task.sleep(for: .milliseconds(100)) } layoutRestoreTask = nil if restoredCount > 0, !storedVisibleHiddenRestoreCandidates().isEmpty { scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(900)) } } private var canRestoreStoredVisibleHiddenLayout: Bool { isActive && store.hasVisibleHiddenLayout && permissions.canManageItems && divider.isInstalled && !isDraggingMenuBarItem && !isRecoveringControlItemOrder && temporarilyShownItemContexts.isEmpty } private struct StoredVisibleHiddenRestoreCandidate { let item: MenuBarItem let section: MenuBarHiddenSection } private func storedVisibleHiddenRestoreCandidates() -> [StoredVisibleHiddenRestoreCandidate] { let layout = storedLayout() let currentSections = currentVisibleHiddenSectionsByStableKey() let keyedItems = Dictionary(grouping: snapshot.allItems, by: { $0.tag.stableKey }) .compactMapValues(\.first) return (layout.hiddenItemStableKeys + layout.visibleItemStableKeys).compactMap { key in guard let item = keyedItems[key] else { return nil } let desiredSection = MenuBarHiddenStoredLayoutPolicy.desiredSection( for: item, storedLayout: layout ) guard desiredSection != .alwaysHidden else { return nil } guard currentSections[key] != desiredSection else { return nil } guard !MenuBarHiddenLayoutPolicy.shouldRejectMoveToSection(item: item, section: desiredSection) else { return nil } return StoredVisibleHiddenRestoreCandidate(item: item, section: desiredSection) } } private func currentVisibleHiddenSectionsByStableKey() -> [String: MenuBarHiddenSection] { var sections: [String: MenuBarHiddenSection] = [:] for item in snapshot.visibleItems { sections[item.tag.stableKey] = .visible } for item in snapshot.hiddenItems { sections[item.tag.stableKey] = .hidden } for item in snapshot.alwaysHiddenItems { sections[item.tag.stableKey] = .alwaysHidden } return sections } private func storedVisibleHiddenPlacement( for item: MenuBarItem, in section: MenuBarHiddenSection ) -> MenuBarHiddenMovePlacement { let sectionKeys: [String] let sectionItems: [MenuBarItem] switch section { case .visible: sectionKeys = store.visibleItemStableKeys sectionItems = snapshot.visibleItems case .hidden: sectionKeys = store.hiddenItemStableKeys sectionItems = snapshot.hiddenItems case .alwaysHidden: return .end } guard let index = sectionKeys.firstIndex(of: item.tag.stableKey) else { return .end } let itemsByKey = Dictionary(grouping: sectionItems, by: { $0.tag.stableKey }) .compactMapValues(\.first) for nextKey in sectionKeys.dropFirst(index + 1) { if let nextItem = itemsByKey[nextKey] { return .before(nextItem.tag) } } for previousKey in sectionKeys[.. 0, !alwaysHiddenRestoreCandidates().isEmpty { scheduleAlwaysHiddenItemsRestoreIfNeeded(delay: .milliseconds(750)) } } private var canRestoreAlwaysHiddenItems: Bool { isActive && store.isAlwaysHiddenEnabled && permissions.canManageItems && alwaysHiddenDivider.isInstalled && !isDraggingMenuBarItem && !isRecoveringControlItemOrder && temporarilyShownItemContexts.isEmpty } private func alwaysHiddenRestoreCandidates() -> [MenuBarItem] { MenuBarHiddenAlwaysHiddenRestorePolicy.restoreCandidates( snapshot: snapshot, recordedStableKeys: store.alwaysHiddenItemStableKeys ) } private func computeMoveTarget( section: MenuBarHiddenSection, placement: MenuBarHiddenMovePlacement, items: [MenuBarItem], hiddenDividerBounds: CGRect?, alwaysHiddenDividerBounds: CGRect? ) -> MenuBarHiddenResolvedMoveTarget? { guard let hiddenDivider = hiddenDividerBounds else { return nil } func target(_ item: MenuBarItem, x: CGFloat) -> MenuBarHiddenResolvedMoveTarget { MenuBarHiddenResolvedMoveTarget( point: CGPoint(x: x, y: item.bounds.minY), windowID: item.windowID ) } func target(x: CGFloat, y: CGFloat, windowID: CGWindowID?) -> MenuBarHiddenResolvedMoveTarget { MenuBarHiddenResolvedMoveTarget( point: CGPoint(x: x, y: y), windowID: windowID ) } switch (section, placement) { case (.hidden, .end): if let alwaysHiddenDividerBounds { return target( x: alwaysHiddenDividerBounds.maxX, y: alwaysHiddenDividerBounds.minY, windowID: alwaysHiddenDivider.windowID ) } return target(x: hiddenDivider.minX, y: hiddenDivider.minY, windowID: divider.windowID) case (.hidden, .before(let tag)): guard let item = items.first(where: { $0.tag == tag }) else { return nil } return target(item, x: item.bounds.minX) case (.hidden, .after(let tag)): guard let item = items.first(where: { $0.tag == tag }) else { return nil } return target(item, x: item.bounds.maxX) case (.alwaysHidden, .end): if let alwaysHiddenDividerBounds { return target( x: alwaysHiddenDividerBounds.minX, y: alwaysHiddenDividerBounds.minY, windowID: alwaysHiddenDivider.windowID ) } return target(x: hiddenDivider.minX, y: hiddenDivider.minY, windowID: divider.windowID) case (.alwaysHidden, .before(let tag)): guard let item = items.first(where: { $0.tag == tag }) else { return nil } return target(item, x: item.bounds.minX) case (.alwaysHidden, .after(let tag)): guard let item = items.first(where: { $0.tag == tag }) else { return nil } return target(item, x: item.bounds.maxX) case (.visible, .end): return target(x: hiddenDivider.maxX, y: hiddenDivider.minY, windowID: divider.windowID) case (.visible, .before(let tag)): guard let item = items.first(where: { $0.tag == tag }) else { return nil } return target(item, x: item.bounds.minX) case (.visible, .after(let tag)): guard let item = items.first(where: { $0.tag == tag }) else { return nil } return target(item, x: item.bounds.maxX) } } // MARK: - Click forwarding private final class TemporarilyShownItemContext { let identity: MenuBarHiddenTemporaryItemIdentity let ownerPID: pid_t let returnDestination: MenuBarHiddenReturnDestination let primaryReturnAnchor: MenuBarHiddenReturnAnchor? let fallbackReturnAnchor: MenuBarHiddenReturnAnchor? let interfaceOwner: MenuBarItem var interfaceWindowID: CGWindowID? private var resolvedPopupDetectionPID: pid_t let firstShownDate = Date() var interfaceDiscoveryDeadline: Date { firstShownDate.addingTimeInterval(1) } var popupDetectionPID: pid_t { resolvedPopupDetectionPID } var rehideAttempts = 0 var notFoundAttempts = 0 var nextReturnAttemptDate = Date.distantPast init( item: MenuBarItem, returnDestination: MenuBarHiddenReturnDestination, primaryReturnAnchor: MenuBarHiddenReturnAnchor?, fallbackReturnAnchor: MenuBarHiddenReturnAnchor? ) { self.identity = MenuBarHiddenTemporaryItemIdentity(item: item) self.ownerPID = item.ownerPID self.interfaceOwner = item self.returnDestination = returnDestination self.primaryReturnAnchor = primaryReturnAnchor self.fallbackReturnAnchor = fallbackReturnAnchor self.resolvedPopupDetectionPID = MenuBarHiddenTemporaryInterfacePolicy.popupDetectionPID( sourcePID: item.sourcePID, ownerPID: item.ownerPID ) } func updatePopupDetectionPID(from item: MenuBarItem) { guard let sourcePID = item.sourcePID else { return } resolvedPopupDetectionPID = sourcePID } func scheduleReturnRetry(after delay: TimeInterval) { nextReturnAttemptDate = Date().addingTimeInterval(delay) } } private enum TemporaryRehideResult { case returned case notFound case retryLater } private enum TemporaryRehideReadiness { case ready case waitingForInterface case waitingForUserInput case waitingForRetry } private struct LiveClickResolution { let result: MenuBarHiddenEnumeratorResult let layoutItems: [MenuBarItem] let item: MenuBarItem } private func performClick(item: MenuBarItem, button: CGMouseButton) async { var shouldRunTemporaryRehideLoopOnExit = false defer { if shouldRunTemporaryRehideLoopOnExit, !temporarilyShownItemContexts.isEmpty { startTemporaryRehideLoop() } } guard var resolution = liveClickResolution(matching: item) else { MenuBarHiddenLog.plugin.warning("performClick: missing live item \(item.tag.stableKey)") return } if resolution.item.isOnScreen { shouldRunTemporaryRehideLoopOnExit = await clickOnScreenItem( resolution.item, button: button ) return } if !temporarilyShownItemContexts.isEmpty { guard await prepareForTemporaryShow(replacingExistingContextsUsing: resolution.result) else { MenuBarHiddenLog.plugin.warning( "performClick: previous temporary item is still pending; skipping temporary show for \(item.tag.stableKey)" ) return } guard let refreshedResolution = liveClickResolution(matching: item) else { MenuBarHiddenLog.plugin.warning("performClick: missing live item after temporary cleanup \(item.tag.stableKey)") return } resolution = refreshedResolution if resolution.item.isOnScreen { shouldRunTemporaryRehideLoopOnExit = await clickOnScreenItem( resolution.item, button: button ) return } } guard let hiddenDividerBounds = resolution.result.hiddenDividerBounds, let originalSection = originalSection( for: resolution.item, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: resolution.result.alwaysHiddenDividerBounds ), let returnDestination = returnDestination( for: resolution.item, section: originalSection, items: resolution.layoutItems, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: resolution.result.alwaysHiddenDividerBounds ), let showTarget = temporaryShowTarget( items: resolution.result.items, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: resolution.result.alwaysHiddenDividerBounds ) else { MenuBarHiddenLog.plugin.warning("performClick: could not resolve temporary show target") return } let temporaryContext = TemporarilyShownItemContext( item: resolution.item, returnDestination: returnDestination.destination, primaryReturnAnchor: returnDestination.primaryAnchor, fallbackReturnAnchor: returnDestination.fallbackAnchor ) do { try await events.move(item: resolution.item, to: showTarget) } catch { MenuBarHiddenLog.plugin.error("temporary show failed: \(self.localizedDescription(for: error))") return } temporarilyShownItemContexts = [temporaryContext] shouldRunTemporaryRehideLoopOnExit = true let clickItem = await refreshedClickItem( matching: resolution.item, previousBounds: resolution.item.bounds ) temporaryContext.updatePopupDetectionPID(from: clickItem) try? await Task.sleep(for: .milliseconds(25)) guard MenuBarHiddenWindowServer.isWindowOnScreen(clickItem.windowID) else { MenuBarHiddenLog.plugin.warning("performClick: temporary show did not make item visible \(resolution.item.tag.stableKey)") return } let baselineWindowIDs = Set(MenuBarHiddenWindowServer.onScreenWindowIDs()) do { try await events.click(item: clickItem, button: button) temporaryContext.interfaceWindowID = await openedTemporaryInterfaceWindowID( afterClickBy: clickItem, baselineWindowIDs: baselineWindowIDs ) } catch { MenuBarHiddenLog.plugin.error("click failed: \(self.localizedDescription(for: error))") } rebuildSnapshot() refreshIconsIfUIVisible() } private func liveClickResolution(matching item: MenuBarItem) -> LiveClickResolution? { let result = enumerateMenuBarItems( hiddenDividerWindowID: divider.windowID, hiddenDividerFrame: divider.screenFrame, alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil, alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil, excludedWindowIDs: excludedControlWindowIDs() ) let layoutItems = MenuBarHiddenLayoutPolicy.layoutEditorItems(from: result.items) guard let liveItem = resolvedLiveItem(matching: item, in: layoutItems) else { return nil } return LiveClickResolution( result: result, layoutItems: layoutItems, item: liveItem ) } private func clickOnScreenItem(_ item: MenuBarItem, button: CGMouseButton) async -> Bool { let baselineWindowIDs = Set(MenuBarHiddenWindowServer.onScreenWindowIDs()) do { try await events.click(item: item, button: button) guard let context = temporarilyShownItemContexts.first(where: { contextMatchesItem($0, item: item) }) else { return false } context.updatePopupDetectionPID(from: item) context.interfaceWindowID = await openedTemporaryInterfaceWindowID( afterClickBy: item, baselineWindowIDs: baselineWindowIDs ) return true } catch { MenuBarHiddenLog.plugin.error("click failed: \(self.localizedDescription(for: error))") return false } } private func prepareForTemporaryShow( replacingExistingContextsUsing result: MenuBarHiddenEnumeratorResult ) async -> Bool { guard !temporarilyShownItemContexts.isEmpty else { return true } stopTemporaryRehideLoop() discardReturnedOrMissingTemporaryContexts(using: result) guard !temporarilyShownItemContexts.isEmpty else { return true } await rehideTemporarilyShownItems( force: true, isCalledFromTemporarilyShow: true, discardsMissingItems: true ) return temporarilyShownItemContexts.isEmpty } private func discardReturnedOrMissingTemporaryContexts(using result: MenuBarHiddenEnumeratorResult) { guard !temporarilyShownItemContexts.isEmpty else { return } guard let hiddenDividerBounds = result.hiddenDividerBounds else { return } temporarilyShownItemContexts.removeAll { context in guard let liveItem = result.items.first(where: { contextMatchesItem(context, item: $0) }) else { return !temporaryContextWindowIsVisible(context) } return MenuBarHiddenLayoutPolicy.section( for: liveItem, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: result.alwaysHiddenDividerBounds ) == context.returnDestination.section } } private func resolvedLiveItem(matching item: MenuBarItem, in items: [MenuBarItem]) -> MenuBarItem? { if let exact = items.first(where: { $0.windowID == item.windowID }) { return exact } let sourcePID = item.sourcePID ?? item.ownerPID return items.first { $0.tag.matchesIgnoringWindowID(item.tag) && ($0.sourcePID ?? $0.ownerPID) == sourcePID } } private func contextMatchesItem( _ context: TemporarilyShownItemContext, item: MenuBarItem ) -> Bool { if context.identity.matches(item) { return true } return item.tag.matchesIgnoringWindowID(context.identity.tag) && (item.sourcePID ?? item.ownerPID) == context.popupDetectionPID } private func originalSection( for item: MenuBarItem, hiddenDividerBounds: CGRect, alwaysHiddenDividerBounds: CGRect? ) -> MenuBarHiddenSection? { let section = MenuBarHiddenLayoutPolicy.section( for: item, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) return section == .visible ? nil : section } private func returnDestination( for item: MenuBarItem, section: MenuBarHiddenSection, items: [MenuBarItem], hiddenDividerBounds: CGRect, alwaysHiddenDividerBounds: CGRect? ) -> ( destination: MenuBarHiddenReturnDestination, primaryAnchor: MenuBarHiddenReturnAnchor?, fallbackAnchor: MenuBarHiddenReturnAnchor? )? { let sectionItems = MenuBarHiddenLayoutPolicy.sectionItems( section, from: items, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) guard let destination = MenuBarHiddenLayoutPolicy.returnDestination( for: item, in: sectionItems, section: section ) else { return nil } let primaryAnchor = anchor(for: destination.placement, in: sectionItems) let fallbackAnchor = destination.fallbackPlacement.flatMap { anchor(for: $0, in: sectionItems) } return (destination, primaryAnchor, fallbackAnchor) } private func anchor( for placement: MenuBarHiddenMovePlacement, in items: [MenuBarItem] ) -> MenuBarHiddenReturnAnchor? { switch placement { case .end: return nil case .before(let tag), .after(let tag): guard let item = items.first(where: { $0.tag.matchesIgnoringWindowID(tag) }) else { return nil } return MenuBarHiddenReturnAnchor(tag: item.tag, sourcePID: item.sourcePID ?? item.ownerPID) } } private func temporaryShowTarget( items: [MenuBarItem], hiddenDividerBounds: CGRect, alwaysHiddenDividerBounds: CGRect? ) -> MenuBarHiddenResolvedMoveTarget? { let visibleAnchor = items.first(where: \.isVisibleControlItem) ?? items.first(where: \.isHostApplicationIcon) if let visibleAnchor, let target = computeMoveTarget( section: .visible, placement: .before(visibleAnchor.tag), items: items, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) { return target } return computeMoveTarget( section: .visible, placement: .end, items: items, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) } private func refreshedClickItem( matching item: MenuBarItem, previousBounds: CGRect? ) async -> MenuBarItem { await waitForItemToLeavePreviousBounds(item, previousBounds: previousBounds) await waitForItemPositionToSettle(item) let result = enumerateMenuBarItems( hiddenDividerWindowID: divider.windowID, hiddenDividerFrame: divider.screenFrame, alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil, alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil, excludedWindowIDs: excludedControlWindowIDs() ) let sourcePID = item.sourcePID ?? item.ownerPID return result.items.first { $0.windowID == item.windowID && $0.isOnScreen } ?? result.items.first { $0.tag.matchesIgnoringWindowID(item.tag) && ($0.sourcePID ?? $0.ownerPID) == sourcePID && $0.isOnScreen } ?? item } private func waitForItemToLeavePreviousBounds(_ item: MenuBarItem, previousBounds: CGRect?) async { guard let previousBounds else { try? await Task.sleep(for: .milliseconds(150)) return } let deadline = Date().addingTimeInterval(0.15) while Date() < deadline { if let bounds = MenuBarHiddenWindowServer.screenRect(for: item.windowID), bounds.origin != previousBounds.origin { return } try? await Task.sleep(for: .milliseconds(15)) } } private func waitForItemPositionToSettle(_ item: MenuBarItem) async { let deadline = Date().addingTimeInterval(0.25) var previousBounds = MenuBarHiddenWindowServer.screenRect(for: item.windowID) var stableReads = 0 while Date() < deadline { try? await Task.sleep(for: .milliseconds(20)) guard let currentBounds = MenuBarHiddenWindowServer.screenRect(for: item.windowID) else { previousBounds = nil stableReads = 0 continue } if currentBounds == previousBounds { stableReads += 1 if stableReads >= 2 { return } } else { stableReads = 0 } previousBounds = currentBounds } } private func openedTemporaryInterfaceWindowID( afterClickBy item: MenuBarItem, baselineWindowIDs: Set ) async -> CGWindowID? { let deadline = Date().addingTimeInterval(0.7) while Date() < deadline { let windowsAfterClick = WindowInfo.createOnScreenWindows() if let window = windowsAfterClick.first(where: { MenuBarHiddenTemporaryInterfacePolicy.isNewTemporaryInterfaceWindow( $0, item: item, contextWindowID: item.windowID, baselineWindowIDs: baselineWindowIDs ) }) { return window.windowID } try? await Task.sleep(for: .milliseconds(40)) } return nil } private func startTemporaryRehideLoop() { guard !temporarilyShownItemContexts.isEmpty else { return } temporaryRehideTask?.cancel() temporaryRehideTask = Task { [weak self] in while !Task.isCancelled { try? await Task.sleep(for: .milliseconds(250)) guard !Task.isCancelled else { return } guard let self else { return } let isFinished = await self.rehideTemporarilyShownItemsIfReady() if isFinished { return } } } temporaryRehideCancellable?.cancel() temporaryRehideCancellable = NSWorkspace.shared.publisher(for: \.frontmostApplication) .dropFirst() .debounce(for: .milliseconds(200), scheduler: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } Task { [weak self] in guard let self else { return } _ = await self.rehideTemporarilyShownItemsIfReady() } } } private func stopTemporaryRehideLoop() { temporaryRehideTask?.cancel() temporaryRehideTask = nil temporaryRehideCancellable?.cancel() temporaryRehideCancellable = nil } private func rehideTemporarilyShownItemsIfReady() async -> Bool { guard !temporarilyShownItemContexts.isEmpty else { stopTemporaryRehideLoop() return true } guard temporaryRehideReadiness() == .ready else { return false } await rehideTemporarilyShownItems() return temporarilyShownItemContexts.isEmpty } private func rehideTemporarilyShownItems( force: Bool = false, isCalledFromTemporarilyShow: Bool = false, discardsMissingItems: Bool = false ) async { guard !temporarilyShownItemContexts.isEmpty else { return } if !force { guard temporaryRehideReadiness() == .ready else { startTemporaryRehideLoop() return } } var pending = temporarilyShownItemContexts temporarilyShownItemContexts.removeAll() var failed: [TemporarilyShownItemContext] = [] try? await Task.sleep( for: isCalledFromTemporarilyShow ? .milliseconds(50) : .milliseconds(250) ) while let context = pending.popLast() { switch await returnTemporarilyShownItem(context) { case .returned: continue case .notFound: if discardsMissingItems, !temporaryContextWindowIsVisible(context) { continue } context.scheduleReturnRetry(after: min(5, 0.5 * Double(max(1, context.notFoundAttempts)))) failed.append(context) case .retryLater: if context.rehideAttempts < 3 { pending.append(context) } else { context.rehideAttempts = 0 context.scheduleReturnRetry(after: 1) failed.append(context) } } } temporarilyShownItemContexts = failed.reversed() if temporarilyShownItemContexts.isEmpty { stopTemporaryRehideLoop() updateAlwaysHiddenDividerVisibility() try? await Task.sleep(for: .milliseconds(200)) rebuildSnapshot() scheduleAlwaysHiddenItemsRestoreIfNeeded() scheduleStoredLayoutRestoreIfNeeded() refreshIconsIfUIVisible() } else { startTemporaryRehideLoop() } } private func temporaryContextWindowIsVisible(_ context: TemporarilyShownItemContext) -> Bool { MenuBarHiddenWindowServer.isWindowOnScreen(context.identity.windowID) } private func hasUserPausedInput() -> Bool { NSEvent.modifierFlags.isEmpty && !MouseCursorHelper.lastMovementOccurred(within: 0.25) && !MouseCursorHelper.lastScrollWheelOccurred(within: 0.25) && !MouseCursorHelper.isButtonPressed() } private func temporaryRehideReadiness() -> TemporaryRehideReadiness { if temporarilyShownItemContexts.contains(where: isTemporaryItemInterfaceActive) { return .waitingForInterface } if temporarilyShownItemContexts.contains(where: { $0.nextReturnAttemptDate > Date() }) { return .waitingForRetry } if !hasUserPausedInput() { return .waitingForUserInput } return .ready } private func isTemporaryItemInterfaceActive(_ context: TemporarilyShownItemContext) -> Bool { if let interfaceWindowID = context.interfaceWindowID { return isTemporaryInterfaceWindowActive(interfaceWindowID, item: context.interfaceOwner) } if Date() < context.interfaceDiscoveryDeadline { return true } return false } private func isTemporaryInterfaceWindowActive(_ windowID: CGWindowID, item: MenuBarItem) -> Bool { guard let window = WindowInfo(windowID: windowID) else { return false } return MenuBarHiddenTemporaryInterfacePolicy.isTemporaryInterfaceWindow(window, for: item) } private func returnTemporarilyShownItem(_ context: TemporarilyShownItemContext) async -> TemporaryRehideResult { let result = enumerateMenuBarItems( hiddenDividerWindowID: divider.windowID, hiddenDividerFrame: divider.screenFrame, alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil, alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil, excludedWindowIDs: excludedControlWindowIDs() ) guard let hiddenDividerBounds = result.hiddenDividerBounds else { context.rehideAttempts += 1 return .retryLater } let layoutItems = MenuBarHiddenLayoutPolicy.layoutEditorItems(from: result.items) guard let liveItem = result.items.first(where: { contextMatchesItem(context, item: $0) }) else { context.notFoundAttempts += 1 return .notFound } guard let target = returnMoveTarget( for: context, items: result.items, layoutItems: layoutItems, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: result.alwaysHiddenDividerBounds ) else { context.rehideAttempts += 1 return .retryLater } do { try await events.move(item: liveItem, to: target) if await waitForTemporaryItemReturn(context) { context.rehideAttempts = 0 context.notFoundAttempts = 0 return .returned } context.rehideAttempts += 1 MenuBarHiddenLog.plugin.warning("temporary rehide did not reach target section for \(context.identity.tag.stableKey)") return .retryLater } catch { context.rehideAttempts += 1 MenuBarHiddenLog.plugin.error("temporary rehide failed: \(self.localizedDescription(for: error))") return .retryLater } } private func waitForTemporaryItemReturn(_ context: TemporarilyShownItemContext) async -> Bool { let deadline = Date().addingTimeInterval(0.25) while true { if temporaryItemHasReturned(context) { return true } guard Date() < deadline else { return false } try? await Task.sleep(for: .milliseconds(20)) } } private func temporaryItemHasReturned(_ context: TemporarilyShownItemContext) -> Bool { let result = enumerateMenuBarItems( hiddenDividerWindowID: divider.windowID, hiddenDividerFrame: divider.screenFrame, alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil, alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil, excludedWindowIDs: excludedControlWindowIDs() ) guard let hiddenDividerBounds = result.hiddenDividerBounds else { return false } guard let liveItem = result.items.first(where: { contextMatchesItem(context, item: $0) }) else { return false } return MenuBarHiddenLayoutPolicy.section( for: liveItem, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: result.alwaysHiddenDividerBounds ) == context.returnDestination.section } private func returnMoveTarget( for context: TemporarilyShownItemContext, items: [MenuBarItem], layoutItems: [MenuBarItem], hiddenDividerBounds: CGRect, alwaysHiddenDividerBounds: CGRect? ) -> MenuBarHiddenResolvedMoveTarget? { let destination = context.returnDestination if let target = computeMoveTarget( section: destination.section, placement: resolvedPlacement( destination.placement, anchor: context.primaryReturnAnchor, items: items ), items: items, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) { return target } if let fallbackPlacement = destination.fallbackPlacement, let target = computeMoveTarget( section: destination.section, placement: resolvedPlacement( fallbackPlacement, anchor: context.fallbackReturnAnchor, items: items ), items: items, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) { return target } let sectionItems = MenuBarHiddenLayoutPolicy.sectionItems( destination.section, from: layoutItems, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) if let anchor = sectionItems.first { return computeMoveTarget( section: destination.section, placement: .before(anchor.tag), items: items, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) } return computeMoveTarget( section: destination.section, placement: .end, items: items, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) } private func resolvedPlacement( _ placement: MenuBarHiddenMovePlacement, anchor: MenuBarHiddenReturnAnchor?, items: [MenuBarItem] ) -> MenuBarHiddenMovePlacement { guard let anchor else { return placement } guard let freshItem = items.first(where: { $0.tag.matchesIgnoringWindowID(anchor.tag) && ($0.sourcePID ?? $0.ownerPID) == anchor.sourcePID }) else { return placement } switch placement { case .end: return .end case .before: return .before(freshItem.tag) case .after: return .after(freshItem.tag) } } // MARK: - Host icon recovery private func recoverControlItemOrderIfNeeded(sessionID: Int?) { recoverControlItemOrderIfNeeded(target: currentDragTarget, sessionID: sessionID) } private func recoverControlItemOrderIfNeeded( target: MenuBarHiddenMenuBarDragTarget, sessionID: Int? ) { switch target { case .hostIcon: recoverHostIconIfNeeded(sessionID: sessionID) case .divider: recoverDividerIfNeeded(sessionID: sessionID) recoverAlwaysHiddenDividerIfNeeded(sessionID: sessionID) case .unknown: recoverHostIconIfNeeded(sessionID: sessionID) recoverDividerIfNeeded(sessionID: sessionID) recoverAlwaysHiddenDividerIfNeeded(sessionID: sessionID) } } private func recoverHostIconIfNeeded(sessionID: Int? = nil) { guard divider.isInstalled else { return } if visibleControlItemStoredPositionNeedsRecovery() || hostStatusItemNeedsRecovery() { scheduleHostIconRecovery(sessionID: sessionID) return } let result = enumerateMenuBarItems( hiddenDividerWindowID: divider.windowID, hiddenDividerFrame: divider.screenFrame, alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil, alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil, excludedWindowIDs: excludedControlWindowIDs() ) recoverHostIconIfNeeded(items: result.items, hiddenDividerBounds: result.hiddenDividerBounds) } private func recoverHostIconIfNeeded(items: [MenuBarItem], hiddenDividerBounds: CGRect?) { guard divider.isInstalled, !isDraggingMenuBarItem, !isRecoveringControlItemOrder, MenuBarHiddenLayoutPolicy.hostIconNeedsRecovery(items: items, hiddenDividerBounds: hiddenDividerBounds) else { return } scheduleHostIconRecovery(sessionID: nil) } private func hostStatusItemNeedsRecovery() -> Bool { guard divider.isInstalled, let dividerFrame = divider.screenFrame, let hostFrame = hostStatusItemFrameProvider?() else { return false } let verticalOverlap = hostFrame.maxY > dividerFrame.minY && hostFrame.minY < dividerFrame.maxY return verticalOverlap && hostFrame.maxX <= dividerFrame.minX } private func visibleControlItemStoredPositionNeedsRecovery() -> Bool { MenuBarControlItemDefaults.visibleControlItemNeedsRecovery() } private func recoverDividerIfNeeded(sessionID: Int?) { guard dividerNeedsRecovery() else { return } scheduleDividerRecovery(sessionID: sessionID) } private func recoverAlwaysHiddenDividerIfNeeded(sessionID: Int?) { guard store.isAlwaysHiddenEnabled, alwaysHiddenDivider.isInstalled, !isDraggingMenuBarItem, !isRecoveringControlItemOrder else { return } let result = enumerateMenuBarItems( hiddenDividerWindowID: divider.windowID, hiddenDividerFrame: divider.screenFrame, alwaysHiddenDividerWindowID: alwaysHiddenDivider.windowID, alwaysHiddenDividerFrame: alwaysHiddenDivider.screenFrame, excludedWindowIDs: excludedControlWindowIDs() ) guard MenuBarHiddenLayoutPolicy.alwaysHiddenDividerNeedsRecovery( hiddenDividerBounds: result.hiddenDividerBounds, alwaysHiddenDividerBounds: result.alwaysHiddenDividerBounds ) else { return } scheduleAlwaysHiddenDividerRecovery(sessionID: sessionID) } private func dividerNeedsRecovery() -> Bool { guard divider.isInstalled, let dividerFrame = divider.screenFrame, let hostFrame = hostStatusItemFrameProvider?() else { return false } let verticalOverlap = hostFrame.maxY > dividerFrame.minY && hostFrame.minY < dividerFrame.maxY return verticalOverlap && dividerFrame.minX >= hostFrame.maxX } private func dragTarget(at point: NSPoint?) -> MenuBarHiddenMenuBarDragTarget { guard let point else { return .unknown } if hostStatusItemFrameProvider?().map({ $0.insetBy(dx: -6, dy: -6).contains(point) }) == true { return .hostIcon } if divider.screenFrame.map({ $0.insetBy(dx: -6, dy: -6).contains(point) }) == true { return .divider } return .unknown } private func startControlItemOrderRecoveryPolling(sessionID: Int) { controlItemRecoveryPollTask?.cancel() controlItemRecoveryPollTask = Task { [weak self] in while !Task.isCancelled { try? await Task.sleep(for: .milliseconds(120)) guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard let self else { return } guard self.dragSessionID == sessionID else { self.stopControlItemOrderRecoveryPolling() return } guard self.isDraggingMenuBarItem else { self.stopControlItemOrderRecoveryPolling() return } guard !Self.isLeftMouseButtonPressed else { return } self.isDraggingMenuBarItem = false self.stopControlItemOrderRecoveryPolling() self.scheduleMenuBarDragCommit( target: self.currentDragTarget, sessionID: sessionID ) self.currentDragTarget = .unknown } } } } private func stopControlItemOrderRecoveryPolling() { controlItemRecoveryPollTask?.cancel() controlItemRecoveryPollTask = nil } private static var isLeftMouseButtonPressed: Bool { (NSEvent.pressedMouseButtons & 1) != 0 } private func waitForMenuBarFramesToSettle() async { var previousSignature = menuBarFrameSignature() var stableReads = 0 let deadline = Date().addingTimeInterval(0.45) while Date() < deadline { try? await Task.sleep(for: .milliseconds(45)) guard !Task.isCancelled else { return } let currentSignature = menuBarFrameSignature() if currentSignature == previousSignature { stableReads += 1 if stableReads >= 2 { return } } else { stableReads = 0 previousSignature = currentSignature } } } private func menuBarFrameSignature() -> [String] { let result = enumerateMenuBarItems( hiddenDividerWindowID: divider.windowID, hiddenDividerFrame: divider.screenFrame, alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil, alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil, excludedWindowIDs: excludedControlWindowIDs() ) return result.items.map { item in let bounds = item.bounds.integral return "\(item.tag.stableKey):\(Int(bounds.minX)):\(Int(bounds.maxX))" } } private var isMenuBarDragCommitPending: Bool { menuBarDragCommitSessionID != nil } private func scheduleMenuBarDragCommit(target: MenuBarHiddenMenuBarDragTarget, sessionID: Int) { cancelMenuBarDragCommit() menuBarDragCommitSessionID = sessionID menuBarDragCommitTask = Task { [weak self] in try? await Task.sleep(for: .milliseconds(120)) guard !Task.isCancelled else { return } guard let self else { return } await self.waitForMenuBarFramesToSettle() guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard self.dragSessionID == sessionID, !self.isDraggingMenuBarItem else { self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } self.commitMenuBarDragLayout(target: target, sessionID: sessionID) } } } private func commitMenuBarDragLayout( target: MenuBarHiddenMenuBarDragTarget, sessionID: Int ) { let decision = MenuBarHiddenMenuBarDragCommitPolicy.decision( target: target, dividerNeedsRecovery: dividerNeedsRecovery() ) switch decision { case .commit: recordStoredLayoutAfterMenuBarDrag() finishMenuBarDragCommit(sessionID: sessionID) case .recoverThenCommit: recoverDividerThenCommitMenuBarDrag(sessionID: sessionID) case .recoverOnly: recoverControlItemOrderIfNeeded(target: target, sessionID: sessionID) if !isRecoveringControlItemOrder { finishMenuBarDragCommit(sessionID: sessionID) } } } private func cancelMenuBarDragCommit() { menuBarDragCommitTask?.cancel() menuBarDragCommitTask = nil menuBarDragCommitSessionID = nil } private func isMenuBarDragCommitCurrent(sessionID: Int) -> Bool { menuBarDragCommitSessionID == sessionID } private func clearMenuBarDragCommitIfCurrent(sessionID: Int) { guard menuBarDragCommitSessionID == sessionID else { return } menuBarDragCommitTask = nil menuBarDragCommitSessionID = nil } private func scheduleHostIconRecovery(sessionID: Int?) { guard !isRecoveringControlItemOrder else { return } isRecoveringControlItemOrder = true controlItemRecoveryTask?.cancel() controlItemRecoveryTask = Task { [weak self] in guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard let self else { return } if let sessionID { guard self.dragSessionID == sessionID else { self.isRecoveringControlItemOrder = false self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } } self.divider.showSection(isDragging: false) self.resetHostStatusItemPosition?() } try? await Task.sleep(for: .milliseconds(400)) guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard let self else { return } if let sessionID { guard self.dragSessionID == sessionID else { self.isRecoveringControlItemOrder = false self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } } self.isRecoveringControlItemOrder = false if let sessionID, self.isMenuBarDragCommitCurrent(sessionID: sessionID) { self.finishMenuBarDragCommit(sessionID: sessionID) return } self.refresh(reason: .hostIconRecovered) if self.shouldRestoreHiddenAfterDrag { self.scheduleHiddenRestoreAfterDrag(delay: .milliseconds(250), sessionID: sessionID) } else if self.store.isEnabled, self.divider.isInstalled, !self.isDraggingMenuBarItem { self.installAndExpand() } self.updateAlwaysHiddenDividerVisibility() } } } private func scheduleDividerRecovery(sessionID: Int?) { guard !isRecoveringControlItemOrder else { return } isRecoveringControlItemOrder = true controlItemRecoveryTask?.cancel() controlItemRecoveryTask = Task { [weak self] in guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard let self else { return } if let sessionID { guard self.dragSessionID == sessionID else { self.isRecoveringControlItemOrder = false self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } } self.divider.showSection(isDragging: false) self.divider.reinstall() } try? await Task.sleep(for: .milliseconds(400)) guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard let self else { return } if let sessionID { guard self.dragSessionID == sessionID else { self.isRecoveringControlItemOrder = false self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } } self.isRecoveringControlItemOrder = false self.refresh(reason: .hostIconRecovered) if self.shouldRestoreHiddenAfterDrag { self.scheduleHiddenRestoreAfterDrag(delay: .milliseconds(250), sessionID: sessionID) } else if self.store.isEnabled, self.divider.isInstalled, !self.isDraggingMenuBarItem { self.divider.hideSection() } self.updateAlwaysHiddenDividerVisibility() } } } private func recoverDividerThenCommitMenuBarDrag(sessionID: Int) { guard !isRecoveringControlItemOrder else { return } isRecoveringControlItemOrder = true controlItemRecoveryTask?.cancel() controlItemRecoveryTask = Task { [weak self] in guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard let self else { return } guard self.dragSessionID == sessionID else { self.isRecoveringControlItemOrder = false self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } self.divider.showSection(isDragging: false) self.divider.reinstall() } try? await Task.sleep(for: .milliseconds(250)) guard !Task.isCancelled else { return } guard let self else { return } await self.waitForMenuBarFramesToSettle() guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard self.dragSessionID == sessionID else { self.isRecoveringControlItemOrder = false self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } self.isRecoveringControlItemOrder = false self.recordStoredLayoutAfterMenuBarDrag() self.finishMenuBarDragCommit(sessionID: sessionID) } } } private func scheduleAlwaysHiddenDividerRecovery(sessionID: Int?) { guard !isRecoveringControlItemOrder else { return } isRecoveringControlItemOrder = true controlItemRecoveryTask?.cancel() controlItemRecoveryTask = Task { [weak self] in guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard let self else { return } if let sessionID { guard self.dragSessionID == sessionID else { self.isRecoveringControlItemOrder = false self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } } self.alwaysHiddenDivider.showSection(isDragging: false) self.divider.showSection(isDragging: false) } try? await Task.sleep(for: .milliseconds(150)) guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard let self else { return } if let sessionID { guard self.dragSessionID == sessionID else { self.isRecoveringControlItemOrder = false self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } } let result = self.enumerateMenuBarItems( hiddenDividerWindowID: self.divider.windowID, hiddenDividerFrame: self.divider.screenFrame, alwaysHiddenDividerWindowID: self.alwaysHiddenDivider.windowID, alwaysHiddenDividerFrame: self.alwaysHiddenDivider.screenFrame, excludedWindowIDs: self.excludedControlWindowIDs() ) guard let alwaysHiddenDividerWindowID = self.alwaysHiddenDivider.windowID, let hiddenDividerBounds = result.hiddenDividerBounds, MenuBarHiddenLayoutPolicy.alwaysHiddenDividerNeedsRecovery( hiddenDividerBounds: result.hiddenDividerBounds, alwaysHiddenDividerBounds: result.alwaysHiddenDividerBounds ) else { self.isRecoveringControlItemOrder = false self.restoreControlItemsAfterAlwaysHiddenRecovery() self.rebuildSnapshot() return } let controlItem = MenuBarItem( tag: MenuBarItemTag( namespace: "\(ProcessInfo.processInfo.processIdentifier)", title: MenuBarHiddenConstants.alwaysHiddenControlItemTitle, windowID: alwaysHiddenDividerWindowID, instanceIndex: 0 ), windowID: alwaysHiddenDividerWindowID, ownerPID: ProcessInfo.processInfo.processIdentifier, bounds: result.alwaysHiddenDividerBounds ?? .zero, title: MenuBarHiddenConstants.alwaysHiddenControlItemTitle, isOnScreen: true ) let target = MenuBarHiddenResolvedMoveTarget( point: CGPoint(x: hiddenDividerBounds.minX, y: hiddenDividerBounds.minY), windowID: self.divider.windowID ) self.controlItemRecoveryTask = Task { [weak self] in guard let self else { return } do { try await self.events.move(item: controlItem, to: target) } catch { MenuBarHiddenLog.plugin.error( "always-hidden divider recovery failed: \(self.localizedDescription(for: error))" ) } try? await Task.sleep(for: .milliseconds(300)) guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } if let sessionID { guard self.dragSessionID == sessionID else { self.isRecoveringControlItemOrder = false self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } } self.isRecoveringControlItemOrder = false self.restoreControlItemsAfterAlwaysHiddenRecovery() self.rebuildSnapshot() self.refreshIconsIfUIVisible() } } } } } private func recordStoredLayoutAfterMenuBarDrag() { guard permissions.canManageItems else { return } let result = enumerateMenuBarItems( hiddenDividerWindowID: divider.windowID, hiddenDividerFrame: divider.screenFrame, alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil, alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil, excludedWindowIDs: excludedControlWindowIDs() ) guard let hiddenDividerBounds = result.hiddenDividerBounds else { return } let layoutItems = MenuBarHiddenLayoutPolicy.layoutEditorItems(from: result.items) let alwaysHiddenDividerBounds: CGRect? if store.isAlwaysHiddenEnabled { guard let bounds = result.alwaysHiddenDividerBounds else { return } alwaysHiddenDividerBounds = bounds } else { alwaysHiddenDividerBounds = nil } let visibleItems = MenuBarHiddenLayoutPolicy.visibleItems( from: layoutItems, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) let hiddenItems = MenuBarHiddenLayoutPolicy.hiddenItems( from: layoutItems, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) let alwaysHiddenItems = MenuBarHiddenLayoutPolicy.alwaysHiddenItems( from: layoutItems, hiddenDividerBounds: hiddenDividerBounds, alwaysHiddenDividerBounds: alwaysHiddenDividerBounds ) let nextVisibleHidden = MenuBarHiddenStoredLayoutPolicy.visibleHiddenLayout( visibleItems: visibleItems, hiddenItems: hiddenItems, alwaysHiddenItems: alwaysHiddenItems, previousVisibleItemStableKeys: store.visibleItemStableKeys, previousHiddenItemStableKeys: store.hiddenItemStableKeys ) store.recordVisibleHiddenLayout( visibleKeys: nextVisibleHidden.visibleItemStableKeys, hiddenKeys: nextVisibleHidden.hiddenItemStableKeys ) guard store.isAlwaysHiddenEnabled else { return } let currentAlwaysHiddenKeys = Set(alwaysHiddenItems.filter(\.canBeHidden).map(\.tag.stableKey)) let currentItemKeys = Set(layoutItems.map(\.tag.stableKey)) let previousKeys = store.alwaysHiddenItemStableKeys let keysStillMissing = previousKeys.subtracting(currentItemKeys) let nextKeys = keysStillMissing.union(currentAlwaysHiddenKeys) guard nextKeys != previousKeys else { return } store.alwaysHiddenItemStableKeys = nextKeys } private func finishMenuBarDragCommit(sessionID: Int) { guard dragSessionID == sessionID else { clearMenuBarDragCommitIfCurrent(sessionID: sessionID) return } clearMenuBarDragCommitIfCurrent(sessionID: sessionID) rebuildSnapshot() refreshIconsIfUIVisible() if shouldRestoreHiddenAfterDrag { scheduleHiddenRestoreAfterDrag(delay: .milliseconds(80), sessionID: sessionID) } scheduleAlwaysHiddenItemsRestoreIfNeeded(delay: .milliseconds(400)) } private func restoreControlItemsAfterAlwaysHiddenRecovery() { if store.isEnabled, divider.isInstalled, !isDraggingMenuBarItem { divider.hideSection() } else if divider.isInstalled { divider.showSection(isDragging: isDraggingMenuBarItem) } if store.isAlwaysHiddenEnabled { if alwaysHiddenDivider.isInstalled { alwaysHiddenDivider.hideSection() } } else { alwaysHiddenDivider.uninstall() } } private func scheduleHiddenRestoreAfterDrag(delay: Duration, sessionID: Int?) { guard shouldRestoreHiddenAfterDrag else { return } hiddenRestoreTask?.cancel() hiddenRestoreTask = Task { [weak self] in try? await Task.sleep(for: delay) guard !Task.isCancelled else { return } await MainActor.run { guard !Task.isCancelled else { return } guard let self else { return } if let sessionID { guard self.dragSessionID == sessionID else { return } } guard self.shouldRestoreHiddenAfterDrag else { return } guard !self.isDraggingMenuBarItem, !self.isRecoveringControlItemOrder else { self.scheduleHiddenRestoreAfterDrag(delay: .milliseconds(250), sessionID: sessionID) return } self.shouldRestoreHiddenAfterDrag = false self.store.isEnabled = true self.installAndExpand() self.rebuildSnapshot() self.scheduleAlwaysHiddenItemsRestoreIfNeeded() } } } }