291 lines
9.4 KiB
Markdown
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
|