OpenLogi Configuration Schema Versioning and Migration Paths Explained

OpenLogi manages configuration compatibility by storing a schema_version field in config.toml that is compared against the SCHEMA_VERSION constant in the binary; when versions mismatch, idempotent migration functions automatically upgrade older files while refusing to load newer ones.

OpenLogi (AprilNEA/OpenLogi) persists user preferences in a TOML configuration file. To ensure seamless upgrades across releases, the codebase implements a robust versioning system that detects stale schemas and transforms them at runtime. This article examines the implementation details, migration history, and code paths that handle configuration evolution from version 1 through version 7.

How Schema Versioning Works in OpenLogi

At the core of the system lies the schema_version field within the TOML file and the SCHEMA_VERSION constant defined in crates/openlogi-core/src/config.rs.

The current schema version is defined as a public constant:

/// The schema version the current build produces.
pub const SCHEMA_VERSION: u32 = 7;

When the application initializes, Config::load_from_path reads the persisted file and compares the embedded schema_version against this constant. If the stored version is older, the runtime executes a series of targeted migration methods. If the stored version exceeds the binary's capability, loading fails immediately to prevent data loss.

The Seven Schema Versions and Breaking Changes

OpenLogi has evolved through seven distinct schema iterations, each addressing specific architectural changes:

Version 1: Initial Format

The baseline format stored raw device configurations. No migration logic was required because this represented the starting state.

Version 2: Unified Binding Maps

Per-device button_bindings and gesture_bindings were merged into a single bindings map. Migration occurs through the RawDeviceConfig shim, which folds legacy fields into the new structure during deserialization. The file is rewritten with the new schema on the next save operation.

Version 3: Physical Device Identifiers

The device map key transitioned from model identifiers to physical-device identifiers. No automatic migration exists for this version because safely mapping model-based entries to physical devices is impossible when identical devices are present. Older entries are silently ignored.

Version 4: Owner-Locked Gesture Removal

The global owner-locked gesture mode was replaced with per-button ownership. The Config::migrate_owner_locked_gestures function handles this transition by rewriting the old gesture_owner field and demoting non-owner gestures to single-click bindings.

Version 5: Transport-Scoped to Identity Keys

Device keys changed from transport-scoped format (direct:<vid>:<pid>:…) to plain identity keys (unit:<id>), while preserving route information as metadata. Config::migrate_transport_scoped_keys parses legacy keys, creates new identity mappings, folds duplicate entries, and updates all references including selected_device and host_switch_targets.

Version 6: Threshold-Based Bindings

Added support for short and long press thresholds. No explicit migration code was required because the new fields utilize sensible defaults, allowing older configurations to load unchanged.

Version 7: Thumb-Wheel Native Direction (Current)

Thumb-wheel scroll defaults were normalized to match the device's physical direction. Config::migrate_thumbwheel_native_direction detects the legacy default pair (HorizontalScrollRight/Left) and swaps the actions to the new defaults (HorizontalScrollLeft/Right).

Migration Execution Flow

Migrations execute sequentially during the loading process in Config::load_from_path. The logic follows this pattern:

if self.schema_version != SCHEMA_VERSION {
    // Apply migrations for each older version in order
}

The system guarantees idempotency: each migration checks the current version before executing, ensuring a configuration file upgrades at most once per release. The typical flow proceeds through:

  1. migrate_owner_locked_gestures (for versions ≤ 3) – Demotes non-owner bindings and ensures default owner entries exist.
  2. migrate_transport_scoped_keys (for versions ≤ 4) – Renames keys and collapses duplicates.
  3. migrate_thumbwheel_native_direction (for versions ≤ 6) – Corrects scroll orientation defaults.

Key Implementation Files

Understanding the migration architecture requires examining these specific source files:

Practical Code Examples

Creating a Fresh Configuration

New configurations automatically use the current schema version:

use openlogi_core::config::Config;

let cfg = Config::default();
// cfg.schema_version == SCHEMA_VERSION (7)

Loading and Automatically Migrating

The standard entry point handles version detection and migration transparently:

use openlogi_core::config::ConfigFile;
use std::path::PathBuf;

let path = dirs::config_dir()
    .unwrap()
    .join("openlogi")
    .join("config.toml");

// Loads file, checks version, runs migrations if needed
let cfg = ConfigFile::load(&path)?;
// cfg.schema_version is now 7

Manual Migration for Testing

For testing specific upgrade paths, you can simulate old versions and trigger migrations individually:

use openlogi_core::config::Config;

let mut cfg = Config::default();
cfg.schema_version = 4;

cfg.migrate_owner_locked_gestures();
cfg.migrate_transport_scoped_keys();
// Continue with additional migrations as needed

Summary

  • OpenLogi stores a schema_version field in config.toml alongside the SCHEMA_VERSION constant in the binary.
  • Seven schema versions exist, with versions 3, 4, 5, and 7 requiring explicit migration logic.
  • Migrations run automatically during Config::load_from_path when the stored version differs from the binary constant.
  • Each migration function is idempotent and executes only when the file version is below the target threshold.
  • Configuration files with versions newer than the binary are rejected to prevent data loss.
  • The core logic resides in crates/openlogi-core/src/config.rs with comprehensive tests in config/tests.rs.

Frequently Asked Questions

What happens if I open a newer configuration file with an older OpenLogi binary?

Loading fails with an error. The code explicitly checks if the stored version exceeds SCHEMA_VERSION and returns an error rather than attempting to downgrade or ignore unknown fields. This prevents silent data loss or corruption of newer configuration features.

Why doesn't version 3 have an automatic migration?

Version 3 changed device identification from model-based keys to physical-device identifiers. Because multiple identical devices could exist simultaneously, the code cannot safely determine which physical instance corresponds to a legacy model-based entry. Therefore, crates/openlogi-core/src/config.rs intentionally omits automatic migration for this version, ignoring older entries rather than risking incorrect device mappings.

Are the migration functions safe to run multiple times?

Yes, all migration functions are idempotent. Each function checks the current schema_version before executing transformations. For example, migrate_thumbwheel_native_direction only executes when the version is less than or equal to 6, and migrate_transport_scoped_keys only processes versions ≤ 4. This ensures that rerunning migrations on already-updated files has no effect.

How does version 2 migration work without a dedicated function?

Version 2 merged separate binding maps using Rust's deserialization shims. Rather than an explicit migration function, the RawDeviceConfig struct handles the transformation during TOML parsing, folding button_bindings and gesture_bindings into the unified bindings map. The file is automatically rewritten with the new schema on the next save operation.

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 →