# Module Specs --- # Module: config/parser.rs **Purpose:** Read and write MangoHud .conf files preserving all comments and whitespace. **Core constraint:** A config written by MangoTune must be byte-for-byte identical to the original file except for the lines that were explicitly changed. All comments, blank lines, section headers, and ordering must survive a read-write-read cycle unchanged. ## Parsing State Machine ``` for each line in file: if line.trim().is_empty() → ConfigLine::Blank if line.starts_with('#'): strip leading '# ' or '#' try parse as "key=value" or "key" if valid MangoHud key format: → ConfigLine::CommentedOption { key, value } else: → ConfigLine::Comment(original_line) else: split on first '=' if present → ConfigLine::Option { key, value, raw: original_line } ``` ## Write Strategy When serializing back to disk: - For each ConfigLine in order: write it back - For changed options: find the line by key and update ONLY that line's value portion - For new options (key didn't exist): append at end of file - For disabled options: prepend "# " to the line ## Key sanitization - Keys are trimmed of whitespace - Keys are case-sensitive (MangoHud is case-sensitive) - Keys must match `^[a-zA-Z_][a-zA-Z0-9_]*$` to be recognized as options --- # Module: config/validator.rs **Purpose:** Stateless validation engine. Every function is pure (no side effects). ## Validation Priority When multiple validation rules apply, return the most severe result (Error > Warning > Ok). ## Regex patterns (compile once with once_cell::Lazy) ```rust static COLOR_RE: Lazy = Lazy::new(|| Regex::new(r"^[0-9A-Fa-f]{6}$").unwrap()); static PCI_DEV_RE: Lazy = Lazy::new(|| { Regex::new(r"^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$").unwrap() }); static KEYBIND_RE: Lazy = Lazy::new(|| { Regex::new(r"^((Shift|Control|Alt|Super)_[LR]\+)*(F[1-9]|F1[0-2]|[A-Z])$").unwrap() }); static FTRACE_RE: Lazy = Lazy::new(|| { Regex::new(r"^(histogram|linegraph|label)/[a-zA-Z0-9_]+(/[a-zA-Z0-9_]+)?(\+(histogram|linegraph|label)/[a-zA-Z0-9_]+(/[a-zA-Z0-9_]+)?)*$").unwrap() }); ``` ## Special cases `fps_metrics` validation: - Split by comma - Each element must be: "AVG" (case-insensitive) OR a decimal between 0.0 and 100.0 `font_glyph_ranges` validation: - Valid values: `["korean", "chinese", "chinese_simplified", "japanese", "cyrillic", "thai", "vietnamese", "latin_ext_a", "latin_ext_b"]` `graphs` validation: - Valid values: `["gpu_load", "cpu_load", "gpu_core_clock", "gpu_mem_clock", "vram", "ram", "cpu_temp", "gpu_temp"]` `time_format` validation: - Must be a valid strftime format string - Validate by attempting to format a known date with the string using the `time` crate (add `time = "0.3"` to dev-dependencies if not already present, or use chrono) - If format produces empty string or contains '?': ValidationResult::Warning --- # Module: system/detect.rs **Purpose:** One-shot async system probe run at startup. Results are immutable after detection. ## MangoHud version parsing `mangohud --version` outputs something like: - `MangoHud 0.7.2` - `v0.7.1-3-gabcdef` Parse with: `^(?:MangoHud\s+)?v?(\d+\.\d+[\.\d]*)` to extract version string. ## GPU vendor detection (primary method: /sys/class/drm) ``` /sys/class/drm/card0/device/vendor → e.g. "0x1002\n" 0x1002 → AMD 0x10de → NVIDIA 0x8086 → Intel ``` If multiple GPUs found, use the first discrete GPU (non-Intel if Intel also present). Store all detected GPUs in a `Vec` so the UI can show a GPU selector. ## Fallback GPU detection (if /sys fails) Parse `lspci -nn 2>/dev/null` output — look for lines containing: - "VGA compatible controller" - "3D controller" - "Display controller" Extract vendor from PCI ID in brackets: `[10de:xxxx]` → NVIDIA, `[1002:xxxx]` → AMD. ## SystemInfo::unknown() constructor Returns a SystemInfo with all fields set to "not detected" / false. Used when detect_system() fails (should not happen in normal operation). --- # Module: launcher/runner.rs **Purpose:** Manage child processes for test applications. ## Environment setup Always set these environment variables for launched processes: ``` MANGOHUD=1 MANGOHUD_CONFIGFILE={absolute_path_to_config} ``` Also preserve the user's existing environment (don't replace it — add to it). Use `std::process::Command::env()` not `env_clear()`. ## Terminal detection For `show_terminal=true`, detect the user's terminal in this order: 1. `$MANGOTUNE_TERMINAL` env var (user override) 2. `$TERM_PROGRAM` 3. Try `which` for: `gnome-terminal`, `kgx` (GNOME Console), `konsole`, `xfce4-terminal`, `mate-terminal`, `lxterminal`, `xterm` 4. If none found: launch without terminal, show toast warning Terminal command construction: - gnome-terminal: `gnome-terminal -- {command}` - kgx: `kgx -e {command}` - konsole: `konsole -e {command}` - xterm: `xterm -e {command}` ## Process monitoring After launch, spawn a tokio task that: 1. Waits for process exit via `child.wait()` 2. On exit: sends a message via channel back to UI thread 3. UI removes the "running process" row ## SIGUSR1 for config reload MangoHud reloads config on SIGUSR1. ```rust use nix::sys::signal::{kill, Signal}; use nix::unistd::Pid; pub async fn reload_config(pid: u32) -> anyhow::Result<()> { kill(Pid::from_raw(pid as i32), Signal::SIGUSR1)?; Ok(()) } ``` Add `nix = { version = "0.29", features = ["signal"] }` to Cargo.toml. --- # Module: ui/widgets/cascade_view.rs **Purpose:** Visual CSS-cascade-style display of config layers and conflicts. ## Data model ```rust pub struct CascadeViewModel { pub layers: Vec, pub filter: CascadeFilter, } pub struct LayerViewModel { pub source: LayerSource, pub priority: u8, pub label: String, pub is_editable: bool, pub options: Vec, } pub struct OptionViewModel { pub key: String, pub value: String, pub state: OptionState, pub overridden_by: Option, // layer label that wins } pub enum OptionState { Effective, // this layer's value is used at runtime Shadowed, // overridden by a higher-priority layer Winning, // this layer provides the winning value (overrides lower) } pub enum CascadeFilter { All, ConflictsOnly, ShadowedOnly, } ``` ## Widget construction ```rust pub fn build_cascade_view(model: CascadeViewModel) -> gtk4::Widget ``` Returns a scrollable `GtkScrolledWindow` containing a `GtkBox` (vertical) of `AdwPreferencesGroup` widgets, one per layer in `model.layers`. The widget must be efficiently rebuildable when the filter changes. Connect filter button signals to rebuild/filter the view in-place.