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 presentAction::translation_key()— Returns stable i18n keys for unit actionsAction::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
Actionenum incrates/openlogi-core/src/binding/action.rs. - Unit variants become bare strings:
"Copy"deserializes toAction::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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →