9.4 KiB
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.rssrc/config/parser.rssrc/config/schema.rssrc/config/validator.rssrc/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):
- Lines starting with
#→ConfigLine::Comment(preserve verbatim) - Empty/whitespace-only lines →
ConfigLine::Blank key=value→ConfigLine::Option { key, value: Some(value) }keyalone →ConfigLine::Option { key, value: None }(flag)# key→ConfigLine::CommentedOption { key, value: None }# key=value→ConfigLine::CommentedOption { key, value: Some(value) }- On duplicate keys: last occurrence wins (update options map accordingly)
- 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 validateBool: 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 bytesColor: 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 integerKeyBind: must match the keybind regex from mangohud_schema.mdCommaSepInts: each comma-part must parse as integerCommaSepFloats: each comma-part must parse as f64CommaSepStrings { valid_values: Some(_) }: each part must be in valid setCommaSepStrings { valid_values: None }: any non-empty string OKPath { 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 --libpasses 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