# How to Migrate TOML Configuration Files in OpenLogi

> Easily migrate TOML configuration files in OpenLogi. Learn how OpenLogi automatically upgrades your TOML files to the latest schema, ensuring a smooth transition.

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

---

**OpenLogi automatically upgrades legacy TOML configuration files to the current schema version (currently 7) when they are loaded, creating a backup of the original before applying transformations.**

OpenLogi stores user preferences and per-device state in a TOML file located at `~/.config/openlogi/config.toml`. As the project evolves, the configuration schema changes, but the built-in migration pipeline ensures backward compatibility by transforming older files into the latest format without manual intervention.

## Understanding the Configuration Schema Versioning

The migration system centers around a schema version constant defined in the core crate. In [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs), the constant `SCHEMA_VERSION` (currently set to `7`) represents the latest supported configuration format. When `Config::load_from_path` reads a TOML file, it compares the file's stored schema version against this constant to determine which migrations to apply.

The loading process begins with `openlogi_core::config::ConfigFile::load`, which parses the raw TOML and returns both the configuration structure and the original schema version. This occurs in [`crates/openlogi-core/src/config/file.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/file.rs) at line 107. If the loaded version is older than `SCHEMA_VERSION`, the system triggers an automatic upgrade sequence.

## Built-In Migration Functions

OpenLogi's migration logic applies a series of targeted transformations to bring older configurations up to date. These migrations handle breaking changes introduced across different versions.

### migrate_owner_locked_gestures (v4 to later)

The `migrate_owner_locked_gestures` function (found in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) at line 387) removes the historic "gesture-owner lock" mechanism introduced in schema version 4. It rewrites gesture configurations from the legacy locked format to the modern per-button format, ensuring that gesture mappings remain functional after the upgrade.

### migrate_transport_scoped_keys (v5 to later)

Introduced in version 5, transport-scoped keys prefixed with `direct:` required special handling. The `migrate_transport_scoped_keys` function (line 458 in [`config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/config.rs)) strips these prefixes from device keys and folds them into the device's `links` map while preserving the identifier fragments. This consolidation simplifies the device linking structure while maintaining backward compatibility with older configuration files.

### migrate_thumbwheel_native_direction (v7)

For the version 7 schema, `migrate_thumbwheel_native_direction` (line 534 in [`config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/config.rs)) normalizes thumb-wheel scroll defaults. It converts legacy explicit-direction defaults to the canonical native direction representation, ensuring consistent scrolling behavior across different hardware generations.

### Device-Level Migrations

Beyond global migrations, `DeviceConfig` in [`crates/openlogi-core/src/config/device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/device.rs) (see line 84) handles device-specific transformations. These include merging older `button_bindings` and `gesture_bindings` maps from schema version 2, and managing the legacy `RawDeviceConfig` shim from version 1. These changes ensure that per-device settings migrate correctly even when the global schema remains unchanged.

## Backup and Safety Mechanisms

Before applying any transformations, OpenLogi preserves the original configuration to prevent data loss. When `ConfigFile` detects that migrations are necessary, it writes the pre-migration content to `config.toml.bak` in the same directory before overwriting the original file with the migrated version. This backup logic is implemented in [`crates/openlogi-core/src/config/file.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/file.rs) at line 175.

The migration process is atomic in the sense that the file is only overwritten after the backup completes. If the migration fails or produces unexpected results, users can restore their previous configuration by manually renaming the `.bak` file.

## Programmatic Migration Examples

You can trigger migrations programmatically using the OpenLogi core library. The following examples demonstrate how to load and migrate configurations in Rust applications.

### Loading and Migrating via the Config API

```rust
use std::path::Path;
use openlogi_core::config::Config;

/// Load (and automatically migrate) the user configuration.
fn load_config() -> anyhow::Result<Config> {
    // Resolve the platform-standard config directory via `openlogi_core::paths`.
    let cfg_path = openlogi_core::paths::config_dir()?.join("config.toml");
    
    // `load_from_path` parses the file, runs all migrations, and writes back a
    // migrated copy (with a .bak backup of the original).
    let cfg = Config::load_from_path(&cfg_path)?;
    println!("Config schema version after load: {}", cfg.schema_version);
    Ok(cfg)
}

```

### Dry-Run Migration Testing

For testing or inspection purposes, you can load and migrate a configuration without writing changes to disk:

```rust
use std::path::Path;
use openlogi_core::config::ConfigFile;

fn dry_run_migration(path: &Path) -> anyhow::Result<()> {
    // Load the file but keep it in memory; no backup or overwrite is performed.
    let (cfg, original_version) = ConfigFile::load(path, /*dry_run=*/true)?;
    println!("Original version: {}", original_version);
    println!("Migrated version: {}", cfg.schema_version);
    Ok(())
}

```

## Key Source Files for Migration Logic

Understanding the migration architecture requires familiarity with these core files:

- **[`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs)** — Defines the `Config` struct, `SCHEMA_VERSION` constant, and the primary migration functions (`migrate_owner_locked_gestures`, `migrate_transport_scoped_keys`, `migrate_thumbwheel_native_direction`).
- **[`crates/openlogi-core/src/config/file.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/file.rs)** — Handles TOML serialization and deserialization, backup creation, and orchestrates the migration sequence via `ConfigFile::load`.
- **[`crates/openlogi-core/src/config/device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/device.rs)** — Contains `DeviceConfig` and legacy `RawDeviceConfig` handling for device-specific migration logic.
- **[`crates/openlogi-core/src/config/settings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/settings.rs)** — Stores per-device and global settings (lighting, SmartShift, etc.) that may be affected by schema changes.

## Summary

- **Automatic upgrades**: OpenLogi detects schema version mismatches automatically when loading `~/.config/openlogi/config.toml` and applies necessary migrations.
- **Safety first**: The system creates a `.bak` backup of the original file before writing migrated content, preventing data loss.
- **Current schema**: Version 7 is the latest schema, supporting modern thumb-wheel direction handling and consolidated transport keys.
- **API access**: Developers can trigger migrations programmatically using `Config::load_from_path` or test migrations in-memory using `ConfigFile::load` with `dry_run=true`.

## Frequently Asked Questions

### When does OpenLogi migrate my configuration files?

Migrations run automatically whenever the configuration is loaded, which occurs during GUI startup (`openlogi-desktop`), CLI commands (such as `openlogi-cli list`), or programmatic calls to `Config::load_from_path`. The migration happens transparently before the application presents any interface or returns the configuration object.

### Can I prevent OpenLogi from modifying my original TOML file?

Yes, you can perform a dry-run migration using `ConfigFile::load(path, true)`, which parses and migrates the configuration in memory without creating a backup or overwriting the original file. However, normal application usage always writes the migrated version back to disk to ensure consistency between the file and the running system.

### What happens if a migration fails midway?

OpenLogi writes the backup file (`config.toml.bak`) before beginning any modifications to the original. If the migration process fails, the original file remains unchanged (or can be restored from the backup). The application will typically return an error rather than leaving the configuration in a partially migrated state, as the transformations are applied in memory before the final write operation.

### How do I migrate configurations from schema version 1 or 2?

Legacy configurations from versions 1 and 2 are fully supported through the device-level migration logic in [`device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/device.rs). The system automatically merges old `button_bindings` and `gesture_bindings` maps into the current format and handles the `RawDeviceConfig` shim transparently. Simply place the legacy file in the expected location, and OpenLogi will upgrade it to schema version 7 on the next startup.