u/DustEnvironmental

▲ 0 r/rust

[macOS 15] NSMenuBar doesn't switch displays when using LSUIElement background process — keyboard input breaks after manual activation

Hi r/swift,

I'm building a macOS app with a two-process architecture and hitting a wall with menu bar + keyboard input on macOS 15 (Sequoia). Looking for ideas from anyone who's dealt with similar constraints.

Architecture

Swift launcher (com.rnvlt.lamia)
  └─ regular NSApplication
  └─ owns NSApp.mainMenu (the actual macOS menu bar)
  └─ handles IAP, Dock, Finder integration

LamiaCore (com.rnvlt.lamia.core)
  └─ LSUIElement = YES (background/agent app)
  └─ renders the actual window via winit/egui (Rust)
  └─ handles all keyboard input and editing

The window is drawn by LamiaCore (a Rust/winit process), while the menu bar belongs to the Swift launcher.

The Problem

Symptom 1 — Menu bar doesn't switch on multi-display setups:

  1. Launch Lamia on Display A → menu bar shows "Lamia" ✅
  2. Use another app (e.g. Terminal) on Display B
  3. Drag the Lamia window to Display B and click it
  4. Display B's menu bar still shows "Terminal" instead of "Lamia" ❌

Symptom 2 — Keyboard input breaks after forced activation: If I call NSRunningApplication.activateFromApplication(_:options:) from LamiaCore to make the Swift launcher take focus (which fixes the menu bar), the Swift launcher steals the key window. After that, LamiaCore's window no longer receives keyboard events even after makeKeyWindow().

What I've Tried

Attempt 1: NSApp.activate(ignoringOtherApps: true)

No-op on macOS 15. Apple changed the security model in Sonoma/Sequoia.

Attempt 2: NSDistributedNotificationCenter → Swift activates

→ Crashes inside App Sandbox. NSDistributedNotificationCenter falls back to local center and throws a selector mismatch exception.

Attempt 3: Unix socket IPC (LamiaCore notifies Swift → Swift activates)

→ Works in sandbox. But NSApp.activate(ignoringOtherApps:) is still a no-op on macOS 15, so the menu bar doesn't switch.

Attempt 4: activateFromApplication:options: (macOS 14+)

// Called from Rust via ObjC runtime
[swiftApp activateFromApplication:selfApp options:NSApplicationActivateIgnoringOtherApps]

Menu bar switches correctly ✅ but the Swift launcher steals the key window. After activation, makeKeyWindow() on LamiaCore's window does not restore keyboard input. windowDidBecomeKey fires but NSApp.keyWindow stays as Swift's window (which is invisible). Text input is completely broken until the user clicks something else and back ❌.

Attempt 5: Invisible NSWindow in Swift launcher (to retain "has windows" state)

→ No change. The key window ownership problem persists.

Core Constraint

The fundamental tension is:

Goal Required action Side effect
Update menu bar on display switch Swift launcher must become active app Steals key window from LamiaCore
Maintain keyboard input LamiaCore must remain key window Swift launcher stays inactive → menu bar frozen

On macOS 14 and earlier, activateFromApplication:options: with a subsequent makeKeyWindow() call on the LamiaCore window worked. On macOS 15, makeKeyWindow() after activation no longer restores keyboard focus to LamiaCore.

Question

Is there any way on macOS 15 to:

  1. Make the Swift launcher's menu bar appear (i.e., become the "active app"), and
  2. Keep keyboard input flowing into an LSUIElement background process's window?

Or is this combination fundamentally impossible with the current macOS 15 security model?

I've read that NSPanel with NSNonactivatingPanelMask might be a path forward, but that would require migrating LamiaCore's window management away from winit — a significant rewrite. Has anyone done this or found a lighter workaround?

Any ideas appreciated. Thanks!

Tags/Flair: Swift, macOS, Objective-C, AppKit, multi-display

reddit.com
u/DustEnvironmental — 7 days ago