How OpenLogi Handles Configuration Schema Migrations: Inside the Version-Gated Migration System

OpenLogi automatically migrates outdated TOML configuration files to the current schema version by running a series of private migration helpers during Config::load_from_path, ensuring backward compatibility without user intervention.

OpenLogi stores user preferences in a TOML configuration file whose shape evolves as new features are added. To keep old configuration files usable across updates, the core crate (openlogi-core) embeds a configuration schema migration system that runs automatically on load, transforming legacy data structures into the current canonical format.

How OpenLogi Tracks Configuration Schema Versions

Each configuration file contains a schema_version field that marks its structural generation. The current build defines SCHEMA_VERSION = 7 in crates/openlogi-core/src/config.rs at line 94. When Config::load_from_path reads a file, it compares the stored version against this constant. If the file’s version is older, the loader invokes a pipeline of migration functions to bring the data up to date before the application uses it.

The Migration Pipeline: Three Private Helpers

The load routine calls three private migration helpers in sequence inside crates/openlogi-core/src/config.rs. Each function handles a specific historical transition:

migrate_owner_locked_gestures (v3 to v4)

Located at lines 387-436, this function upgrades legacy "owner-locked" gesture layouts from schema versions 3 and 4. The old model stored a single "gesture owner" and dormant gesture maps. The migration converts those maps into explicit per-button Binding::Gesture objects, demotes non-owner gestures to simple clicks, and injects a canonical gesture button if missing. This eliminates the owner-lock concept entirely, producing a self-contained set of bindings.

migrate_transport_scoped_keys (v5)

Defined at lines 458-522, this migration rewrites transport-scoped device keys introduced in v5. Keys following the pattern direct:<vid>:<pid>:<identity> (for example, direct:046d:c08d:unit:6be9d300) are parsed to extract the stable <identity> fragment. The device is then re-indexed under this new key, while a DeviceConfig::links entry records the discarded route. The function also consolidates duplicate entries—such as the same mouse reachable via USB and Bluetooth—and updates all references (selected_device, host_switch_targets) to point to the new stable key.

migrate_thumbwheel_native_direction (v7)

Found at lines 534-564, this function fixes pre-v7 thumb-wheel scroll defaults. Before v7, "up" mapped to HorizontalScrollRight and "down" to HorizontalScrollLeft. Version 7 swapped these defaults to match physical wheel polarity. The migration detects bindings still using the old defaults (or absent bindings implying the old defaults) and rewrites them to the new actions, leaving any custom user bindings untouched.

Automatic Migration Execution and Persistence

The migration functions are private and invoked only during the load sequence in crates/openlogi-core/src/config/file.rs. After the transformations complete, the configuration is saved back to disk with the updated schema_version, unless the config is marked as ephemeral. This ensures subsequent runs operate on the canonical format without re-processing legacy data.

use openlogi_core::config::{Config, ConfigFile};
use std::path::Path;

fn load_user_config(path: &Path) -> Result<Config, ConfigError> {
    // Loads the file, automatically runs migrations if needed
    Config::load_from_path(path)
}

While rarely necessary, you can manually force a migration loop if you need to re-normalize data:

// Manually forcing a migration (rarely needed – the load path does it):
let mut cfg = Config::load_from_path(&path)?;
cfg.migrate_transport_scoped_keys(); // rewrites historic direct keys
cfg.save(&path)?; // persists the migrated file

Unit Testing Configuration Migrations

The migration logic is exercised by dedicated unit tests within the inline test module in crates/openlogi-core/src/config.rs (around line 1000). Specific test cases include migrates_v1_button_and_gesture_bindings and migrates_pre_v7_thumbwheel_defaults_to_normalised_native_direction. These tests instantiate legacy TOML snippets, invoke Config::load_from_path, and assert that the resulting in-memory structures match the expected post-migration state, ensuring that upgrades remain correct across refactors.

Summary

  • OpenLogi uses a schema_version field and the SCHEMA_VERSION constant to detect outdated TOML configuration files.
  • Three private migration functions—migrate_owner_locked_gestures, migrate_transport_scoped_keys, and migrate_thumbwheel_native_direction—handle upgrades from v3, v5, and pre-v7 schemas respectively.
  • Migrations run automatically inside Config::load_from_path and persist the updated file unless the configuration is ephemeral.
  • Unit tests in config.rs validate each migration path using historical configuration snippets.

Frequently Asked Questions

What happens if my OpenLogi config file is from an older schema version?

OpenLogi detects the version mismatch during Config::load_from_path and automatically executes the required migration functions to upgrade your data to the current SCHEMA_VERSION = 7. Your old gestures, device keys, and wheel bindings are rewritten to the new format without data loss.

Does OpenLogi migration modify my original configuration file?

Yes, unless the configuration is marked as ephemeral. After migrating the in-memory structure, OpenLogi saves the updated TOML back to disk with the new schema_version, ensuring future loads skip the migration steps.

How does OpenLogi handle duplicate device entries during migration?

During the migrate_transport_scoped_keys step, OpenLogi detects when the same physical device appears under different transport routes (for example, USB and Bluetooth). It folds these duplicates into a single entry indexed by the stable identity fragment while preserving alternative routes in the DeviceConfig::links field.

Can I manually trigger a configuration migration in OpenLogi?

The migration methods are private and normally run automatically, but you can force the process by loading the configuration, calling the private methods through internal APIs if exposed, or simply loading and saving the file. The standard Config::load_from_path routine handles this transparently for most users.

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 →