How OpenLogi Handles Config Schema Migrations for v1-v3 Gesture Layouts and Physical Key Transitions

OpenLogi performs atomic, load-time migrations that automatically rewrite legacy v1-v3 gesture layouts and pre-v4 transport-scoped physical keys into the current TOML schema, creating versioned backup files before any modifications occur.

OpenLogi persists user-customizable device settings in TOML configuration files tracked by a schema_version field. When the application initializes, it executes a version-gated migration pipeline that upgrades older configurations to the current runtime format, ensuring seamless backward compatibility without requiring manual file edits.

The Load-Time Migration Pipeline

The migration system operates transparently during config loading, transforming legacy structures before they reach the runtime. This process follows a strict three-phase sequence designed for data integrity.

Pre-Migration Safety: Automatic Backups

Before any schema transformation occurs, OpenLogi guarantees recoverability by cloning the original file. In crates/openlogi-core/src/config/file.rs, the ConfigFile::save method creates a backup at <file>.v<old>.bak (lines 104-184), preserving the pre-migration state. If a migration fails or the subsequent write corrupts data, users can restore the original configuration from this versioned backup.

Gesture Layout Migrations for v1-v3

For configuration files with schema_version ≤ 3, OpenLogi invokes Config::migrate_owner_locked_gestures (defined in crates/openlogi-core/src/config.rs, lines 59-88 under the fs feature). This method addresses the owner-locked gesture model used in early schema versions, performing four critical transformations:

  • Infer the gesture owner: If the config lacks an explicit owner field, the migration deduces which button owned the gesture map based on legacy layout rules.
  • Demote non-owner bindings: Gesture-shaped bindings belonging to non-owner buttons are converted to Single bindings that preserve only the click action, while the full gesture map is archived in the disabled_gestures field for potential later re-enabling.
  • Materialize default HID++ maps: For HID++ owners that previously relied on implicit default direction maps, the migration explicitly materializes these mappings to ensure the new runtime dispatches identical gestures.
  • Enforce canonical gesture button: The migration guarantees the presence of a dedicated ButtonId::GestureButton entry, ensuring the modern schema always contains a canonical gesture entry point.

As noted in the source comments (lines 68-84), the algorithm ensures "the owner keeps its gesture map" while "every other gesture-shaped binding is stashed into disabled_gestures," preventing data loss during the ownership model transition.

Physical Key Renames for Transport-Scoped Identities

Version 4 introduced a refined transport-scoped key format. Config::migrate_transport_scoped_keys (lines 39-70 in src/config.rs) rewrites legacy direct:<vid>:<pid>:unit:<id> keys to the modern direct:<vid>:<pid>:<identity-kind>:<identity> format.

The algorithm parses each legacy key, extracts the identity fragment, and constructs an old-to-new name mapping. It then removes legacy entries, inserts the updated keys, and folds duplicate renames—such as a mouse reachable via both USB and Bluetooth—ensuring no configuration data is lost during consolidation. The migration updates all internal references including selected_device and host_switch_targets to point to the new key format.

Deep Dive: v1-v3 Gesture Layout Transformations

The v1-v3 gesture system used an owner-locked model where only one button could "own" the gesture map while others could trigger gestures through shaped bindings. The modern schema eliminates this restriction, requiring specific migration logic to preserve user intent.

When migrate_owner_locked_gestures detects a legacy layout, it first identifies the gesture owner. Non-owner buttons with gesture-shaped bindings have their complex mappings demoted to simple Single actions (preserving only the click portion), while the full gesture specifications are stored in disabled_gestures. This allows users to later re-enable these gestures through the new UI without losing their original configuration.

For devices using HID++ protocol owners, the migration explicitly writes out previously implicit default direction mappings. This materialization step ensures that a config migrated from v3 produces identical hardware behavior to the legacy runtime, even though the internal representation has changed significantly.

Deep Dive: Transport-Scoped Key Migration

Physical device identification evolved between schema versions. Pre-v4 configs used a simplified unit identifier in the format direct:<vid>:<pid>:unit:<id>, while v4+ uses a more robust identity-kind system: direct:<vid>:<pid>:<identity-kind>:<identity>.

The migrate_transport_scoped_keys function handles this transition by:

  1. Parsing each legacy key to extract the vendor ID (VID), product ID (PID), and unit identifier.
  2. Mapping the unit segment to the appropriate identity-kind and identity pair based on transport context.
  3. Colliding entries—where the same physical device appeared under different transport paths (e.g., USB vs. Bluetooth unified receivers)—are folded into single canonical entries to prevent configuration fragmentation.

All references throughout the config hierarchy, including per-device settings and host-switch targets, are atomically updated to reference the new key format before the migration completes.

Working with Migrations Programmatically

While migrations run automatically during Config::load, developers can interact with the system explicitly for testing or custom tooling.

Loading a legacy config triggers automatic migration:

// Loading automatically applies migrations based on schema_version
let mut cfg = Config::load(path_to_toml)?;
assert_eq!(cfg.schema_version, CURRENT_VERSION);

For testing migration logic or forcing a re-serialization:

// Manual migration invocation (useful in test suites)
let mut cfg = Config::load(old_v3_path)?;
cfg.migrate_owner_locked_gestures();   // v1-v3 gesture fixes
cfg.migrate_transport_scoped_keys();   // physical key renaming
cfg.save()?; // Writes upgraded file and creates .v3.bak backup

Summary

  • OpenLogi uses a schema_version field in TOML files to track configuration schema evolution.
  • Load-time migrations execute automatically in Config::load, ensuring runtime code always receives modern schema structures.
  • The migrate_owner_locked_gestures method in src/config.rs transforms v1-v3 owner-locked gesture models by inferring owners, demoting non-owner bindings to Single actions, and archiving full maps in disabled_gestures.
  • The migrate_transport_scoped_keys method rewrites pre-v4 physical device keys from direct:<vid>:<pid>:unit:<id> to the transport-scoped format direct:<vid>:<pid>:<identity-kind>:<identity>, folding duplicates to prevent data loss.
  • Atomic backups are created at <file>.v<old>.bak by ConfigFile::save in src/config/file.rs before any write operations, ensuring recoverability.

Frequently Asked Questions

What triggers a config migration in OpenLogi?

Migrations trigger automatically when Config::load detects a schema_version value lower than the current schema version defined in the source code. The system checks this field immediately after parsing the TOML file and sequentially applies all necessary transformation functions before returning the configuration object to the caller.

How does OpenLogi prevent data loss during schema migration?

OpenLogi implements a defensive backup strategy in src/config/file.rs. Before writing the migrated configuration, ConfigFile::save copies the original file to a versioned backup path (.v<old>.bak). Additionally, gesture migrations preserve deprecated bindings by stashing them in disabled_gestures rather than deleting them, and physical key migrations fold duplicates rather than overwriting entries.

What is the difference between v1-v3 and v4 gesture layouts?

Versions 1-3 used an owner-locked gesture model where only one button could own the gesture map; other buttons could only reference gestures through shaped bindings. Version 4 eliminates this restriction, allowing any button to host gesture maps independently. The migration demotes legacy non-owner gesture bindings to simple click actions while preserving the full gesture specifications for potential reactivation.

Can I manually roll back a migration if something goes wrong?

Yes. OpenLogi creates a versioned backup file (e.g., config.toml.v3.bak) before applying any migration. To roll back, simply delete the migrated file and rename the backup to restore the original pre-migration state. The backup preserves the exact byte-level content of the file prior to schema transformation.

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 →