Spaces:
Runtime error
Runtime error
A newer version of the Gradio SDK is available: 6.14.0
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.tomlschema, 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 β
tracinginitialised (config loader usestracing::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.tomlcreated at repo root with all fields from the schema below and sensible defaults -
kansas/src/config.rsβAppConfigstruct,load(),watch()functions - On startup,
AppConfig::load()readsconfig.toml; missing file uses defaults (does not crash) -
notifywatcher fires whenconfig.tomlis saved; updated config logged at INFO level -
AppConfigstored astauri::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