Initial import
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user