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

> OpenLogi handles config schema migrations with atomic, load-time rewrites for v1-v3 gesture layouts and physical keys, ensuring data integrity with versioned backups before modification.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: migration-guide
- Published: 2026-09-02

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
// 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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.