chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
struct StatusItemVisibilitySnapshot: Equatable {
|
||||
let isVisible: Bool
|
||||
let hasButton: Bool
|
||||
let hasWindow: Bool
|
||||
let hasScreen: Bool
|
||||
let isOnCurrentScreen: Bool
|
||||
let buttonWidth: CGFloat
|
||||
|
||||
init(
|
||||
isVisible: Bool,
|
||||
hasButton: Bool,
|
||||
hasWindow: Bool,
|
||||
hasScreen: Bool,
|
||||
isOnCurrentScreen: Bool = true,
|
||||
buttonWidth: CGFloat)
|
||||
{
|
||||
self.isVisible = isVisible
|
||||
self.hasButton = hasButton
|
||||
self.hasWindow = hasWindow
|
||||
self.hasScreen = hasScreen
|
||||
self.isOnCurrentScreen = isOnCurrentScreen
|
||||
self.buttonWidth = buttonWidth
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusItemVisibilitySnapshot: CustomStringConvertible {
|
||||
var description: String {
|
||||
"visible=\(self.isVisible),button=\(self.hasButton),window=\(self.hasWindow),"
|
||||
+ "screen=\(self.hasScreen),currentScreen=\(self.isOnCurrentScreen),"
|
||||
+ "width=\(String(format: "%.1f", Double(self.buttonWidth)))"
|
||||
}
|
||||
}
|
||||
|
||||
struct StatusItemStartupVisibilityEvidence: Equatable, CustomStringConvertible {
|
||||
let autosaveName: String
|
||||
let expectsVisibility: Bool
|
||||
let visibilityDefault: Bool?
|
||||
let snapshot: StatusItemVisibilitySnapshot
|
||||
|
||||
var description: String {
|
||||
"name=\(self.autosaveName),expected=\(self.expectsVisibility),"
|
||||
+ "default=\(self.visibilityDefault.map(String.init) ?? "unset"),\(self.snapshot)"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func isStatusItemBlocked(_ item: NSStatusItem) -> Bool {
|
||||
MenuBarVisibilityWatcher.isBlockedSnapshot(snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
|
||||
}
|
||||
|
||||
enum MenuBarVisibilityWatcher {
|
||||
static let guidanceShownKey = "hasShownTahoeAllowListGuidance"
|
||||
static let guidanceLastShownAtKey = "tahoeAllowListGuidanceLastShownAt"
|
||||
static let guidanceRepeatInterval: TimeInterval = 24 * 60 * 60
|
||||
static let startupFreshnessInterval: TimeInterval = 10
|
||||
static let startupCheckDelay: TimeInterval = 2
|
||||
static let screenChangeCheckDelay: Duration = .milliseconds(750)
|
||||
static let screenChangeFollowUpDelay: Duration = .seconds(2)
|
||||
static let settingsURL = URL(string: "x-apple.systempreferences:com.apple.MenuBarSettings")!
|
||||
|
||||
@MainActor
|
||||
static func visibilitySnapshot(_ item: NSStatusItem) -> StatusItemVisibilitySnapshot {
|
||||
let screen = item.button?.window?.screen
|
||||
return StatusItemVisibilitySnapshot(
|
||||
isVisible: item.isVisible,
|
||||
hasButton: item.button != nil,
|
||||
hasWindow: item.button?.window != nil,
|
||||
hasScreen: screen != nil,
|
||||
isOnCurrentScreen: screen.map(self.isCurrentScreen) ?? false,
|
||||
buttonWidth: item.button?.frame.size.width ?? 0)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func isCurrentScreen(_ screen: NSScreen) -> Bool {
|
||||
let screenNumber = self.screenNumber(screen)
|
||||
return NSScreen.screens.contains { candidate in
|
||||
if let screenNumber, let candidateNumber = self.screenNumber(candidate) {
|
||||
return candidateNumber == screenNumber
|
||||
}
|
||||
return candidate === screen
|
||||
}
|
||||
}
|
||||
|
||||
private static func screenNumber(_ screen: NSScreen) -> NSNumber? {
|
||||
screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber
|
||||
}
|
||||
|
||||
static func isBlockedSnapshot(snapshot: StatusItemVisibilitySnapshot) -> Bool {
|
||||
guard snapshot.isVisible else { return false }
|
||||
guard snapshot.hasButton else { return true }
|
||||
// Menu bar managers can park status-item windows off the current screen while preserving the
|
||||
// underlying NSStatusItem. Recreating in that state makes those managers see a new item.
|
||||
return !snapshot.hasWindow || snapshot.buttonWidth <= 0
|
||||
}
|
||||
|
||||
static func isDisplacedSnapshot(snapshot: StatusItemVisibilitySnapshot) -> Bool {
|
||||
guard snapshot.isVisible, snapshot.hasButton, snapshot.hasWindow, snapshot.buttonWidth > 0 else {
|
||||
return false
|
||||
}
|
||||
return !snapshot.hasScreen || !snapshot.isOnCurrentScreen
|
||||
}
|
||||
|
||||
static func hasBlockedVisibleSnapshots(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
|
||||
let visibleItems = snapshots.filter(\.isVisible)
|
||||
guard !visibleItems.isEmpty else { return false }
|
||||
return visibleItems.allSatisfy { snapshot in
|
||||
self.isBlockedSnapshot(snapshot: snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
static func hasAnyBlockedVisibleSnapshot(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
|
||||
snapshots.contains { snapshot in
|
||||
snapshot.isVisible && self.isBlockedSnapshot(snapshot: snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
static func hasAnyDisplacedVisibleSnapshot(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
|
||||
snapshots.contains { snapshot in
|
||||
self.isDisplacedSnapshot(snapshot: snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
static func hasAnyStartupRecoveryCandidate(
|
||||
snapshots: [StatusItemVisibilitySnapshot],
|
||||
evidence: [StatusItemStartupVisibilityEvidence] = [],
|
||||
windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
|
||||
detectTahoeBlockedStatusItem: Bool = false)
|
||||
-> Bool
|
||||
{
|
||||
if self.hasAnyBlockedVisibleSnapshot(snapshots) {
|
||||
return true
|
||||
}
|
||||
if detectTahoeBlockedStatusItem,
|
||||
self.hasAnyTahoeHiddenNoProxyCandidate(evidence: evidence, windowSnapshots: windowSnapshots)
|
||||
{
|
||||
return true
|
||||
}
|
||||
guard detectTahoeBlockedStatusItem,
|
||||
self.hasAnyDisplacedVisibleSnapshot(snapshots),
|
||||
windowSnapshots.contains(where: \.isTahoeBlockedProxy)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func hasAnyTahoeHiddenNoProxyCandidate(
|
||||
evidence: [StatusItemStartupVisibilityEvidence],
|
||||
windowSnapshots: [MenuBarStatusItemWindowSnapshot])
|
||||
-> Bool
|
||||
{
|
||||
evidence.contains { item in
|
||||
// Tahoe can destroy the Control Center scene while leaving its enabled default behind.
|
||||
// Requiring both app intent and that default avoids treating ordinary hidden items as blocked.
|
||||
item.expectsVisibility
|
||||
&& item.visibilityDefault == true
|
||||
&& !item.snapshot.isVisible
|
||||
&& !item.snapshot.hasWindow
|
||||
&& !windowSnapshots.contains {
|
||||
$0.name == item.autosaveName && $0.isOnscreen && $0.isWithinDisplayBounds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func visibilitySnapshots(_ items: [NSStatusItem]) -> [StatusItemVisibilitySnapshot] {
|
||||
items.map { item in
|
||||
self.visibilitySnapshot(item)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func hasBlockedVisibleStatusItems(_ items: [NSStatusItem]) -> Bool {
|
||||
self.hasBlockedVisibleSnapshots(self.visibilitySnapshots(items))
|
||||
}
|
||||
|
||||
static func shouldAttemptStartupRecovery(
|
||||
appLaunchedAt: Date,
|
||||
now: Date = Date(),
|
||||
snapshots: [StatusItemVisibilitySnapshot],
|
||||
evidence: [StatusItemStartupVisibilityEvidence] = [],
|
||||
windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
|
||||
detectTahoeBlockedStatusItem: Bool = false)
|
||||
-> Bool
|
||||
{
|
||||
guard now.timeIntervalSince(appLaunchedAt) <= self.startupFreshnessInterval else { return false }
|
||||
return self.hasAnyStartupRecoveryCandidate(
|
||||
snapshots: snapshots,
|
||||
evidence: evidence,
|
||||
windowSnapshots: windowSnapshots,
|
||||
detectTahoeBlockedStatusItem: detectTahoeBlockedStatusItem)
|
||||
}
|
||||
|
||||
static func shouldRefreshScreenChangePlacement(
|
||||
previousScreenCount _: Int,
|
||||
currentScreenCount _: Int,
|
||||
snapshots: [StatusItemVisibilitySnapshot])
|
||||
-> Bool
|
||||
{
|
||||
self.hasAnyDisplacedVisibleSnapshot(snapshots)
|
||||
}
|
||||
|
||||
static func shouldAttemptScreenChangeRecovery(snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
|
||||
self.hasAnyBlockedVisibleSnapshot(snapshots)
|
||||
}
|
||||
|
||||
static func shouldShowGuidance(defaults: UserDefaults, now: Date = Date()) -> Bool {
|
||||
guard defaults.bool(forKey: self.guidanceShownKey) else { return true }
|
||||
let lastShownAt = defaults.double(forKey: self.guidanceLastShownAtKey)
|
||||
guard lastShownAt > 0 else { return false }
|
||||
return now.timeIntervalSince1970 - lastShownAt >= self.guidanceRepeatInterval
|
||||
}
|
||||
|
||||
static func markGuidanceShown(defaults: UserDefaults, now: Date = Date()) {
|
||||
defaults.set(true, forKey: self.guidanceShownKey)
|
||||
defaults.set(now.timeIntervalSince1970, forKey: self.guidanceLastShownAtKey)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func presentGuidance(
|
||||
defaults: UserDefaults,
|
||||
now: Date = Date(),
|
||||
openURL: (URL) -> Void = { NSWorkspace.shared.open($0) })
|
||||
{
|
||||
self.markGuidanceShown(defaults: defaults, now: now)
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = L("CodexBar can't show its menu bar icon")
|
||||
alert.informativeText = L(
|
||||
"macOS Tahoe can block menu bar apps in System Settings → Menu Bar → Allow in the Menu Bar. "
|
||||
+ "CodexBar is running, but macOS may be hiding its icon. Open Menu Bar settings and turn CodexBar on.")
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: L("Open Menu Bar Settings"))
|
||||
alert.addButton(withTitle: L("Dismiss"))
|
||||
|
||||
if alert.runModal() == .alertFirstButtonReturn {
|
||||
openURL(self.settingsURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusItemController {
|
||||
func scheduleStartupStatusItemVisibilityCheck(appLaunchedAt: Date = Date()) {
|
||||
guard !SettingsStore.isRunningTests else { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + MenuBarVisibilityWatcher.startupCheckDelay) { [weak self] in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.checkStartupStatusItemVisibility(appLaunchedAt: appLaunchedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func checkStartupStatusItemVisibility(appLaunchedAt: Date, now: Date = Date()) {
|
||||
let evidence = self.startupStatusItemVisibilityEvidence()
|
||||
let snapshots = evidence.map(\.snapshot)
|
||||
let windowSnapshots = self.statusItemWindowSnapshots()
|
||||
guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
|
||||
appLaunchedAt: appLaunchedAt,
|
||||
now: now,
|
||||
snapshots: snapshots,
|
||||
evidence: evidence,
|
||||
windowSnapshots: windowSnapshots,
|
||||
detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
self.menuLogger.error(
|
||||
"Status item failed to materialize or remained detached; recreating status items",
|
||||
metadata: [
|
||||
"snapshots": snapshots.map(\.description).joined(separator: " | "),
|
||||
"evidence": evidence.map(\.description).joined(separator: " | "),
|
||||
"windows": self.statusItemWindowDiagnosticsDescription(windowSnapshots),
|
||||
])
|
||||
self.recreateStatusItemsForVisibilityRecovery()
|
||||
|
||||
let recoveredEvidence = self.startupStatusItemVisibilityEvidence()
|
||||
let recoveredSnapshots = recoveredEvidence.map(\.snapshot)
|
||||
let recoveredWindowSnapshots = self.statusItemWindowSnapshots()
|
||||
guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
|
||||
appLaunchedAt: appLaunchedAt,
|
||||
now: now,
|
||||
snapshots: recoveredSnapshots,
|
||||
evidence: recoveredEvidence,
|
||||
windowSnapshots: recoveredWindowSnapshots,
|
||||
detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
|
||||
else {
|
||||
self.menuLogger.info(
|
||||
"Status item materialized after recreation",
|
||||
metadata: ["snapshots": recoveredSnapshots.map(\.description).joined(separator: " | ")])
|
||||
return
|
||||
}
|
||||
|
||||
self.menuLogger.error(
|
||||
"Status item still unavailable after recreation",
|
||||
metadata: [
|
||||
"snapshots": recoveredSnapshots.map(\.description).joined(separator: " | "),
|
||||
"evidence": recoveredEvidence.map(\.description).joined(separator: " | "),
|
||||
"windows": self.statusItemWindowDiagnosticsDescription(recoveredWindowSnapshots),
|
||||
])
|
||||
guard #available(macOS 26.0, *),
|
||||
MenuBarVisibilityWatcher.shouldShowGuidance(defaults: self.settings.userDefaults, now: now)
|
||||
else {
|
||||
return
|
||||
}
|
||||
MenuBarVisibilityWatcher.presentGuidance(defaults: self.settings.userDefaults, now: now)
|
||||
}
|
||||
|
||||
@objc func handleScreenParametersDidChange(_: Notification) {
|
||||
let previousScreenCount = max(
|
||||
self.pendingScreenChangePreviousCount ?? self.lastKnownScreenCount,
|
||||
self.lastKnownScreenCount)
|
||||
let currentScreenCount = NSScreen.screens.count
|
||||
self.pendingScreenChangePreviousCount = previousScreenCount
|
||||
self.lastKnownScreenCount = currentScreenCount
|
||||
self.scheduleScreenChangeStatusItemVisibilityCheck(
|
||||
previousScreenCount: previousScreenCount,
|
||||
currentScreenCount: currentScreenCount)
|
||||
}
|
||||
|
||||
private func scheduleScreenChangeStatusItemVisibilityCheck(
|
||||
previousScreenCount: Int,
|
||||
currentScreenCount: Int)
|
||||
{
|
||||
guard !SettingsStore.isRunningTests else { return }
|
||||
self.screenChangeVisibilityTask?.cancel()
|
||||
self.screenChangeVisibilityTask = Task { @MainActor [weak self] in
|
||||
do {
|
||||
try await Task.sleep(for: MenuBarVisibilityWatcher.screenChangeCheckDelay)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
self?.checkScreenChangeStatusItemVisibility(
|
||||
previousScreenCount: previousScreenCount,
|
||||
currentScreenCount: currentScreenCount)
|
||||
}
|
||||
}
|
||||
|
||||
private func checkScreenChangeStatusItemVisibility(previousScreenCount: Int, currentScreenCount: Int) {
|
||||
self.pendingScreenChangePreviousCount = nil
|
||||
let settledCurrentScreenCount = NSScreen.screens.count
|
||||
self.lastKnownScreenCount = settledCurrentScreenCount
|
||||
let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
|
||||
if MenuBarVisibilityWatcher.shouldAttemptScreenChangeRecovery(snapshots: snapshots) {
|
||||
self.menuLogger.error(
|
||||
"Display configuration changed; recreating status items",
|
||||
metadata: [
|
||||
"previousScreenCount": "\(previousScreenCount)",
|
||||
"currentScreenCount": "\(settledCurrentScreenCount)",
|
||||
"capturedScreenCount": "\(currentScreenCount)",
|
||||
"snapshots": snapshots.map(\.description).joined(separator: " | "),
|
||||
"windows": self.statusItemWindowDiagnosticsDescription(),
|
||||
])
|
||||
self.recreateStatusItemsForVisibilityRecovery()
|
||||
self.schedulePostScreenChangeRecoveryVerification(attempt: 1)
|
||||
return
|
||||
}
|
||||
|
||||
guard MenuBarVisibilityWatcher.shouldRefreshScreenChangePlacement(
|
||||
previousScreenCount: previousScreenCount,
|
||||
currentScreenCount: settledCurrentScreenCount,
|
||||
snapshots: snapshots)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
self.menuLogger.info(
|
||||
"Display configuration changed; refreshing existing status items",
|
||||
metadata: [
|
||||
"previousScreenCount": "\(previousScreenCount)",
|
||||
"currentScreenCount": "\(settledCurrentScreenCount)",
|
||||
"capturedScreenCount": "\(currentScreenCount)",
|
||||
"snapshots": snapshots.map(\.description).joined(separator: " | "),
|
||||
])
|
||||
self.refreshExistingStatusItemsForVisibilityRecovery()
|
||||
}
|
||||
|
||||
private func schedulePostScreenChangeRecoveryVerification(attempt: Int) {
|
||||
self.screenChangeVisibilityTask = Task { @MainActor [weak self] in
|
||||
do {
|
||||
try await Task.sleep(for: MenuBarVisibilityWatcher.screenChangeFollowUpDelay)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
self?.verifyScreenChangeRecoveryIfNeeded(attempt: attempt)
|
||||
}
|
||||
}
|
||||
|
||||
private func verifyScreenChangeRecoveryIfNeeded(attempt: Int) {
|
||||
let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
|
||||
guard MenuBarVisibilityWatcher.hasAnyBlockedVisibleSnapshot(snapshots) else {
|
||||
self.menuLogger.info(
|
||||
"Status item recovered after display-change recovery",
|
||||
metadata: ["attempt": "\(attempt)", "snapshots": snapshots.map(\.description).joined(separator: " | ")])
|
||||
return
|
||||
}
|
||||
|
||||
self.menuLogger.error(
|
||||
"Status item still blocked after display-change recovery; recreating status items again",
|
||||
metadata: [
|
||||
"attempt": "\(attempt)",
|
||||
"snapshots": snapshots.map(\.description).joined(separator: " | "),
|
||||
"windows": self.statusItemWindowDiagnosticsDescription(),
|
||||
])
|
||||
self.recreateStatusItemsForVisibilityRecovery()
|
||||
// No further async retries: a menu bar manager may park the newly recreated item in a state
|
||||
// that still looks blocked, causing repeated NSStatusItem destruction that corrupts Control Center.
|
||||
// Instead, do one synchronous re-check to surface guidance if macOS itself is blocking the item.
|
||||
let finalSnapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
|
||||
guard MenuBarVisibilityWatcher.hasAnyBlockedVisibleSnapshot(finalSnapshots) else { return }
|
||||
self.menuLogger.error(
|
||||
"Status item still blocked after display-change recovery recreation",
|
||||
metadata: [
|
||||
"snapshots": finalSnapshots.map(\.description).joined(separator: " | "),
|
||||
"windows": self.statusItemWindowDiagnosticsDescription(),
|
||||
])
|
||||
guard #available(macOS 26.0, *),
|
||||
MenuBarVisibilityWatcher.shouldShowGuidance(defaults: self.settings.userDefaults)
|
||||
else { return }
|
||||
MenuBarVisibilityWatcher.presentGuidance(defaults: self.settings.userDefaults)
|
||||
}
|
||||
|
||||
private var startupVisibilityStatusItems: [NSStatusItem] {
|
||||
[self.statusItem] + Array(self.statusItems.values)
|
||||
}
|
||||
|
||||
private func startupStatusItemVisibilityEvidence() -> [StatusItemStartupVisibilityEvidence] {
|
||||
self.startupVisibilityStatusItems.map { item in
|
||||
let autosaveName = item.autosaveName ?? ""
|
||||
return StatusItemStartupVisibilityEvidence(
|
||||
autosaveName: autosaveName,
|
||||
expectsVisibility: self.expectedVisibleStatusItemAutosaveNames.contains(autosaveName),
|
||||
visibilityDefault: MenuBarStatusItemDefaultsRepair.visibilityDefault(
|
||||
defaults: self.settings.userDefaults,
|
||||
autosaveName: autosaveName),
|
||||
snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
|
||||
}
|
||||
}
|
||||
|
||||
private var canDetectTahoeBlockedStatusItem: Bool {
|
||||
if #available(macOS 26.0, *) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func statusItemWindowSnapshots() -> [MenuBarStatusItemWindowSnapshot] {
|
||||
let names = Set(self.startupVisibilityStatusItems.compactMap { item in
|
||||
item.autosaveName.isEmpty ? nil : item.autosaveName
|
||||
})
|
||||
return MenuBarStatusItemWindowProbe.snapshots(matching: names)
|
||||
}
|
||||
|
||||
private func statusItemWindowDiagnosticsDescription(
|
||||
_ snapshots: [MenuBarStatusItemWindowSnapshot]? = nil)
|
||||
-> String
|
||||
{
|
||||
let snapshots = snapshots ?? self.statusItemWindowSnapshots()
|
||||
guard !snapshots.isEmpty else { return "none" }
|
||||
return snapshots.map(\.description).joined(separator: " | ")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user