How OpenLogi Handles Config Schema Migration for Bindings and Physical Device Keys

OpenLogi implements a load-time migration system in its core crate that automatically rewrites TOML configuration files on first read after an application upgrade, versioning both binding definitions and physical device keys while creating pre-migration backups to prevent data loss.

The AprilNEA/OpenLogi repository stores user preferences in evolving TOML files that must remain compatible across application updates. To manage this, the project employs a sophisticated config schema migration pipeline that intercepts file loading, applies necessary transformations, and persists the upgraded format without manual user intervention.

Version-Gated Migration Entry Point

All migration logic originates in crates/openlogi-core/src/config.rs, where the Config struct tracks the schema_version of each file. When Config::load deserializes a configuration file, it immediately invokes Self::apply_migrations(version) to determine which transformations are required.

This method iterates over all registered migrations whose target version is greater than the file's current version. Only after applying every necessary upgrade does the application use the configuration, ensuring that internal logic always operates on the latest schema regardless of the file's original format.

Source: crates/openlogi-core/src/config.rs#L354-L362

Migrating Binding Definitions Across Schema Versions

Bindings are versioned entities in OpenLogi because variant renames and structural changes constitute migration events. The system specifically handles owner-locked gesture bindings through a dedicated migration path.

Handling Owner-Locked Gesture Bindings

For configuration files with schema versions ≤ 3, the system invokes Config::migrate_owner_locked_gestures to transform legacy gesture ownership models. This migration executes four critical steps:

  • Determining the gesture owner – The migration inspects device.gesture_owner.take() and passes it to Self::infer_gesture_owner to establish which physical button dispatches gesture events when the field is missing from older configs.

  • Demoting non-owner gestures – Every button that is not the owner but contains a Binding::Gesture is moved into DeviceConfig::disabled_gestures using device.disabled_gestures.insert(*id, map.clone()). The binding is then downgraded to a Binding::Single containing only the Click action via binding.demote_to_single(...).

  • Materializing the owner's map – If the owner is a HID++ gesture source, the migration seeds a full direction map containing all GestureDirection variants with default bindings. For non-gesture sources like haptic panels, the map is only inserted when explicitly needed.

  • Discarding deprecated fields – After migration completes, the consumed gesture_owner field is never serialized again; the shape of the binding itself becomes the source of truth, as noted in the source comment // the consumed gesture_owner never serializes again.

Source: crates/openlogi-core/src/config.rs#L354-L410

Physical Device Key Migration

Device identification keys underwent significant changes between v1 and v2 of the config format. The migration shim located in crates/openlogi-core/src/config/device.rs handles the transition by detecting legacy entry patterns.

The system identifies keys prefixed with receiver: or raw: and preserves those entries unchanged for devices that still rely on the old naming convention, such as a G502 reporting a specific raw HID++ ID. For newer devices, the migration renames keys to match the current schema while maintaining backward compatibility with legacy hardware definitions.

Source: crates/openlogi-core/src/config/device.rs#L2-L10

Backup Strategy for Key-Rewriting Migrations

Because migrations may rewrite the keys of an entire configuration file, OpenLogi implements a defensive backup mechanism. When a migration that alters file structure runs, ConfigFile::migration_backup_path generates a backup copy named <original>_pre-migration_<version>.toml.

This backup is written before the mutated configuration is saved, ensuring that a failure during the write operation cannot corrupt or lose the user's original file. The backup remains available for manual recovery if the migration produces unexpected results.

Source: crates/openlogi-core/src/config/file.rs#L104-L210

Testing Migration Correctness

The core crate includes a comprehensive test suite in crates/openlogi-core/src/config/tests.rs that exercises each migration path. These tests verify that backups are produced correctly, validate that resulting configurations match the expected modern schema, and ensure that data transformations preserve user-defined settings like disabled gestures across version upgrades.

Source: [crates/openlogi-core/src/config/tests.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/config/tests.rs)

Code Examples

The following examples demonstrate how to interact with the migration system programmatically:

// Automatically migrate a legacy config file on load
let cfg = Config::load("config.toml")?; // applies migrations if needed
println!("Current schema version: {}", cfg.schema_version);
// Manual invocation of the gesture-owner migration
let mut cfg = Config::load("config.toml")?;
cfg.migrate_owner_locked_gestures(); // rewrites older gesture bindings
cfg.save()?;                         // persists the migrated file
// Inspect the backup created by a migration
let backup_path = migration_backup_path("config.toml", old_version)?;
println!("Backup of pre-migration config: {}", backup_path.display());

Summary

  • Load-time detection: Config::load in config.rs automatically detects file versions and triggers apply_migrations before returning the configuration object.
  • Binding evolution: The migrate_owner_locked_gestures method handles complex binding restructures, including inferring gesture owners and demoting incompatible bindings to single actions.
  • Device key compatibility: Legacy device identifiers with receiver: or raw: prefixes are preserved or renamed in device.rs based on hardware requirements.
  • Data safety: The migration_backup_path mechanism creates timestamped backups before any destructive key-rewriting operations occur.
  • Verification: Comprehensive tests in tests.rs ensure that every migration path produces valid modern schemas without data loss.

Frequently Asked Questions

How does OpenLogi determine which migrations to run?

OpenLogi stores a schema_version integer in every TOML file. When Config::load executes, it compares this version against the application's current schema version. The system then invokes apply_migrations, which iterates through all migration functions targeting versions newer than the file's current state, applying them sequentially until the configuration reaches the latest format.

What happens to my original config file during a migration?

Before any changes are written, OpenLogi creates a backup using ConfigFile::migration_backup_path. This generates a file named <original>_pre-migration_<version>.toml containing the exact pre-migration state. Only after successfully creating this backup does the system write the upgraded configuration, ensuring you can manually restore the original if necessary.

Why are binding variants considered migration events in OpenLogi?

Binding variants require migration handling because structural changes—such as the transition to owner-locked gesture bindings in schema v3—affect how input events are dispatched. When the configuration format changes to remove fields like gesture_owner in favor of binding-shape-based logic, the migration system must transform existing user configurations to match the new API without losing custom gesture mappings or disabled actions.

Where are physical device key migrations implemented?

Physical device key migrations are handled in crates/openlogi-core/src/config/device.rs. This module specifically manages the transition between v1 and v2 configuration formats, detecting legacy keys that start with receiver: or raw: and determining whether to preserve them for backward compatibility or rename them according to modern device identification standards.

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 →