How to Define Custom Actions and Bindings in OpenLogi: A Complete Guide
You define custom actions and bindings in OpenLogi by selecting payload-carrying variants from the Action enum (such as CustomShortcut or Workflow) and mapping them to physical buttons via the TOML configuration file located at ~/.config/openlogi/config.toml, where the Config::effective_bindings method automatically merges global defaults with per-application overrides.
OpenLogi is an open-source input remapping framework that translates physical button presses into executable OS-level commands. To customize your input devices, you work with two core components: the extensible Action enum defined in crates/openlogi-core/src/binding/action.rs and the TOML-based configuration system managed by crates/openlogi-core/src/config.rs. This architecture allows you to create everything from simple keyboard shortcuts to complex multi-step workflows without modifying the Rust source code.
Understanding the Action Enum Structure
The foundation of every user-triggered operation resides in the Action enum located in crates/openlogi-core/src/binding/action.rs. This enum is deliberately designed to be extensible, separating operations into two distinct categories.
Payload-Free Actions
Payload-free actions are simple variants that execute fixed commands without additional parameters. Common examples include:
CopyPlayPauseScrollUp
These variants are processed by the for_each_unit_action! macro, which generates the label, category, and catalog helper methods. These helpers populate the UI picker with user-friendly descriptions.
Payload-Carrying Actions
Payload-carrying actions accept parameters that customize their behavior, enabling complex automation:
CustomShortcut(KeyCombo)– Dispatches arbitrary keyboard chords.Workflow(Vec<WorkflowStep>)– Executes scripted sequences of text input, delays, and key presses.OpenApplication(ApplicationTarget)– Launches specific programs.HoldShortcut(KeyCombo)– Implements "hold-to-talk" chords that activate while a button remains pressed.
These variants allow you to define custom actions entirely through configuration without extending the enum definition.
Configuring Bindings in config.toml
Bindings—the mapping from physical buttons to Action variants—are stored in a TOML configuration file (config.toml) located in the platform-specific config directory (e.g., ~/.config/openlogi/config.toml on Linux/macOS). The Config struct in crates/openlogi-core/src/config.rs parses this file and exposes the effective_bindings(device_id, app_bundle) method to resolve final mappings.
Global Bindings
Global bindings apply to your device regardless of the active application. Define them under the [bindings] table using ButtonId variants (defined in openlogi_core::binding::ButtonId) as keys:
[bindings]
ButtonId.Back = "Copy" # Simple payload-free action
ButtonId.Forward = { CustomShortcut = "Ctrl+Shift+T" } # Payload-carrying action
ButtonId.Middle = { Workflow = [
{ TypeText = "Hello, world!" },
{ Delay = { millis = 500 } },
{ PressKey = "Enter" }
] }
Values can be bare strings for payload-free actions or TOML tables specifying the variant name and its payload for complex operations.
Per-Application Overrides
You can override global bindings for specific applications by nesting configurations under the [per_app] table, using the application bundle ID as the key:
[per_app."com.microsoft.VSCode"]
ButtonId.Back = "Undo"
ButtonId.Forward = { CustomShortcut = "Ctrl+Shift+P" }
When resolving bindings, Config::effective_bindings merges these definitions, with per-app overrides taking precedence over global defaults.
Creating Complex Workflows
The Workflow action type accepts a vector of WorkflowStep enums, allowing you to script multi-step automations directly in TOML. Each step executes sequentially:
# ~/.config/openlogi/config.toml
[bindings]
ButtonId.Middle = { Workflow = [
{ TypeText = "git status" },
{ Delay = { millis = 200 } },
{ PressKey = "Enter" }
] }
Valid workflow steps include TypeText for string input, Delay for timed pauses, and PressKey for individual keystrokes. The injector (openlogi-inject) synthesizes these into OS-level commands at runtime.
Runtime Binding Resolution
When the GUI or CLI needs active mappings, it invokes Config::effective_bindings(device_id, app_bundle) from crates/openlogi-core/src/config.rs. This method implements a hierarchical merge strategy:
- Device-global bindings serve as the base configuration.
- Per-app overrides are layered on top when a specific application window is foreground.
This resolution guarantees that the most specific definition wins. The resulting map drives the Actions Ring (the on-screen radial menu), with slot-to-action mappings transmitted over IPC using the ActionRingInvocation struct defined in crates/openlogi-ipc/src/ipc.rs.
You can also manipulate bindings programmatically using the Rust API:
use openlogi_core::binding::{Action, KeyCombo, ButtonId};
use openlogi_core::config::Config;
// Build a custom shortcut programmatically
let combo = KeyCombo::parse("Ctrl+Alt+L").unwrap();
let custom = Action::CustomShortcut(combo);
// Bind it to a button in memory
let mut cfg = Config::load_default().unwrap();
cfg.set_binding(ButtonId::Back, custom.clone());
// Resolve the final map for a device (no per-app overrides)
let bindings = cfg.effective_bindings("2b042", None);
assert_eq!(bindings.get(&ButtonId::Back), Some(&custom));
Summary
- OpenLogi stores all possible operations in the
Actionenum atcrates/openlogi-core/src/binding/action.rs, which supports both simple commands and complex parameterized actions. - You map buttons to actions via
config.tomlin your platform-specific config directory, usingButtonIdkeys and TOML values that specify either bare action names or detailed payload tables. Config::effective_bindingsincrates/openlogi-core/src/config.rsresolves final bindings by merging global defaults with per-application overrides.Workflowactions accept sequences ofWorkflowStepenums to create scripted automations without external tools.- The
ActionRingInvocationstruct incrates/openlogi-ipc/src/ipc.rscarries binding data to the UI layer for the on-screen radial menu.
Frequently Asked Questions
Do I need to modify the Rust source code to add custom actions?
No. The Action enum in crates/openlogi-core/src/binding/action.rs already includes payload-carrying variants like CustomShortcut and Workflow that accept user-defined parameters. You create custom behavior by configuring these variants in config.toml rather than extending the enum.
What configuration file format does OpenLogi use?
OpenLogi uses TOML for its configuration files. The main configuration resides in config.toml within the platform-specific config directory (e.g., ~/.config/openlogi/config.toml on Linux/macOS), parsed by the Config struct in crates/openlogi-core/src/config.rs.
How does OpenLogi resolve conflicting bindings for the same button?
The Config::effective_bindings method implements a specificity-based merge: per-application bindings defined under [per_app."bundle_id"] override global bindings defined under [bindings]. When no application-specific override exists, the global binding applies.
Can I create different actions for the same button depending on which application is active?
Yes. Define global bindings under [bindings] for default behavior, then create application-specific tables under [per_app."com.example.AppName"] to override those bindings when that application is foregrounded. The resolution system automatically selects the appropriate mapping based on the current active window.
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 →