How OpenLogi Actions Are Represented and Serialized in TOML: A Complete Guide

OpenLogi models every button-press outcome as a serde-annotated Rust Action enum in crates/openlogi-core/src/binding/action.rs, using external tagging to serialize unit variants as bare strings and payload-bearing variants as single-key tables in TOML configuration files.

OpenLogi stores user configurations in TOML files, and every executable action—from simple key presses to complex application workflows—travels through a well-defined serialization pipeline. This article examines how the Action enum bridges Rust's type system with human-editable TOML configuration, based on the source code in the AprilNEA/OpenLogi repository.

The Action Enum: Core Definition

The foundation of OpenLogi's action system lives in a single source file:

// File: crates/openlogi-core/src/binding/action.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Action {
    // Unit variants — no payload
    Copy,
    LeftClick,
    ShowDesktop,
    
    // Tuple variants — carry data
    CustomShortcut(KeyCombo),
    HoldShortcut(KeyCombo),
    
    // Struct variants — carry named fields
    OpenApplication(ApplicationTarget),
    Workflow(Vec<WorkflowStep>),
    // ... additional variants
}

The #[derive(Serialize, Deserialize)] attribute delegates all serialization logic to serde, Rust's standard serialization framework. OpenLogi relies on serde's default external tagging strategy without custom #[serde(...)] annotations on the enum itself.

TOML Serialization Rules for OpenLogi Actions

External tagging produces two distinct TOML shapes depending on variant type:

Variant Type Rust Example TOML Output
Unit variant Action::Copy "Copy"
Tuple/struct variant with payload Action::CustomShortcut(KeyCombo) { CustomShortcut = "Ctrl+Space" }

Unit Variants: Bare Strings

When an Action variant has no associated data, serde emits the variant name as a plain string. These appear throughout configuration files as direct value assignments:


# File: docs/config.example.toml excerpt

[devices."receiver:aabbccdd:slot:1".action_ring.default.slots]
Top    = { action = "Copy",         icon = "Keyboard" }
Bottom = { action = "ShowDesktop",  label = "Desktop" }

Both "Copy" and "ShowDesktop" are unit variants from the Action enum. The TOML parser recognizes these strings and deserializes them to Action::Copy and Action::ShowDesktop respectively.

Payload-Bearing Variants: Single-Key Tables

Variants that carry data serialize as single-key tables where the key matches the variant name and the value contains the payload:


# Tuple variant with string payload

MiddleClick = { HoldShortcut = "Ctrl+Space" }

# Struct variant with nested fields

Right = {
  action = {
    OpenApplication = {
      path = "/Applications/Safari.app",
      display_name = "Safari"
    }
  },
  icon = "Applications"
}

The OpenApplication example demonstrates nested external tagging: the outer table key (OpenApplication) identifies the enum variant, and the inner table provides the ApplicationTarget struct fields (path, display_name).

Complex Nested Types: Workflows

The Workflow variant accepts a vector of steps, each itself an enum following the same tagging rules:

Workflow = [
  { PressKey = { key = "Command", modifiers = ["Shift"] } },
  { Delay = { millis = 500 } },
  "ReleaseAllKeys"
]

Each element in the array independently applies external tagging—some as strings (unit variants), others as single-key tables.

Parsing Actions from TOML in Rust

The deserialization pipeline converts TOML strings directly into typed Action values through toml::from_str:

// Rust deserialization via serde
use openlogi_core::binding::action::Action;

let config_str = r#"
[devices."receiver:aabbccdd:slot:1".bindings]
Back         = "BrowserBack"
MiddleClick  = { CustomShortcut = "Ctrl+Space" }
"#;

let cfg: Config = toml::from_str(config_str)?;

// Unit variant deserialization
assert_eq!(
    cfg.devices["receiver:aabbccdd:slot:1"].bindings["Back"],
    Action::BrowserBack
);

// Payload variant deserialization
assert_eq!(
    cfg.devices["receiver:aabbccdd:slot:1"].bindings["MiddleClick"],
    Action::CustomShortcut(KeyCombo::parse("Ctrl+Space")?)
);

The toml crate handles the external tagging transparently—no custom deserializers are required for standard enum shapes.

Macro-Generated Catalog Support

The same action.rs file contains an X-macro pattern that generates auxiliary implementations for UI presentation. The for_each_unit_action! macro (lines 332-390) defines metadata for payload-free actions:

macro_rules! for_each_unit_action {
    ($callback:ident) => {
        $callback!(
            Copy, "Copy", "action.copy", "clipboard", Some("Clipboard"),
            LeftClick, "Left Click", "action.left_click", "mouse", None,
            ShowDesktop, "Show Desktop", "action.show_desktop", "desktop", Some("Desktop"),
            // ... additional entries
        );
    };
}

This macro feeds three generated trait implementations:

  • Action::label() — Produces display strings, incorporating payload data when present
  • Action::translation_key() — Returns stable i18n keys for unit actions
  • Action::catalog() — Enumerates all UI-selectable actions in deterministic order

The macro system keeps the action list in one location while supporting multiple derived behaviors.

Complete Configuration Examples

Minimal Unit Variants

[devices."receiver:aabbccdd:slot:1".bindings]
Back        = "BrowserBack"
Forward     = "BrowserForward"
LeftClick   = "LeftClick"
RightClick  = "RightClick"

Payload-Bearing Actions

[devices."receiver:aabbccdd:slot:1".bindings]

# Custom keyboard shortcut

MiddleClick = { CustomShortcut = "Ctrl+Space" }

# Application launcher with metadata

Right = {
  action = {
    OpenApplication = {
      path = "/Applications/Notes.app",
      display_name = "Notes"
    }
  },
  icon = "Applications",
  label = "Quick Notes"
}

Action Ring Configuration

[devices."receiver:aabbccdd:slot:1".action_ring.default.slots]
Top     = { action = "Copy", icon = "Clipboard" }
Right   = { action = { OpenApplication = { path = "/Applications/Safari.app", display_name = "Safari" } }, icon = "Globe" }
Bottom  = { action = "Paste", label = "Paste" }
Left    = { action = { CustomShortcut = "Command+Tab" }, icon = "Switch" }

Key Source Files

Path Responsibility
crates/openlogi-core/src/binding/action.rs Action enum definition, serde derives, and X-macro catalog generation
docs/config.example.toml Reference TOML demonstrating all serialization patterns
crates/openlogi-core/src/binding/action_ring.rs Action Ring UI wrapper consuming Action values
crates/openlogi-inject/src/lib.rs OS-level action execution (completes the round-trip)

Summary

  • OpenLogi serializes actions in TOML through a serde-backed Action enum in crates/openlogi-core/src/binding/action.rs.
  • Unit variants become bare strings: "Copy" deserializes to Action::Copy.
  • Payload variants become single-key tables: { CustomShortcut = "Ctrl+Space" } carries the key-combo data.
  • External tagging enables human-readable configuration without custom parsers or intermediate formats.
  • The X-macro system auto-generates labels, i18n keys, and UI catalogs from a single source of truth.

Frequently Asked Questions

Why does OpenLogi use external tagging instead of internal tagging for TOML serialization?

External tagging produces clearer configuration syntax. With external tagging, { CustomShortcut = "Ctrl+Space" } immediately identifies the variant name as the table key. Internal tagging would require a nested structure like { type = "CustomShortcut", value = "Ctrl+Space" }, which adds noise for human-edited files. The OpenLogi design prioritizes readability in docs/config.example.toml.

How can I add a new custom action variant that serializes to TOML?

Add the variant to the Action enum in crates/openlogi-core/src/binding/action.rs. For unit variants, include the variant in for_each_unit_action! to receive automatic UI catalog support. For payload variants, ensure the payload type implements Serialize and Deserialize. No additional TOML configuration is required—serde handles the rest.

Does OpenLogi support round-trip serialization without losing configuration data?

Yes. The Action enum derives both Serialize and Deserialize, and the codebase uses the same types for reading and writing configuration. When OpenLogi persists settings back to disk, the TOML output matches the input structure—unit variants remain strings, payload variants maintain their single-key table shape.

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 →