Skip to content

Commit cbcbffb

Browse files
Make system menu bar visibility configurable
1 parent 9992c70 commit cbcbffb

5 files changed

Lines changed: 77 additions & 38 deletions

File tree

OverflowBar/Models/MenuBarItem.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import AppKit
22
import ApplicationServices
33

44
enum MenuBarSystemItemClassifier {
5+
static func isGenericControlCenterItem(_ title: String, owner: String? = nil) -> Bool {
6+
let normalized = normalize(title)
7+
let normalizedOwner = normalize(owner ?? "")
8+
return normalizedOwner.contains("controlcenter") &&
9+
["item-0", "item0", "statusmenu", "menubaritem"].contains(normalized)
10+
}
11+
512
static func isProtected(_ title: String, owner: String? = nil) -> Bool {
613
let normalized = normalize(title)
714
if normalized.contains("clock") ||
@@ -19,9 +26,7 @@ enum MenuBarSystemItemClassifier {
1926
// macOS 26 gives several Control Center-owned status items only a
2027
// generic WindowServer/AX title (for example "Item-0" or
2128
// "status menu"). Their owner is the reliable system boundary.
22-
let normalizedOwner = normalize(owner ?? "")
23-
return normalizedOwner.contains("controlcenter") &&
24-
["item-0", "item0", "statusmenu", "menubaritem"].contains(normalized)
29+
return isGenericControlCenterItem(title, owner: owner)
2530
}
2631

2732
static func canonicalName(_ title: String, owner: String? = nil) -> String {

OverflowBar/Services/MenuBarItemStore.swift

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ final class MenuBarItemStore: ObservableObject {
5353
workspaceObservers.forEach(NSWorkspace.shared.notificationCenter.removeObserver)
5454
}
5555

56-
var selectedItems: [MenuBarItem] { items.filter { $0.isSelected && !$0.isProtectedSystemItem } }
56+
/// Items the user selected to move out of the original menu bar. System
57+
/// controls are recognized separately for safety, but their visibility is
58+
/// still user-configurable in Settings.
59+
var selectedItems: [MenuBarItem] { items.filter(\.isSelected) }
5760

5861
/// Items currently in OverflowBar's off-screen staging area but absent
5962
/// from the persisted selection set. This recovers protected macOS
@@ -118,23 +121,36 @@ final class MenuBarItemStore: ObservableObject {
118121
let deselectedBefore = preferences.deselectedItemIDs
119122
let selectedBefore = preferences.selectedIDs
120123
let scanned = scanner.scan(selectedIDs: selectedBefore)
121-
let currentIDs = Set(scanned.filter { !$0.isProtectedSystemItem }.map(\.id))
124+
let currentIDs = Set(scanned.map(\.id))
125+
let selectableCurrentIDs = Set(scanned.filter { !$0.isProtectedSystemItem }.map(\.id))
122126
let currentWindowIDs = Set(scanned.compactMap(\.windowID))
123127
let newWindowIDs = currentWindowIDs.subtracting(knownWindowIDsBefore)
124128

125129
for item in scanned {
126130
let previous = previousByID[item.id] ?? item.windowID.flatMap { previousByWindowID[$0] }
127131
item.iconImage = previous?.iconImage
128-
if let previous { item.isSelected = previous.isSelected }
132+
// System-item selection is derived from the current WindowServer
133+
// frame (or an explicit persisted selection), not from a stale
134+
// pre-1.0.15 object that may have treated every system item as
135+
// permanently unselected.
136+
if let previous, !item.isProtectedSystemItem { item.isSelected = previous.isSelected }
137+
// An explicit user deselection wins over the frame-based stale
138+
// hidden-state recovery used for system controls.
139+
if item.isProtectedSystemItem && deselectedBefore.contains(item.id) {
140+
item.isSelected = false
141+
}
129142
}
130143

131144
if !preferences.didApplyDefaultLayout, !scanned.isEmpty {
132-
scanned.forEach { $0.isSelected = !$0.isProtectedSystemItem }
145+
// Keep the scanner's off-screen state for system controls so a
146+
// previous build's hidden icons can be restored from Settings;
147+
// visible system controls remain unselected by default.
148+
scanned.forEach { if !$0.isProtectedSystemItem { $0.isSelected = true } }
133149
preferences.didApplyDefaultLayout = true
134150
layoutManager.isEnabled = false
135151
layoutManagementEnabled = false
136152
} else if !knownBefore.isEmpty, layoutManagementEnabled {
137-
let newIDs = currentIDs.subtracting(knownBefore)
153+
let newIDs = selectableCurrentIDs.subtracting(knownBefore)
138154
let newWindowItems = scanned.filter { item in
139155
!item.isProtectedSystemItem && item.windowID.map(newWindowIDs.contains) == true &&
140156
!deselectedBefore.contains(item.id)
@@ -200,7 +216,6 @@ final class MenuBarItemStore: ObservableObject {
200216
}
201217

202218
func setSelected(_ item: MenuBarItem, selected: Bool) {
203-
guard !item.isProtectedSystemItem else { return }
204219
item.isSelected = selected
205220
objectWillChange.send()
206221
var deselected = preferences.deselectedItemIDs
@@ -214,15 +229,23 @@ final class MenuBarItemStore: ObservableObject {
214229
}
215230

216231
func selectAll(_ selected: Bool) {
217-
for item in items where !item.isProtectedSystemItem { item.isSelected = selected }
232+
let previouslySelected = items.filter(\.isSelected)
233+
for item in items { item.isSelected = selected }
218234
if selected {
219235
preferences.saveDeselectedItems([])
220236
} else {
221-
preferences.saveDeselectedItems(Set(items.filter { !$0.isProtectedSystemItem }.map(\.id)))
237+
preferences.saveDeselectedItems(Set(items.map(\.id)))
222238
}
223239
preferences.saveSelected(Set(items.filter(\.isSelected).map(\.id)))
224240
objectWillChange.send()
225-
if selected { applyLayout() } else { restoreLayout() }
241+
if selected {
242+
applyLayout()
243+
} else if let controlItemFrame {
244+
layoutOperationMessage = "Restoring menu bar items…"
245+
layoutManager.restore(previouslySelected, relativeTo: controlItemFrame) { [weak self] count in
246+
self?.layoutOperationMessage = count > 0 ? "Restored \(count) menu bar items." : "Menu bar items are already visible."
247+
}
248+
}
226249
onLayoutStateChanged?()
227250
}
228251

OverflowBar/Services/MenuBarLayoutManager.swift

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ final class MenuBarLayoutManager {
2323

2424
func hide(_ items: [MenuBarItem], relativeTo controlFrame: CGRect, targetAttempt: Int = 0, completion: @escaping (Int) -> Void = { _ in }) {
2525
guard isEnabled else { completion(0); return }
26-
restoreProtectedSystemItems { [weak self] _ in
27-
self?.hideAfterRestoringProtectedItems(items, relativeTo: controlFrame, targetAttempt: targetAttempt, completion: completion)
26+
let managedWindowIDs = Set(items.compactMap(\.windowID))
27+
let managedSystemNames = protectedNames(for: items)
28+
restoreProtectedSystemItems(excluding: managedWindowIDs, excludingSystemNames: managedSystemNames) { [weak self] _ in
29+
self?.hideAfterRestoringProtectedItems(items, relativeTo: controlFrame, targetAttempt: targetAttempt, managedSystemNames: managedSystemNames, completion: completion)
2830
}
2931
}
3032

31-
private func hideAfterRestoringProtectedItems(_ items: [MenuBarItem], relativeTo controlFrame: CGRect, targetAttempt: Int, completion: @escaping (Int) -> Void) {
33+
private func hideAfterRestoringProtectedItems(_ items: [MenuBarItem], relativeTo controlFrame: CGRect, targetAttempt: Int, managedSystemNames: Set<String>, completion: @escaping (Int) -> Void) {
3234
guard isEnabled else { completion(0); return }
3335
guard let target = hiddenTargetWindow() else {
3436
guard targetAttempt < 10 else {
@@ -38,19 +40,19 @@ final class MenuBarLayoutManager {
3840
}
3941
logger.info("Control window pending; retrying attempt \(targetAttempt + 1, privacy: .public)")
4042
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
41-
self?.hideAfterRestoringProtectedItems(items, relativeTo: controlFrame, targetAttempt: targetAttempt + 1, completion: completion)
43+
self?.hideAfterRestoringProtectedItems(items, relativeTo: controlFrame, targetAttempt: targetAttempt + 1, managedSystemNames: managedSystemNames, completion: completion)
4244
}
4345
return
4446
}
4547
logger.info("Hiding \(items.count, privacy: .public) items relative to window \(target.id, privacy: .public)")
46-
let protectedWindowIDs = Set(windowRecords().filter { MenuBarSystemItemClassifier.isProtected($0.title, owner: $0.owner) }.map(\.id))
4748
let managed = items.filter {
48-
!$0.isProtectedSystemItem && $0.windowID != target.id && $0.windowID.map(protectedWindowIDs.contains) != true
49+
$0.windowID != target.id
4950
}.filter(needsHiding)
5051
publishCurrentFrames(for: managed)
5152
hideSequentially(managed, index: 0, movedCount: 0) { [weak self] movedCount in
5253
self?.publishCurrentFrames(for: managed)
53-
self?.restoreProtectedSystemItems { _ in completion(movedCount) }
54+
let managedWindowIDs = Set(managed.compactMap(\.windowID))
55+
self?.restoreProtectedSystemItems(excluding: managedWindowIDs, excludingSystemNames: managedSystemNames) { _ in completion(movedCount) }
5456
}
5557
}
5658

@@ -122,24 +124,40 @@ final class MenuBarLayoutManager {
122124
move(item, relativeTo: target.id, placement: .left) { _ in }
123125
}
124126

125-
func restoreProtectedSystemItems(attempt: Int = 0, completion: @escaping (Int) -> Void = { _ in }) {
127+
func restoreProtectedSystemItems(attempt: Int = 0, excluding excludedWindowIDs: Set<CGWindowID> = [], excludingSystemNames: Set<String> = [], completion: @escaping (Int) -> Void = { _ in }) {
126128
guard let target = controlTargetWindow() else {
127129
guard attempt < 10 else {
128130
logger.error("Control target unavailable while restoring protected items")
129131
completion(0)
130132
return
131133
}
132134
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
133-
self?.restoreProtectedSystemItems(attempt: attempt + 1, completion: completion)
135+
self?.restoreProtectedSystemItems(attempt: attempt + 1, excluding: excludedWindowIDs, excludingSystemNames: excludingSystemNames, completion: completion)
134136
}
135137
return
136138
}
137139
let hidden = windowRecords().filter {
138-
MenuBarSystemItemClassifier.isProtected($0.title, owner: $0.owner) && $0.frame.maxX <= 0
140+
MenuBarSystemItemClassifier.isProtected($0.title, owner: $0.owner) &&
141+
$0.frame.maxX <= 0 &&
142+
!excludedWindowIDs.contains($0.id) &&
143+
!excludingSystemNames.contains($0.title) &&
144+
!excludingSystemNames.contains(MenuBarSystemItemClassifier.canonicalName($0.title, owner: $0.owner)) &&
145+
!(MenuBarSystemItemClassifier.isGenericControlCenterItem($0.title, owner: $0.owner) && excludingSystemNames.contains("Control Center Item"))
139146
}
140147
restoreProtectedSequentially(hidden, index: 0, target: target, movedCount: 0, completion: completion)
141148
}
142149

150+
private func protectedNames(for items: [MenuBarItem]) -> Set<String> {
151+
var names = Set<String>()
152+
for item in items where item.isProtectedSystemItem {
153+
names.insert(item.title)
154+
if item.title == "Control Center Item" {
155+
names.insert("Control Center Item")
156+
}
157+
}
158+
return names
159+
}
160+
143161
private func hideSequentially(_ items: [MenuBarItem], index: Int, movedCount: Int, completion: @escaping (Int) -> Void) {
144162
guard index < items.count else { completion(movedCount); return }
145163
guard let target = hiddenTargetWindow() else { completion(movedCount); return }

OverflowBar/Services/MenuBarScanner.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ final class MenuBarScanner {
5050
existing.bundleIdentifier = bundleID
5151
continue
5252
}
53-
results.append(MenuBarItem(id: id, title: title, ownerName: isProtected ? "System Menu Bar" : ownerName, bundleIdentifier: bundleID, frame: frame, axElement: child, applicationIcon: app.icon, isSelected: !isProtected && selectedIDs.contains(id), supportsPressAction: supportsPress, isProtectedSystemItem: isProtected))
53+
let isSelected = selectedIDs.contains(id) || (isProtected && isHiddenMenuBarFrame(frame))
54+
results.append(MenuBarItem(id: id, title: title, ownerName: isProtected ? "System Menu Bar" : ownerName, bundleIdentifier: bundleID, frame: frame, axElement: child, applicationIcon: app.icon, isSelected: isSelected, supportsPressAction: supportsPress, isProtectedSystemItem: isProtected))
5455
}
5556
}
5657
return results.sorted { $0.frame.minX < $1.frame.minX }
@@ -103,7 +104,7 @@ final class MenuBarScanner {
103104
let alternateOwnerKey = candidate.ownerKey == "Control Center" ? "com.apple.controlcenter" : candidate.ownerKey
104105
let alternateID = "window|\(alternateOwnerKey)|\(candidate.title)|\(occurrence)"
105106
let legacyID = "window|\(candidate.title)|\(legacyOccurrence)"
106-
let isSelected = !isProtected && (selectedIDs.contains(id) || selectedIDs.contains(alternateID) || selectedIDs.contains(legacyID))
107+
let isSelected = selectedIDs.contains(id) || selectedIDs.contains(alternateID) || selectedIDs.contains(legacyID) || (isProtected && isHiddenMenuBarFrame(candidate.frame))
107108
let displayTitle = candidate.title == "Item-0" ? "Menu Bar Item" : title
108109
return MenuBarItem(id: id, title: displayTitle, ownerName: isProtected ? "System Menu Bar" : candidate.owner, bundleIdentifier: candidate.ownerKey, frame: candidate.frame, axElement: nil, applicationIcon: candidate.appIcon, isSelected: isSelected, supportsPressAction: false, windowID: CGWindowID(candidate.identifier), ownerPID: pid_t(candidate.ownerPID), isProtectedSystemItem: isProtected)
109110
}

OverflowBar/Settings/SettingsView.swift

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,27 +53,19 @@ struct SettingsView: View {
5353
}
5454
if !permissions.accessibilityGranted { Text("Enable Accessibility to scan and activate menu bar items.").foregroundStyle(.secondary) }
5555
ForEach(store.items) { item in
56-
if item.isProtectedSystemItem {
56+
Toggle(isOn: Binding(get: { item.isSelected }, set: { store.setSelected(item, selected: $0) })) {
5757
HStack {
5858
icon(for: item)
5959
VStack(alignment: .leading) {
6060
Text(item.ownerName)
6161
Text(item.title).font(.caption).foregroundStyle(.secondary)
6262
}
6363
Spacer()
64-
Label("Always Visible", systemImage: "lock.fill")
65-
.font(.caption)
66-
.foregroundStyle(.secondary)
67-
}
68-
} else {
69-
Toggle(isOn: Binding(get: { item.isSelected }, set: { store.setSelected(item, selected: $0) })) {
70-
HStack {
71-
icon(for: item)
72-
VStack(alignment: .leading) {
73-
Text(item.ownerName)
74-
Text(item.title).font(.caption).foregroundStyle(.secondary)
75-
}
76-
Spacer()
64+
if item.isProtectedSystemItem {
65+
Text("System · configurable")
66+
.font(.caption)
67+
.foregroundStyle(.secondary)
68+
} else {
7769
Image(systemName: item.supportsPressAction ? "hand.tap" : "exclamationmark.triangle").help(item.supportsPressAction ? "Supports Accessibility press" : "Accessibility activation unavailable")
7870
}
7971
}

0 commit comments

Comments
 (0)