Lab note · from Ghostmark
NSWindow behind all app windows on macOS: use normal level minus one
To keep a borderless NSWindow behind every ordinary app window but still in front of the wallpaper and desktop icons, Ghostmark sets its level to NSWindow.Level(rawValue: NSWindow.Level.normal.rawValue - 1), which is -1. Its first build used CGWindowLevelForKey(.desktopIconWindow) + 1, and a screen capture on the development Mac showed that window painted on top of a maximized Chrome window. Ghostmark puts one window on each display, which raises a second problem: identity. Key those windows by CGDirectDisplayID, read from deviceDescription["NSScreenNumber"], not by the NSScreen object.
Symptoms
Ghostmark tiles a watermark across each display in a click-through window. In Desktop mode it should sit under your apps.
- The first build set the Desktop level from the desktop-icon key. A
screencapturetaken while testing showed the watermark drawn over a maximized Chrome window. - The unit test could not catch it. It only checks that Desktop < Above Windows < Always on Top, which both -2147483602 and -1 satisfy. A test that compares only your own levels can't tell them apart.
- With more than one display, the first version keyed its per-display window controllers by
ObjectIdentifier(NSScreen). The project's code review found that any display change therefore tore down and rebuilt every overlay window, not just the window for the display that changed.
Why it happens
Levels are one integer order
CGWindowLevel.h defines levels as int32_t and says: "Windows with a higher level are sorted in front of windows with a lower level." Apple's NSWindow.Level page adds that "even the bottom window in a level will obscure the top window of the next level down." Reading the values back with CGWindowLevelForKey on macOS 27.2 gives the same numbers as the header macros:
.desktopWindow: -2147483623 (INT32_MIN + 25).desktopIconWindow: -2147483603 (INT32_MIN + 45).backstopMenu: -20.normalWindow: 0.floatingWindow: 3.screenSaverWindow: 1000
On paper, desktop-icon + 1 is over two billion levels below the normal level, 0, and should never be in front of an ordinary app window.
The bottom band belongs to the system
Apple's material steers applications away from that band. The header says levels are reached "via a key and function, so that levels may be changed or adjusted in future releases". The CGWindowLevelForKey reference is blunter: "This function is not recommended for use in applications. (This function is provided for application frameworks that create and manage windows, like Cocoa.)"
The band is also crowded. Listing every window with CGWindowListCopyWindowInfo on macOS 27.2 found:
- Finder at exactly the desktop-icon level.
- Two Window Server windows at desktop-icon + 1, the level Ghostmark first chose.
- Eight Notification Center windows at desktop-icon + 2.
- WindowManager, loginwindow and Window Server windows just below the desktop level.
No window used any level between -20 and 0.
The project never found why its window ended up above Chrome, but two causes can be ruled out.
- The value wasn't clamped. AppKit reads the level back as -2147483602, and the window server reports the same
kCGWindowLayer. - It didn't reproduce as an ordering fault on macOS 27.2. Apple documents
.optionOnScreenOnlyas returning windows "in order from front to back". On the macOS 27.2 machine used for this note, that list put a desktop-icon + 1 window behind every normal window.
The practical reading: that band is shared with system windows, and how it behaves isn't up to your app. The gap just below normal held no windows at all.
NSScreen objects are not stable keys
The NSScreen.screens reference says: "The array should not be cached. Screens can be added, removed, or dynamically reconfigured at any time." Ghostmark's review commit goes further and records that NSScreen instances are recreated on every screen-parameter change. An ObjectIdentifier taken before a change therefore never matches one taken after it.
Between changes the instances are stable (two back-to-back calls returned the same objects on the test machine), so the bug only shows up when a display is actually reconfigured. Apple documents a CGDirectDisplayID as an ID that "typically remains constant until the machine is restarted".
The fix
The first version, in WindowLevelMapping.swift:
case .desktop:
return NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.desktopIconWindow)) + 1)
The shipped version:
enum WindowLevelMapping {
static func nsLevel(for mode: WindowLevelMode) -> NSWindow.Level {
switch mode {
case .desktop:
return NSWindow.Level(rawValue: NSWindow.Level.normal.rawValue - 1) // -1
case .aboveWindows:
return .floating // 3
case .alwaysOnTop:
return .screenSaver // 1000
}
}
}
The other two modes are Above Windows (level 3, above normal windows but below the main menu level of 24) and Always on Top (level 1000, which the README describes as visible even over full-screen apps).
The window itself is set up in OverlayWindowController.swift:
let window = NSWindow(contentRect: screen.frame, styleMask: [.borderless],
backing: .buffered, defer: false, screen: screen)
window.isOpaque = false
window.backgroundColor = .clear
window.hasShadow = false
window.ignoresMouseEvents = true
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenAuxiliary]
window.level = WindowLevelMapping.nsLevel(for: settings.windowLevel)
window.orderFrontRegardless()
Level -1 is below every normal window (0) and above the desktop-icon level, so the watermark covers the wallpaper and icons but no app. Because it covers the icons, ignoresMouseEvents = true is required: clicks pass through to the icons and windows underneath.
NSWindow.h says a window whose level isn't normal defaults to the transient behaviour ("Floats in spaces, hidden by exposé"). .stationary replaces that ("Unaffected by exposé. Stays visible and stationary, like desktop window"), and .canJoinAllSpaces lets it "appear in all spaces", per Apple's reference.
For displays, AppDelegate.swift keys each controller by display ID:
private var overlayControllers: [CGDirectDisplayID: OverlayWindowController] = [:]
private static func displayID(for screen: NSScreen) -> CGDirectDisplayID? {
(screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value
}
private func rebuildOverlayWindows() {
let current = Set(NSScreen.screens.compactMap(AppDelegate.displayID))
for removed in Set(overlayControllers.keys).subtracting(current) {
overlayControllers[removed]?.tearDown() // orderOut + close
overlayControllers[removed] = nil
}
for screen in NSScreen.screens {
guard let id = AppDelegate.displayID(for: screen) else { continue }
if let existing = overlayControllers[id] {
existing.updateFrame(to: screen)
} else {
overlayControllers[id] = OverlayWindowController(screen: screen, settings: settings)
}
}
}
This runs on NSApplication.didChangeScreenParametersNotification. A display that stays connected keeps its window and gets a new frame; only added or removed displays create or destroy windows.
The key is documented on deviceDescription: "The value associated with this key is an NSNumber object containing the display ID value."
How to check you've fixed it
- Pixels, as the project did it. Maximize an ordinary window on the overlay's display, take a
screencapture, and confirm the watermark isn't drawn on top of it. Repeat for each mode. - Ordering. Find your window number in
CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID)and confirm it comes after every other app's level-0 window. A transparent 1×1 probe at -1 landed at index 12 on the test machine, after the normal windows at 8 to 11. - Level. Read back
window.level.rawValueand thekCGWindowLayerfor your window number. Both should be -1. - Displays. Connect or disconnect a display, or change the arrangement. Confirm that the controller for the display you didn't touch is reused, not created again. A log line in
OverlayWindowController.initmakes this easy to see.
Notes
- Target. The v1.0.0 release binary is arm64 only, with an
LC_BUILD_VERSIONminimum OS of 26.0, built against the 26.5 SDK. Ghostmark therefore targets macOS 26 and later on Apple silicon. - Scope. The paint-over was seen on one Mac in July 2026. It doesn't show that the desktop-icon level is broken on every macOS version, only that an app relying on that band depends on behaviour it doesn't control.
- A native property on macOS 26. The SDK has
NSScreen.cgDirectDisplayID(an optional,@available(macOS 26.0, *)), which matchedNSScreenNumberfor both displays on the test machine. Ghostmark still reads the dictionary key. - The trade-off of -1. The watermark is drawn over desktop icons, because they sit below it. That is why click-through is required here.
More lab notes
- Hide a macOS app the moment it activates, then ask for Touch IDHide the app inside the didActivateApplicationNotification handler, then run LAContext. It can't promise no flash: the app is already active.
- macOS global hotkey: RegisterEventHotKey vs an NSEvent monitorA Carbon hotkey that never fires may just be registered late. An NSEvent monitor needs Accessibility and can't stop the key reaching the frontmost app.
- Swift menu bar app stops updating: Timer run loop mode and App NapA Timer in the default run loop mode doesn't fire while a menu is open, and App Nap throttles it later. Add it in .common and hold a beginActivity token.
- AXIsProcessTrusted false after rebuild: ad-hoc signing and TCCAn ad-hoc signature's designated requirement is the build's cdhash, so changed code no longer matches its Accessibility grant. Sign with a stable identity.