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 toSelf::infer_gesture_ownerto 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::Gestureis moved intoDeviceConfig::disabled_gesturesusingdevice.disabled_gestures.insert(*id, map.clone()). The binding is then downgraded to aBinding::Singlecontaining only theClickaction viabinding.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
GestureDirectionvariants 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_ownerfield 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::loadinconfig.rsautomatically detects file versions and triggersapply_migrationsbefore returning the configuration object. - Binding evolution: The
migrate_owner_locked_gesturesmethod handles complex binding restructures, including inferring gesture owners and demoting incompatible bindings to single actions. - Device key compatibility: Legacy device identifiers with
receiver:orraw:prefixes are preserved or renamed indevice.rsbased on hardware requirements. - Data safety: The
migration_backup_pathmechanism creates timestamped backups before any destructive key-rewriting operations occur. - Verification: Comprehensive tests in
tests.rsensure 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →