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

291 lines
9.4 KiB
Markdown

# 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<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=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<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.
```rust
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:
```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<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
```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