How OpenLogi Resolves Per-Application Profiles Across macOS, Linux, and Windows
OpenLogi resolves per-application profiles by normalizing platform-specific foreground application identifiers into a common ForegroundApp struct, then performing an exact-match lookup against configured profile keys in effective_bindings.
OpenLogi is an open-source input remapping tool that needs to switch profiles automatically based on which application currently has focus. This article explains how the codebase detects the foreground application on each supported operating system and maps it to the correct per-application profile configuration.
The Core Abstraction: ForegroundApp
All platform-specific detection logic converges on a single type defined in crates/openlogi-core/src/app.rs:
/// Platform-agnostic representation of the currently focused application
pub struct ForegroundApp {
/// OS-specific identifier used for profile matching
pub id: String,
/// Human-readable label for UI display
pub display_name: String,
}
The id field contains the raw platform identifier, while display_name provides a user-friendly label when the platform doesn't supply one. This separation allows OpenLogi to match profiles deterministically while still presenting readable names in the interface.
Platform-Specific Identifier Extraction
Each operating system provides a different native identifier that becomes the ForegroundApp.id. The hook layer implementations handle these differences without polluting the core logic.
macOS: Bundle Identifiers and Fallback Paths
In crates/openlogi-hook/src/macos.rs, the function foreground_app_from_running_application extracts the identifier from NSRunningApplication:
- Primary method: Read the
bundleIdentifierproperty (e.g.,com.apple.Safari) - Fallback: Use the executable path if the bundle ID is unavailable
The hook registers an observer via watch_frontmost_application_activations, which subscribes to NSWorkspaceDidActivateApplicationNotification and builds a ForegroundApp on each activation change.
// From macos.rs – notification callback converts NSRunningApplication
fn foreground_app_from_running_application(app: NSRunningApplication) -> Option<ForegroundApp> {
let id = app.bundle_identifier()
.map(|s| s.to_string())
.unwrap_or_else(|| app.executable_url()?.path().to_string());
Some(ForegroundApp {
id,
display_name: app.localized_name()?.to_string(),
})
}
Linux: X11 WM_CLASS and Wayland app_id
The Linux implementation in crates/openlogi-hook/src/linux.rs uses a trait-based FrontmostSource abstraction to support multiple window systems:
| Backend | Identifier Source | Example |
|---|---|---|
| X11 | WM_CLASS property (class component) |
firefox |
| Wayland | app_id via xdg-shell protocol |
org.mozilla.Firefox |
The FrontmostController selects the appropriate backend at runtime and yields a ForegroundApp with the same structure as macOS. This allows OpenLogi to work across display server transitions without configuration changes.
Windows: Executable Path Normalization
Windows detection in crates/openlogi-hook/src/windows.rs uses ForegroundApplicationObserver to monitor foreground window changes. The identifier is constructed from:
- Query the foreground window handle via
GetForegroundWindow() - Obtain the process ID with
GetWindowThreadProcessId() - Retrieve the full executable path via
QueryFullProcessImageNameW() - Lowercase the entire path for consistent matching
// Conceptual flow from windows.rs
let path = get_process_image_path(pid)?;
let id = path.to_lowercase(); // "c:\program files\chrome\chrome.exe"
This path-based approach avoids ambiguity that could arise from process names alone, since multiple applications may share the same executable name in different directories.
From Detection to Profile Resolution
The foreground application flows through four architectural layers before reaching the profile matching logic:
1. Hook Layer (Platform Detection)
Each platform's watch_frontmost_application_activations function accepts a callback that receives Option<ForegroundApp>:
// Unified API across all platforms
openlogi_hook::watch_frontmost_application_activations(|app| {
// app: Option<ForegroundApp>
agent.update_foreground(app);
});
2. Agent Core (State Management)
openlogi-agent-core maintains the latest ForegroundApp in its internal state, making it available for IPC serialization.
3. IPC Layer (Cross-Process Communication)
The ForegroundApp struct derives Serialize and Deserialize for tarpc-based communication. The wire format is defined in crates/openlogi-ipc/src/ipc.rs:
#[derive(Serialize, Deserialize)]
pub struct ForegroundApps {
pub current: Option<ForegroundApp>,
}
4. Desktop UI (Profile Matching)
The UI receives ForegroundApps messages and delegates to openlogi_core::config::effective_bindings for resolution. This function performs an exact string match between ForegroundApp.id and profile configuration keys:
// From openlogi-core configuration logic
impl Config {
pub fn effective_bindings(&self, app_id: String) -> Option<ProfileId> {
// Exact match only — no normalization or fuzzy matching
self.per_app_profiles.get(&app_id).cloned()
}
}
If no profile matches the current id, the system falls back to the "Default" profile.
Why Exact-Match Profile Keys Matter
The design intentionally avoids cross-platform identifier normalization. This has important implications for users:
- macOS profile with key
com.apple.Safariwill not match on Linux or Windows - Linux Wayland profile with key
org.mozilla.Firefoxdiffers from X11 keyfirefox - Windows profile must use the full lowercased path:
c:\program files\mozilla firefox\firefox.exe
Users must create separate per-application profiles for each platform where they run OpenLogi. This trade-off guarantees predictable behavior without heuristic matching that could activate wrong profiles.
Implementation Consistency Across Platforms
The modular architecture enables future platform support. Adding a new window system requires only:
- Implement a
FrontmostSourcetrait that returnsOption<String>for the identifier - Convert to
ForegroundAppwith appropriatedisplay_namefallback - Register with the common
watch_frontmost_application_activationsentry point
All downstream layers—agent state, IPC serialization, and profile resolution—remain unchanged.
Summary
ForegroundAppincrates/openlogi-core/src/app.rsprovides the unified abstraction for foreground application detection- macOS uses bundle identifiers with executable path fallback; Linux uses
WM_CLASSorapp_id; Windows uses lowercased executable paths - Profile matching in
effective_bindingsrequires exact string equality betweenForegroundApp.idand configuration keys - The pipeline flows through hook detection → agent state → IPC serialization → UI resolution with no platform-specific logic in the core matching path
- Cross-platform profile portability is intentionally sacrificed for matching determinism
Frequently Asked Questions
Why doesn't OpenLogi normalize identifiers across platforms?
Heuristic normalization introduces false positive risks. A macOS bundle ID like com.google.Chrome and a Linux app_id of google-chrome might represent the same browser, but automatic equivalence rules could incorrectly match unrelated applications. Exact matching puts control entirely in user configuration.
Can I use the same configuration file on macOS and Linux?
Only the "Default" profile and global settings are portable. Per-application profiles must be recreated for each platform with the appropriate id values. The configuration format supports platform-specific sections if future versions choose to add this feature.
What happens when a profile doesn't match any configured application?
The system transparently falls back to the "Default" profile. This ensures OpenLogi remains functional even when encountering previously unknown applications or when detection temporarily fails.
How does OpenLogi handle applications with no detectable identifier?
Some platforms may yield None from detection—for example, Wayland clients that don't set app_id or restricted system processes. In these cases, watch_frontmost_application_activations passes None to the callback, and the UI continues using the current profile without change.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →