config-easy 0.2.1

A small interactive settings menu for command-line Rust applications
Documentation

config-easy

Crates.io Documentation License Rust Edition

config-easy is a small Rust crate for displaying and editing key/value settings in command-line applications.

The core crate is storage-agnostic. It can be used with any settings backend by implementing SettingsStore. A built-in rusqlite adapter is available behind the optional rusqlite feature.

Applications remain responsible for schema creation, default values, typed configuration structs, CLI parsing, and app-specific behavior.

Install

For custom storage or to avoid database dependencies:

[dependencies]
config-easy = "0.2"

For the built-in SQLite adapter:

[dependencies]
config-easy = { version = "0.2", features = ["rusqlite"] }
rusqlite = "0.40"

Default features are empty, so config-easy does not pull in rusqlite unless requested.

SQLite Usage

The SQLite adapter expects a table with key/value text columns. The default schema is:

CREATE TABLE IF NOT EXISTS settings (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

Basic usage:

let connection = rusqlite::Connection::open("app.db")?;

config_easy::sqlite::run(&connection)?;

This uses:

  • table: settings
  • key column: key
  • value column: value
  • order by: key

Builder usage:

config_easy::sqlite::builder(&connection)
    .table("settings")
    .key_column("key")
    .value_column("value")
    .order_by("key")
    .run()?;

Table and column names are validated as simple SQL identifiers before being interpolated into SQL. Values are always bound as SQL parameters.

Valid identifiers are non-empty, start with an ASCII letter or _, and contain only ASCII letters, numbers, and _.

Secret Values

config_easy::sqlite::builder(&connection)
    .secret_keys(["my_api_key"])
    .run()?;

Non-empty secret values are displayed as ********. Empty secret values are displayed as empty. The underlying value is not changed unless the user selects that setting and enters a new value.

Validation

config_easy::sqlite::builder(&connection)
    .validator(|key, value| {
        if key == "log_level" && !["debug", "info", "warn", "error"].contains(&value) {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                "expected one of debug, info, warn, error",
            )
            .into());
        }

        Ok(())
    })
    .run()?;

Validation runs before a setting is updated.

Input values are trimmed before validation and saving.

Custom Actions

Custom actions are closures with no arguments. Capture app state directly when needed:

config_easy::sqlite::builder(&connection)
    .action("r", "reset delta link", || {
        connection.execute("DELETE FROM sync_state WHERE key = 'delta_link'", [])?;
        println!("Delta link reset.");
        Ok(())
    })
    .run()?;

Custom actions are displayed in the prompt and run when their key is selected. Action keys cannot be empty, numeric, duplicated, q, or quit.

Avoiding rusqlite Conflicts

If your application already uses a different rusqlite version, do not enable the rusqlite feature. Implement SettingsStore in your application and use the generic builder:

use config_easy::{SettingRow, SettingsQuery, SettingsStore};

struct AppSettingsStore<'a> {
    conn: &'a rusqlite::Connection,
}

impl SettingsStore for AppSettingsStore<'_> {
    fn load_settings(
        &self,
        query: &SettingsQuery<'_>,
    ) -> Result<Vec<SettingRow>, Box<dyn std::error::Error + Send + Sync>> {
        let sql = format!(
            "SELECT {}, {} FROM {} ORDER BY {}",
            query.key_column, query.value_column, query.table, query.order_by
        );

        let mut statement = self.conn.prepare(&sql)?;
        let rows = statement.query_map([], |row| {
            Ok(SettingRow {
                key: row.get(0)?,
                value: row.get(1)?,
            })
        })?;

        Ok(rows.collect::<Result<Vec<_>, _>>()?)
    }

    fn update_setting(
        &self,
        query: &SettingsQuery<'_>,
        key: &str,
        value: &str,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let sql = format!(
            "UPDATE {} SET {} = ?1 WHERE {} = ?2",
            query.table, query.value_column, query.key_column
        );
        self.conn.execute(&sql, rusqlite::params![value, key])?;
        Ok(())
    }
}

config_easy::builder(AppSettingsStore { conn: &connection }).run()?;

When writing your own SQL-backed store, validate any table or column names before interpolating them into SQL. Bind setting values as parameters.

Examples

  • examples/custom_store.rs shows the generic SettingsStore API without any database dependency.
  • examples/sqlite_basic.rs shows the built-in SQLite adapter with the default table layout.
  • examples/sqlite_builder.rs shows custom table names, secret keys, validation, and custom actions with SQLite.

Run examples with:

cargo run --example custom_store
cargo run --example sqlite_basic --features rusqlite
cargo run --example sqlite_builder --features rusqlite

The example binaries are compile-focused and do not open the interactive menu.

Migrating from 0.1 to 0.2

0.2 moves SQLite support behind an optional feature and makes the core menu storage-agnostic.

Update dependencies:

config-easy = { version = "0.2", features = ["rusqlite"] }
rusqlite = "0.40"

Update function paths:

// 0.1
config_easy::run(&connection)?;
config_easy::builder(&connection).run()?;

// 0.2
config_easy::sqlite::run(&connection)?;
config_easy::sqlite::builder(&connection).run()?;

Update custom actions:

// 0.1
config_easy::builder(&connection)
    .action("r", "reset", |conn| {
        conn.execute("DELETE FROM sync_state", [])?;
        Ok(())
    });

// 0.2
config_easy::sqlite::builder(&connection)
    .action("r", "reset", || {
        connection.execute("DELETE FROM sync_state", [])?;
        Ok(())
    });

Deliberately Not Included

  • Creating the settings table
  • Seeding default settings
  • Parsing values into app-specific types
  • CLI argument parsing
  • Encryption or secure secret storage
  • App-specific maintenance logic