How OpenLogi's Foreground Application Watcher Detects and Switches Profiles on macOS, Windows, and Linux

OpenLogi uses platform-specific observers running inside the agent process to monitor foreground application changes, publishing updates to an IPC snapshot that the desktop UI consumes to automatically switch per-application profiles across macOS, Windows, and Linux.

OpenLogi (AprilNEA/OpenLogi) implements a sophisticated foreground application watcher that enables automatic profile switching based on the currently focused window. The system relies on an agent process that monitors platform-specific windowing events and communicates state changes to the desktop UI through an IPC mechanism. This architecture ensures consistent cross-platform behavior while respecting each operating system's unique APIs for window management.

Architecture of the Foreground Application Watcher

The system follows a unified data flow across all platforms: the agent detects changes, updates an IPC snapshot, and the desktop resolves the active profile.

The Agent Process and IPC Snapshot

The observer runs inside the agent process and constructs a ForegroundApp struct containing the application identifier and display name. When the foreground changes, the agent updates AgentSnapshot::foreground defined in crates/openlogi-ipc/src/ipc.rs. This snapshot acts as the single source of truth for the desktop UI.

Profile Resolution in the Desktop UI

The desktop UI reads the snapshot.foreground field and invokes state.set_foreground(foreground) to update the internal state. It then resolves the active profile by matching foreground.id against the keys in the per-app profile map. If no match exists or no foreground app is reported, the system falls back to the default global profile.

Platform-Specific Detection Mechanisms

Each platform implements a specialized backend that translates native windowing events into the common ForegroundApp abstraction.

macOS: NSWorkspace Notifications

On macOS, the watcher registers a block with NSWorkspaceDidActivateApplicationNotification to receive callbacks whenever the user switches applications. The handler, implemented in crates/openlogi-hook/src/macos.rs, extracts the bundle identifier and localized name from the NSRunningApplication object.

The key functions are:

  • watch_frontmost_application_activations – Registers the notification observer
  • foreground_app_from_running_application – Converts NSRunningApplication to ForegroundApp

The ForegroundApp.id becomes the bundle identifier, which the desktop matches against profile keys.

Windows: WinEvent Hooks

The Windows implementation in crates/openlogi-hook/src/windows/foreground.rs installs a global WinEvent hook for EVENT_SYSTEM_FOREGROUND. The ForegroundApplicationObserver runs on a dedicated thread and posts a custom WM_FOREGROUND_CHANGED message when the callback fires.

Upon processing the message, the system calls GetForegroundWindow() to retrieve the foreground window handle and resolves its executable path. The ForegroundApp.id is set to the lower-cased full executable path, ensuring consistent matching regardless of filesystem casing.

Key components include:

  • WinEventHook – Manages the global event hook
  • queue_snapshot – Handles the thread-safe emission of foreground updates

Linux: Multi-Backend Support

The Linux implementation in crates/openlogi-hook/src/linux.rs provides three backends to accommodate different display servers:

  • X11 – Uses the WM_CLASS property's class component via X11Source::frontmost_app
  • Wayland (wlroots) – Listens to the wlr-foreign-toplevel protocol
  • GNOME Shell – Queries the org.gnome.Shell DBus API

The Backend::frontmost_app method delegates to FRONTMOST.frontmost_app(), preferring the Wayland foreign-toplevel when available, falling back to X11 otherwise. The resulting ForegroundApp.id uses the class name (X11) or the toplevel's app-id (Wayland).

Implementation Examples

The following Rust snippets demonstrate how the agent initializes the observer on each platform:

// macOS – setting up the observer (run inside the agent)
let observer = openlogi_hook::watch_frontmost_application_activations(|app_opt| {
    // `app_opt` is `Option<ForegroundApp>`
    agent_state.set_foreground(app_opt.unwrap_or_default());
});
// Windows – creating the observer (agent side)
let observer = openlogi_hook::windows::foreground::ForegroundApplicationObserver::new(
    move |app_opt| {
        agent_state.set_foreground(app_opt.unwrap_or_default());
    },
);
// Linux – querying the current foreground app (agent side)
if let Some(fg) = openlogi_hook::Backend::frontmost_app() {
    agent_state.set_foreground(fg);
}

Summary

  • The agent process hosts platform-specific observers that detect foreground application changes using native APIs.
  • macOS uses NSWorkspaceDidActivateApplicationNotification to capture bundle identifiers when apps activate.
  • Windows installs a global WinEvent hook for EVENT_SYSTEM_FOREGROUND and resolves the executable path from the foreground window.
  • Linux supports X11, Wayland (wlroots), and GNOME Shell backends, selecting the best available at runtime.
  • The agent publishes changes to AgentSnapshot::foreground in crates/openlogi-ipc/src/ipc.rs.
  • The desktop UI consumes the IPC snapshot and matches foreground.id against per-app profile keys, falling back to the default profile when necessary.

Frequently Asked Questions

How does OpenLogi determine which profile to activate when switching applications?

The desktop UI reads the foreground field from the AgentSnapshot and compares the ForegroundApp.id against the keys in the per-application profile configuration. When a match is found, it activates that profile; otherwise, it uses the default global profile.

What happens on Linux if multiple desktop environments are detected?

The Linux backend in crates/openlogi-hook/src/linux.rs prioritizes the Wayland foreign-toplevel protocol if available. If the compositor does not support it, the system falls back to X11's WM_CLASS detection, ensuring compatibility across different desktop environments.

Why does the Windows implementation use a lower-cased executable path as the identifier?

The Windows observer lower-cases the full executable path to ensure consistent profile matching regardless of filesystem casing variations. This prevents profile lookup failures caused by path capitalization differences between the file system and user configuration.

Can the foreground detection run without the agent process?

No. According to the source architecture, the platform-specific observers (watch_frontmost_application_activations, ForegroundApplicationObserver, and Backend::frontmost_app) execute inside the agent process and publish updates via IPC. The desktop UI relies entirely on the AgentSnapshot provided by the agent to determine the current foreground application.

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 →