# How OpenLogi Implements Foreground App Detection for Per-Application Profile Switching

> Discover how OpenLogi uses native OS APIs like NSWorkspace and WinEventHook for foreground app detection, enabling seamless per-application profile switching. Learn the technical details.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-09-13

---

**OpenLogi detects the active foreground application using native OS APIs—NSWorkspace notifications on macOS and WinEventHook on Windows—then maps the resulting bundle identifier or executable name to per-application profiles through an observable update channel.**

OpenLogi is an open-source input device management framework that automatically switches hardware profiles based on which application currently holds focus. The foreground app detection system lives primarily in the `openlogi-hook` and `openlogi-agent-core` crates, where platform-specific watchers monitor window activation events and propagate them to the profile resolution engine.

## Cross-Platform Detection Architecture

The detection pipeline follows a producer-consumer pattern across three layers:

- **Platform Hooks** (`crates/openlogi-hook/src/`) – Native threads that subscribe to OS-specific activation events.
- **Observable Channel** ([`crates/openlogi-agent-core/src/watchers/foreground_app.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent-core/src/watchers/foreground_app.rs)) – A type-safe `ForegroundUpdate` struct that wraps an `Observable<String>` to broadcast app ID changes.
- **Profile Resolver** ([`crates/openlogi-desktop/src/features/profiles/catalog.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/features/profiles/catalog.rs)) – Maps incoming app identifiers to configured profiles using normalized keys.

Both macOS and Windows implementations spawn dedicated background threads that run until a `HookControl` shutdown signal is received, ensuring non-blocking operation of the main agent loop.

## macOS Implementation with NSWorkspace

On macOS, the `MacOSWatcher` struct in [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs) initializes a foreground observer via `spawn_foreground_observer`. This method subscribes to the system-wide `NSWorkspaceDidActivateApplicationNotification` using the `objc2` framework to bridge Rust with Apple’s Objective-C runtime.

The callback extracts the frontmost application’s bundle identifier through a series of `msg_send` invocations:

```rust
extern "C" fn callback(this: &Object, _cmd: Sel, notification: *mut Object) {
    unsafe {
        let notif = &*notification;
        let user_info: *mut Object = msg_send![notif, userInfo];
        let app: *mut Object = msg_send![user_info, objectForKey: NSRunningApplication::class()];
        if !app.is_null() {
            let bundle_id: *mut Object = msg_send![app, bundleIdentifier];
            if !bundle_id.is_null() {
                let c_str: *const i8 = msg_send![bundle_id, UTF8String];
                if !c_str.is_null() {
                    let rust_str = std::ffi::CStr::from_ptr(c_str)
                        .to_string_lossy()
                        .into_owned();
                    // Propagate to ForegroundUpdate channel
                }
            }
        }
    }
}

```

The observer registers via `NSWorkspace::sharedWorkspace().notificationCenter()`, passing `sel!(callback:)` as the selector. The thread remains alive in a polling loop checking `control.is_shutdown()` every 200 milliseconds, ensuring the hook disables cleanly on exit.

## Windows Implementation with WinEventHook

For Windows, [`crates/openlogi-hook/src/windows/foreground.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/windows/foreground.rs) defines the `ForegroundApplicationObserver`, which leverages the `SetWinEventHook` API to monitor `EVENT_SYSTEM_FOREGROUND`.

The implementation creates an out-of-context hook with flags `WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS`, meaning the callback runs in the agent’s process space but ignores events from the agent itself:

```rust
let hook = unsafe {
    SetWinEventHook(
        EVENT_SYSTEM_FOREGROUND,
        EVENT_SYSTEM_FOREGROUND,
        None,
        Some(foreground_event_proc),
        0,
        0,
        WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS,
    )
};

```

The `foreground_event_proc` callback receives the window handle (`HWND`) of the newly activated application, which the implementation resolves to a process ID and executable name. A background thread maintains a message loop that polls an `Arc<AtomicBool>` shutdown flag, calling `UnhookWinEvent` on termination to release system resources.

## Profile Resolution and Matching

Once the platform layer emits an app identifier, the desktop frontend resolves the corresponding profile in [`crates/openlogi-desktop/src/features/profiles/catalog.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/features/profiles/catalog.rs). The `resolve_foreground_profile` function normalizes the incoming ID to lowercase to ensure case-insensitive matching against the catalog:

```rust
pub fn resolve_foreground_profile(appcatalog: &AppCatalog) -> Option<ProfileId> {
    let fg_app = appcatalog.foreground_application();
    let key = fg_app.id().to_ascii_lowercase();
    appcatalog.profile_for_key(&key)
}

```

The catalog keys derive from either macOS bundle identifiers (e.g., `com.apple.finder`) or Windows executable names (e.g., `code.exe`), allowing users to configure per-application profiles that activate automatically when the corresponding window gains focus.

## Threading and Lifecycle Management

Both platform implementations follow a strict lifecycle protocol to prevent resource leaks:

1. **Spawn** – Each watcher creates a `JoinHandle` that runs the platform-specific event loop.
2. **Signal** – A `HookControl` struct (macOS) or `Arc<AtomicBool>` (Windows) provides a thread-safe shutdown flag.
3. **Cleanup** – The `Drop` implementation sets the shutdown flag and joins the thread, ensuring hooks like `UnhookWinEvent` or `NSWorkspace` observers unregister before process exit.

This design guarantees that the foreground detection threads terminate gracefully when the OpenLogi agent shuts down, releasing OS-level resources and preventing dangling hooks.

## Summary

- **OpenLogi** uses **NSWorkspaceDidActivateApplicationNotification** on macOS and **SetWinEventHook** on Windows to monitor the active window.
- The `ForegroundUpdate` channel in `openlogi-agent-core` decouples platform hooks from business logic, broadcasting app ID changes via an `Observable<String>`.
- Profile resolution occurs in `openlogi-desktop`, where bundle IDs or executable names are normalized to lowercase keys for catalog lookups.
- Both implementations use dedicated threads with explicit shutdown signaling to ensure clean resource disposal.

## Frequently Asked Questions

### How does OpenLogi detect which application is active on macOS?

OpenLogi uses the `NSWorkspace` notification center to subscribe to `NSWorkspaceDidActivateApplicationNotification`. Through the `objc2` runtime bridge, it extracts the `bundleIdentifier` property from the `NSRunningApplication` object posted with the notification, converting it to a Rust `String` for downstream processing.

### What Windows API enables foreground window tracking in OpenLogi?

The Windows implementation calls `SetWinEventHook` from the Win32 API, requesting `EVENT_SYSTEM_FOREGROUND` events. This hook runs in `WINEVENT_OUTOFCONTEXT` mode, allowing the callback to execute within the agent’s address space without requiring a DLL injection into every process.

### Can OpenLogi distinguish between different instances of the same application?

Currently, the resolution logic relies on static identifiers—bundle IDs on macOS and executable names on Windows. While the hook receives window handles that could distinguish instances, the profile catalog maps only the application-level identifier to a profile, applying the same configuration to all windows of that app.

### How does the system handle rapid application switching?

The `ForegroundUpdate` observable channel processes every activation event individually. Because the platform hooks operate on dedicated threads separate from the UI loop, even rapid switches do not block the agent’s IPC communication, though the profile switch itself executes asynchronously through the desktop frontend’s state management.