Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions Sources/OmniWM/UI/HiddenBar/HiddenBarMenuGuard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,47 @@
// Copyright (C) 2026 OmniNull — https://github.com/OmniNull/OmniWM

import CoreGraphics
import Foundation

enum HiddenBarMenuGuard {
struct WindowInfo {
let layer: Int
let ownerPID: pid_t
let title: String?
var bounds: CGRect = .zero
}

static func isMenuOpen(
windows: [WindowInfo],
menuOwnerPIDs: Set<pid_t>
menuOwnerPIDs: Set<pid_t>,
menuBarFrames: [CGRect] = []
) -> Bool {
guard !menuOwnerPIDs.isEmpty else { return false }
let popUpLevel = Int(CGWindowLevelForKey(.popUpMenuWindow))
let menuLevels: Set<Int> = [popUpLevel, popUpLevel - 1]
let statusBarLevel = Int(CGWindowLevelForKey(.statusWindow))
return windows.contains { window in
menuLevels.contains(window.layer)
&& (window.title?.isEmpty ?? true)
&& menuOwnerPIDs.contains(window.ownerPID)
guard menuOwnerPIDs.contains(window.ownerPID) else { return false }
if menuLevels.contains(window.layer), window.title?.isEmpty ?? true { return true }
return window.layer == statusBarLevel && !window.bounds.isEmpty && !menuBarFrames.isEmpty
&& !menuBarFrames.contains { $0.contains(window.bounds) }
}
}

static func isAnyMenuOpen(menuOwnerPIDs: Set<pid_t>) -> Bool? {
static func isAnyMenuOpen(menuOwnerPIDs: Set<pid_t>, menuBarFrames: [CGRect]) -> Bool? {
guard !menuOwnerPIDs.isEmpty else { return false }
guard let list = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: Any]]
else { return nil }
let windows = list.compactMap { info -> WindowInfo? in
guard let layer = info[kCGWindowLayer as String] as? Int,
let pid = info[kCGWindowOwnerPID as String] as? pid_t
else { return nil }
return WindowInfo(layer: layer, ownerPID: pid, title: info[kCGWindowName as String] as? String)
let bounds = (info[kCGWindowBounds as String] as? NSDictionary)
.flatMap { CGRect(dictionaryRepresentation: $0) } ?? .zero
return WindowInfo(
layer: layer, ownerPID: pid, title: info[kCGWindowName as String] as? String, bounds: bounds
)
}
return isMenuOpen(windows: windows, menuOwnerPIDs: menuOwnerPIDs)
return isMenuOpen(windows: windows, menuOwnerPIDs: menuOwnerPIDs, menuBarFrames: menuBarFrames)
}
}
13 changes: 11 additions & 2 deletions Sources/OmniWM/UI/HiddenBar/MenuBarItemService.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-only
// Copyright (C) 2026 OmniNull — https://github.com/OmniNull/OmniWM

import Foundation
import AppKit

private final class LockedRunLoopJobSet: @unchecked Sendable {
private let lock = NSLock()
Expand Down Expand Up @@ -91,8 +91,17 @@ final class MenuBarItemService {
func isMenuOpen(ownerPIDs: Set<pid_t>) async -> Bool? {
guard !stopped else { return nil }
let epoch = self.epoch
let menuBarFrames = NSScreen.screens.map { screen in
let height = max(
NSStatusBar.system.thickness,
max(screen.safeAreaInsets.top, screen.frame.maxY - screen.visibleFrame.maxY)
)
return ScreenCoordinateSpace.toWindowServer(rect: CGRect(
x: screen.frame.minX, y: screen.frame.maxY - height, width: screen.frame.width, height: height
))
}
let menuOpen = await Task.detached(priority: .userInitiated) {
HiddenBarMenuGuard.isAnyMenuOpen(menuOwnerPIDs: ownerPIDs)
HiddenBarMenuGuard.isAnyMenuOpen(menuOwnerPIDs: ownerPIDs, menuBarFrames: menuBarFrames)
}.value
guard !Task.isCancelled, isCurrent(epoch) else { return nil }
return menuOpen
Expand Down
38 changes: 38 additions & 0 deletions Tests/OmniWMTests/HiddenBarLifecyclePolicyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,44 @@ final class HiddenBarLifecyclePolicyTests: XCTestCase {
hiddenBar.cleanup()
}

@MainActor
func testReconcealmentWaitsForNativePanelToClose() async {
let controller = WindowAdmissionTestSupport.controller(prefix: "HiddenBarNativePanelGuard")
let hiddenBar = controller.hiddenBarController
controller.settings.hiddenBar.rehideIntervalSeconds = 5
let startedAt = ContinuousClock().now
var now = startedAt
var panelOpen = true
let menuBar = CGRect(x: 0, y: 0, width: 2560, height: 24)
let panel = HiddenBarMenuGuard.WindowInfo(
layer: Int(CGWindowLevelForKey(.statusWindow)), ownerPID: 500, title: nil,
bounds: CGRect(x: 2150, y: 24, width: 280, height: 180)
)
hiddenBar.reconcealment.menuGuardNow = { now }
hiddenBar.reconcealment.menuGuardSleeper = { delay in
now = now.advanced(by: delay)
if startedAt.duration(to: now) >= .seconds(8), panelOpen {
XCTAssertFalse(hiddenBar.temporarilyRevealedBundleIDsForTests.isEmpty)
panelOpen = false
}
await Task.yield()
}
hiddenBar.reconcealment.menuOpenProviderForTests = { _ in
HiddenBarMenuGuard.isMenuOpen(
windows: panelOpen ? [panel] : [], menuOwnerPIDs: [500], menuBarFrames: [menuBar]
)
}
hiddenBar.performance.begin()
hiddenBar.startReconcealForTests(revealedBundleIDs: ["com.omniwm.native-panel"])
await driveReconcealTask(hiddenBar)

XCTAssertFalse(panelOpen)
XCTAssertGreaterThanOrEqual(startedAt.duration(to: now), .seconds(13))
XCTAssertEqual(hiddenBar.performance.end()?.terminalReason, .concealed)
XCTAssertTrue(hiddenBar.temporarilyRevealedBundleIDsForTests.isEmpty)
hiddenBar.cleanup()
}

@MainActor
func testDefinitelyOpenMenuUsesWatchdogInsteadOfUnknownBound() async {
let controller = WindowAdmissionTestSupport.controller(prefix: "HiddenBarWatchdog")
Expand Down
58 changes: 58 additions & 0 deletions Tests/OmniWMTests/HiddenBarMenuGuardTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,64 @@ final class HiddenBarMenuGuardTests: XCTestCase {
)
}

func testNativePanelDefersRehideUntilOnlyItsStatusItemRemains() {
let menuBar = CGRect(x: 0, y: 0, width: 2560, height: 24)
let item = HiddenBarMenuGuard.WindowInfo(
layer: Int(CGWindowLevelForKey(.statusWindow)), ownerPID: 500, title: nil,
bounds: CGRect(x: 2400, y: 0, width: 28, height: 24)
)
for title in [nil, "Native panel"] {
let panel = HiddenBarMenuGuard.WindowInfo(
layer: item.layer, ownerPID: item.ownerPID, title: title,
bounds: CGRect(x: 2150, y: 24, width: 280, height: 180)
)
XCTAssertTrue(HiddenBarMenuGuard.isMenuOpen(
windows: [item, panel], menuOwnerPIDs: [500], menuBarFrames: [menuBar]
))
}
XCTAssertFalse(HiddenBarMenuGuard.isMenuOpen(
windows: [item], menuOwnerPIDs: [500], menuBarFrames: [menuBar]
))
}

func testNativePanelGuardKeepsOwnerAndLayerChecks() {
let bounds = CGRect(x: 2150, y: 24, width: 280, height: 180)
let menuBar = CGRect(x: 0, y: 0, width: 2560, height: 24)
for window in [
HiddenBarMenuGuard.WindowInfo(
layer: Int(CGWindowLevelForKey(.statusWindow)), ownerPID: 42, title: nil, bounds: bounds
),
HiddenBarMenuGuard.WindowInfo(layer: 0, ownerPID: 500, title: nil, bounds: bounds)
] {
XCTAssertFalse(HiddenBarMenuGuard.isMenuOpen(
windows: [window], menuOwnerPIDs: [500], menuBarFrames: [menuBar]
))
}
}

func testStatusItemsOnNotchedAndOffsetDisplaysDoNotDeferRehide() {
let menuBars = [
CGRect(x: 0, y: 0, width: 2560, height: 24),
CGRect(x: 2560, y: -1050, width: 1680, height: 38)
]
let items = menuBars.map { bar in
HiddenBarMenuGuard.WindowInfo(
layer: Int(CGWindowLevelForKey(.statusWindow)), ownerPID: 500, title: nil,
bounds: CGRect(x: bar.maxX - 50, y: bar.minY, width: 28, height: bar.height)
)
}
XCTAssertFalse(HiddenBarMenuGuard.isMenuOpen(
windows: items, menuOwnerPIDs: [500], menuBarFrames: menuBars
))
let panel = HiddenBarMenuGuard.WindowInfo(
layer: items[1].layer, ownerPID: 500, title: nil,
bounds: CGRect(x: 3900, y: -1012, width: 280, height: 180)
)
XCTAssertTrue(HiddenBarMenuGuard.isMenuOpen(
windows: items + [panel], menuOwnerPIDs: [500], menuBarFrames: menuBars
))
}

func testEmptyOwnerSetShortCircuits() {
XCTAssertFalse(
HiddenBarMenuGuard.isMenuOpen(
Expand Down