# Phase 02 — Config Parser, Schema & Validator ## Goal Implement the complete non-UI config stack: parser, typed schema, and validation engine. This phase has NO GTK4 code. All modules are pure Rust with full unit tests. ## Files to implement (fully, not stubs) - `src/config/types.rs` - `src/config/parser.rs` - `src/config/schema.rs` - `src/config/validator.rs` - `src/config/mod.rs` --- ## src/config/types.rs Define all shared types used across the config subsystem. ```rust use std::path::PathBuf; /// A single line from a MangoHud config file, preserving its original text. pub enum ConfigLine { Comment(String), // lines starting with # Blank, // empty lines Option { key: String, value: Option, raw: String }, CommentedOption { key: String, value: Option, raw: String }, } /// The current state of an option in the in-memory config. #[derive(Debug, Clone, PartialEq)] pub enum ConfigValue { Absent, // not present in file; use MangoHud compiled default Flag, // bare key with no value (presence = enabled) Value(String), // key=value Disabled, // was commented out explicitly } /// Type system for schema entries. #[derive(Debug, Clone)] pub enum OptionType { Flag, Bool, Int { min: i64, max: i64 }, Float { min: f64, max: f64 }, Str { max_len: usize }, Color, Enum { variants: Vec }, FpsLimitList, KeyBind, CommaSepInts, CommaSepFloats, CommaSepStrings { valid_values: Option> }, Path { must_exist: bool, must_be_writable: bool }, } #[derive(Debug, Clone, PartialEq)] pub enum GpuVendor { Any, AmdOnly, NvidiaOnly, IntelOnly } #[derive(Debug, Clone, PartialEq)] pub enum Category { Performance, DisplayFps, DisplayGpu, DisplayCpu, DisplayMemory, DisplayIoNetwork, DisplayMisc, DisplayGraphs, DisplayBattery, DisplayMediaPlayer, DisplayGamescope, DisplaySteamDeck, DisplayTimeText, AppearanceLayout, AppearanceColors, AppearanceTypography, BehaviorKeybindings, BehaviorFpsLimits, BehaviorLogging, BehaviorMisc, WorkaroundsOpengl, AdvancedFcat, AdvancedFtrace, } /// A single schema entry — defines everything about one MangoHud option. #[derive(Debug, Clone)] pub struct SchemaEntry { pub key: &'static str, pub option_type: OptionType, pub description: &'static str, pub category: Category, pub dependencies: &'static [&'static str], pub conflicts_with: &'static [&'static str], pub gpu_vendor_only: GpuVendor, pub gamescope_only: bool, } /// Validation result for a single option. #[derive(Debug, Clone, PartialEq)] pub enum ValidationResult { Ok, Warning(String), // save is allowed but issue shown Error(String), // save is BLOCKED } /// The full in-memory representation of a parsed config file. pub struct AnnotatedConfig { /// Ordered list of lines as they appear in the file. /// Used for writing back to disk (preserves comments/order). pub lines: Vec, /// Fast lookup map: key → (line_index, current_value). pub options: indexmap::IndexMap, /// Source path, if backed by a file. pub path: Option, /// Whether this config has unsaved in-memory changes. pub dirty: bool, } ``` --- ## src/config/parser.rs Rules to implement (from docs/architecture.md → Config File Format Notes): 1. Lines starting with `#` → `ConfigLine::Comment` (preserve verbatim) 2. Empty/whitespace-only lines → `ConfigLine::Blank` 3. `key=value` → `ConfigLine::Option { key, value: Some(value) }` 4. `key` alone → `ConfigLine::Option { key, value: None }` (flag) 5. `# key` → `ConfigLine::CommentedOption { key, value: None }` 6. `# key=value` → `ConfigLine::CommentedOption { key, value: Some(value) }` 7. On duplicate keys: last occurrence wins (update options map accordingly) 8. All parsing: UTF-8, trim trailing whitespace from values ### Public API ```rust impl Parser { /// Parse a config file from disk. pub fn read(path: &Path) -> anyhow::Result /// Parse config from a string (for env var inline and tests). pub fn parse_str(content: &str, path: Option) -> AnnotatedConfig /// Write an AnnotatedConfig back to disk. /// - Creates backup at {path}.mangotune.bak /// - Writes to {path}.mangotune.tmp /// - Atomically renames to {path} /// - Returns Err if any step fails (restores from backup on failure) pub fn write(config: &AnnotatedConfig) -> anyhow::Result<()> /// Update a specific key's value in the config lines. /// If key exists: update that line in-place. /// If key doesn't exist: append to end of file. /// If setting to Absent/Disabled: comment out the line. pub fn set_value(config: &mut AnnotatedConfig, key: &str, value: ConfigValue) /// Serialize config to a string (for preview or clipboard copy). pub fn to_string(config: &AnnotatedConfig) -> String } ``` ### Tests required (in src/config/parser.rs at bottom, `#[cfg(test)]`) - Parse a file with all line types (comment, blank, key=value, bare key, commented option) - Round-trip: parse → write → parse → values must match - set_value: update existing key preserves surrounding lines - set_value: add new key appends at end - set_value: disable key comments it out - Duplicate key: last value wins - UTF-8 edge cases: file with non-ASCII comments --- ## src/config/schema.rs Implement `MANGOHUD_SCHEMA: &[SchemaEntry]` — a static slice containing one entry per option documented in `docs/mangohud_schema.md`. Every single option in that document must have a corresponding entry here. Count: approximately 120 entries. ```rust use once_cell::sync::Lazy; pub static MANGOHUD_SCHEMA: Lazy> = Lazy::new(|| vec![ SchemaEntry { key: "fps", option_type: OptionType::Flag, description: "Show FPS counter (enabled by default)", category: Category::DisplayFps, dependencies: &[], conflicts_with: &["fps_only"], gpu_vendor_only: GpuVendor::Any, gamescope_only: false, }, // ... all ~120 entries ]); /// Fast lookup by key. pub fn get_schema_entry(key: &str) -> Option<&'static SchemaEntry> ``` Also implement: ```rust /// Return all schema entries for a given category. pub fn entries_for_category(category: &Category) -> Vec<&'static SchemaEntry> /// Return all dependency keys for a given key (recursive). pub fn all_dependencies(key: &str) -> Vec<&'static str> ``` --- ## src/config/validator.rs ```rust /// Validate a single value against its schema entry. /// Returns Ok, Warning, or Error. pub fn validate_value( key: &str, value: &ConfigValue, schema: &SchemaEntry, ) -> ValidationResult /// Validate an entire config against the full schema. /// Returns a map of key → ValidationResult for every key that has an issue. /// Keys with ValidationResult::Ok are NOT included (only problems). pub fn validate_all( config: &AnnotatedConfig, ) -> HashMap /// Check dependency satisfaction. /// Returns list of (dependent_key, missing_required_key) pairs. pub fn check_dependencies(config: &AnnotatedConfig) -> Vec<(String, String)> /// Check for mutual exclusions. /// Returns list of (key_a, key_b) pairs where both are active but they conflict. pub fn check_conflicts(config: &AnnotatedConfig) -> Vec<(String, String)> /// True if config is fully valid (no Errors). Warnings are allowed. pub fn is_saveable(config: &AnnotatedConfig) -> bool ``` ### Validation logic per type (implement all): - `Flag`: always valid if present; no value to validate - `Bool`: value must be "0" or "1" - `Int { min, max }`: must parse as i64, must be in [min, max] - `Float { min, max }`: must parse as f64, must be in [min, max] - `Str { max_len }`: must be ≤ max_len bytes - `Color`: must match regex `^[0-9A-Fa-f]{6}$` - `Enum { variants }`: value must be in variants list (case-sensitive) - `FpsLimitList`: comma-separated, each part must be non-negative integer - `KeyBind`: must match the keybind regex from mangohud_schema.md - `CommaSepInts`: each comma-part must parse as integer - `CommaSepFloats`: each comma-part must parse as f64 - `CommaSepStrings { valid_values: Some(_) }`: each part must be in valid set - `CommaSepStrings { valid_values: None }`: any non-empty string OK - `Path { must_exist, must_be_writable }`: validate with std::fs ### Tests required - Each validation type: valid input, invalid input, boundary values - Dependency check: config with missing dependency detected - Conflict check: fps_only + fps = conflict detected - is_saveable: returns false if any Error present, true if only Warnings --- ## src/config/mod.rs ```rust pub mod types; pub mod parser; pub mod schema; pub mod validator; pub mod resolver; // stub only in this phase pub use types::*; ``` --- ## Acceptance Criteria - [ ] `cargo test --lib` passes all tests with 0 failures - [ ] Schema contains entries for ALL ~120 options from docs/mangohud_schema.md - [ ] Parser round-trips without losing comments or blank lines - [ ] Validator blocks saves on invalid Color, out-of-range Int, unknown Enum value - [ ] Dependency checker catches gpu_mem_clock without vram - [ ] Conflict checker catches fps_only with any other display param - [ ] No `.unwrap()` in production code paths - [ ] No `todo!()` remaining in any of the 4 implemented files