Ashiedu's picture
Sync unified workbench
0490201 verified

A newer version of the Gradio SDK is available: 6.14.0

Upgrade

T-012: TOML Config System β€” Hot-Reload via notify, Typed Config Structs

Type: Task | Phase: 0 | Autonomy: agent:autonomous | Stack: stack:rust
Version: v0.1 | Iteration: iter-1 | Effort: S (half-day)

⚠️ Scope: Create config.toml schema, typed Rust structs, and a hot-reload watcher. Config is read once at startup and re-read when the file changes. No IPC command needed β€” config changes log a message and update shared state. Camera config (camera-config.toml) and controller map (controller-map.toml) have their own files managed by T-031 and T-023; this task owns the application config only.


Prerequisites

  • T-001 merged β€” workspace exists
  • T-011 merged β€” tracing initialised (config loader uses tracing::info!)

New Workspace Dependencies

# Add to [workspace.dependencies]
notify = { version = "*", features = ["macos_kqueue"] }
toml   = { version = "*" }

# Add to kansas/Cargo.toml [dependencies]
notify = { workspace = true }
toml   = { workspace = true }

Acceptance Criteria

  • config.toml created at repo root with all fields from the schema below and sensible defaults
  • kansas/src/config.rs β€” AppConfig struct, load(), watch() functions
  • On startup, AppConfig::load() reads config.toml; missing file uses defaults (does not crash)
  • notify watcher fires when config.toml is saved; updated config logged at INFO level
  • AppConfig stored as tauri::State<Arc<Mutex<AppConfig>>> β€” readable from any command handler
  • cargo check --workspace β€” zero errors; cargo clippy -- -D warnings β€” zero warnings

config.toml Schema

[audio]
sample_rate    = 48000
buffer_frames  = 512
enable_input   = true
wasapi_exclusive = false   # true = lower latency, no other apps share device

[transport]
default_bpm    = 128.0
default_time_sig_num  = 4
default_time_sig_den  = 4

[models]
models_dir     = "models"   # relative to workspace root
directml_device_id = 0      # GPU index for DirectML

[rerun]
enabled        = false      # set true to enable telemetry sink (T-121)
address        = "127.0.0.1:9876"

[ui]
theme          = "dark"
window_width   = 1280
window_height  = 800

kansas/src/config.rs

use std::{
    path::Path,
    sync::{Arc, Mutex},
    time::Duration,
};
use notify::{Event, RecursiveMode, Watcher};
use serde::Deserialize;

const CONFIG_PATH: &str = "config.toml";

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct AppConfig {
    pub audio:     AudioConfig,
    pub transport: TransportConfig,
    pub models:    ModelsConfig,
    pub rerun:     RerunConfig,
    pub ui:        UiConfig,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct AudioConfig {
    pub sample_rate: u32,
    pub buffer_frames: u32,
    pub enable_input: bool,
    pub wasapi_exclusive: bool,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct TransportConfig {
    pub default_bpm: f32,
    pub default_time_sig_num: u8,
    pub default_time_sig_den: u8,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct ModelsConfig {
    pub models_dir: String,
    pub directml_device_id: u32,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct RerunConfig {
    pub enabled: bool,
    pub address: String,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct UiConfig {
    pub theme: String,
    pub window_width: u32,
    pub window_height: u32,
}

impl Default for AppConfig {
    fn default() -> Self {
        toml::from_str("").unwrap_or_else(|_| Self {
            audio:     AudioConfig::default(),
            transport: TransportConfig::default(),
            models:    ModelsConfig::default(),
            rerun:     RerunConfig::default(),
            ui:        UiConfig::default(),
        })
    }
}

impl Default for AudioConfig {
    fn default() -> Self { Self { sample_rate: 48_000, buffer_frames: 512, enable_input: true, wasapi_exclusive: false } }
}
impl Default for TransportConfig {
    fn default() -> Self { Self { default_bpm: 128.0, default_time_sig_num: 4, default_time_sig_den: 4 } }
}
impl Default for ModelsConfig {
    fn default() -> Self { Self { models_dir: "models".into(), directml_device_id: 0 } }
}
impl Default for RerunConfig {
    fn default() -> Self { Self { enabled: false, address: "127.0.0.1:9876".into() } }
}
impl Default for UiConfig {
    fn default() -> Self { Self { theme: "dark".into(), window_width: 1280, window_height: 800 } }
}

pub fn load() -> AppConfig {
    match std::fs::read_to_string(CONFIG_PATH) {
        Ok(text) => toml::from_str(&text).unwrap_or_else(|e| {
            tracing::warn!("[config] Parse error, using defaults: {e}");
            AppConfig::default()
        }),
        Err(_) => {
            tracing::info!("[config] config.toml not found β€” using defaults");
            AppConfig::default()
        }
    }
}

/// Start a background watcher that reloads config on file change.
pub fn watch(config: Arc<Mutex<AppConfig>>) {
    std::thread::Builder::new()
        .name("synesthesia-config-watcher".into())
        .spawn(move || {
            let (tx, rx) = std::sync::mpsc::channel();
            let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
                if let Ok(event) = res { let _ = tx.send(event); }
            }).expect("config watcher init failed");

            watcher.watch(Path::new(CONFIG_PATH), RecursiveMode::NonRecursive).ok();
            tracing::info!("[config] Watching {CONFIG_PATH} for changes");

            for _event in rx {
                let updated = load();
                tracing::info!("[config] Reloaded β€” bpm={}", updated.transport.default_bpm);
                if let Ok(mut cfg) = config.lock() { *cfg = updated; }
            }
        })
        .ok();
}

Register in main.rs:

use std::sync::{Arc, Mutex};
mod config;

fn main() {
    logging::setup();
    let cfg = Arc::new(Mutex::new(config::load()));
    config::watch(Arc::clone(&cfg));

    tauri::Builder::default()
        .manage(cfg)
        // ...

GitHub CLI

gh issue create \
  --title "T-012: TOML config system β€” hot-reload via notify, typed config structs" \
  --label "type:task,stack:rust,agent:autonomous,priority:high,status:ready,day:1" \
  --body-file T-012.md

Parent: GENESIS | Blocks: T-031 (camera config), T-023 (controller-map) | Blocked By: T-001, T-011