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

> Learn how OpenLogi represents and serializes actions in TOML. Discover how Rust enums and external tagging create bare strings and single-key tables for seamless configuration.

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

---

**OpenLogi models every button-press outcome as a serde-annotated Rust `Action` enum in [`crates/openlogi-core/src/binding/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
// 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:

```toml

# 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:

```toml

# 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:

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

```rust
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

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

```

### Payload-Bearing Actions

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

```toml
[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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action.rs) | `Action` enum definition, serde derives, and X-macro catalog generation |
| [`docs/config.example.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/docs/config.example.toml) | Reference TOML demonstrating all serialization patterns |
| [`crates/openlogi-core/src/binding/action_ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action_ring.rs) | Action Ring UI wrapper consuming `Action` values |
| [`crates/openlogi-inject/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.