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

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:

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 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:

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 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:

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. The resolve_foreground_profile function normalizes the incoming ID to lowercase to ensure case-insensitive matching against the catalog:

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →