Files
mangotune/docs/plan/phase_02.md
T
2026-03-30 23:06:06 -04:00

9.4 KiB

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.

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<String>, raw: String },
    CommentedOption { key: String, value: Option<String>, 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<String> },
    FpsLimitList,
    KeyBind,
    CommaSepInts,
    CommaSepFloats,
    CommaSepStrings { valid_values: Option<Vec<String>> },
    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<ConfigLine>,
    /// Fast lookup map: key → (line_index, current_value).
    pub options: indexmap::IndexMap<String, (usize, ConfigValue)>,
    /// Source path, if backed by a file.
    pub path: Option<PathBuf>,
    /// 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=valueConfigLine::Option { key, value: Some(value) }
  4. key alone → ConfigLine::Option { key, value: None } (flag)
  5. # keyConfigLine::CommentedOption { key, value: None }
  6. # key=valueConfigLine::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

impl Parser {
    /// Parse a config file from disk.
    pub fn read(path: &Path) -> anyhow::Result<AnnotatedConfig>

    /// Parse config from a string (for env var inline and tests).
    pub fn parse_str(content: &str, path: Option<PathBuf>) -> 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.

use once_cell::sync::Lazy;
pub static MANGOHUD_SCHEMA: Lazy<Vec<SchemaEntry>> = 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:

/// 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

/// 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<String, ValidationResult>

/// 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

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