# How to Define Custom Actions and Bindings in OpenLogi: A Complete Guide

> Learn to define custom actions and bindings in OpenLogi using the Action enum and TOML config. This guide shows how to map actions to physical buttons for personalized control.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: how-to-guide
- Published: 2026-08-27

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action.rs) and the TOML-based configuration system managed by [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

- `Copy`
- `PlayPause`
- `ScrollUp`

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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```toml
[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:

```toml
[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:

```toml

# ~/.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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs). This method implements a hierarchical merge strategy:

1. **Device-global bindings** serve as the base configuration.
2. **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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs).

You can also manipulate bindings programmatically using the Rust API:

```rust
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 **`Action`** enum at [`crates/openlogi-core/src/binding/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action.rs), which supports both simple commands and complex parameterized actions.
- You map buttons to actions via **[`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml)** in your platform-specific config directory, using `ButtonId` keys and TOML values that specify either bare action names or detailed payload tables.
- **`Config::effective_bindings`** in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) resolves final bindings by merging global defaults with per-application overrides.
- **`Workflow`** actions accept sequences of `WorkflowStep` enums to create scripted automations without external tools.
- The **`ActionRingInvocation`** struct in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs) carries 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.