# Phase 03 — Config Resolver & System Detection ## Goal Implement the config file discovery/priority system and system detection module. No GTK4 code. All modules are pure Rust with unit tests. ## Files to implement - `src/config/resolver.rs` - `src/system/detect.rs` - `src/system/paths.rs` - `src/system/mod.rs` --- ## src/system/paths.rs ```rust use std::path::PathBuf; use xdg::BaseDirectories; pub struct XdgPaths { pub config_home: PathBuf, // $XDG_CONFIG_HOME or ~/.config pub mangohud_dir: PathBuf, // {config_home}/MangoHud/ pub global_config: PathBuf, // {mangohud_dir}/MangoHud.conf pub data_home: PathBuf, } impl XdgPaths { pub fn resolve() -> anyhow::Result } /// Return all possible Steam root directories (native + Flatpak variants). pub fn steam_roots() -> Vec /// Return all possible Heroic config directories. pub fn heroic_config_dirs() -> Vec /// Return all possible Lutris config directories. pub fn lutris_config_dirs() -> Vec /// Expand ~ in paths (since std::fs doesn't do this). pub fn expand_tilde(path: &str) -> PathBuf ``` --- ## src/system/detect.rs Run at application startup. Returns a `SystemInfo` struct that is passed to the rest of the app and refreshed on demand. ```rust #[derive(Debug, Clone)] pub struct SystemInfo { pub mangohud: MangoHudInfo, pub gpu: GpuInfo, pub display_server: DisplayServer, pub tools: AvailableTools, pub integrations: IntegrationAvailability, } #[derive(Debug, Clone)] pub struct MangoHudInfo { pub installed: bool, pub version: Option, // parsed from `mangohud --version` pub lib_path: Option, // path to libMangoHud.so pub flatpak: bool, } #[derive(Debug, Clone)] pub struct GpuInfo { pub vendor: GpuVendor, pub name: Option, pub pci_id: Option, } #[derive(Debug, Clone, PartialEq)] pub enum DisplayServer { Wayland, X11, XwaylandUnderWayland, Unknown, } #[derive(Debug, Clone)] pub struct AvailableTools { pub vkcube: Option, pub glxgears: Option, pub gamemodectl: Option, pub gamemoded: Option, } #[derive(Debug, Clone)] pub struct IntegrationAvailability { pub steam: bool, pub steam_flatpak: bool, pub lutris: bool, pub lutris_flatpak: bool, pub heroic: bool, pub heroic_flatpak: bool, pub gamemode: bool, } /// Run all detection. Runs in a tokio task, sends result back via channel. pub async fn detect_system() -> anyhow::Result ``` ### Detection implementation details: **MangoHud detection:** 1. Try `which mangohud` → path 2. Run `mangohud --version 2>&1` → parse version string 3. Check library existence at common paths: - `/usr/lib/x86_64-linux-gnu/mangohud/libMangoHud.so` - `/usr/lib/mangohud/libMangoHud.so` - `/usr/local/lib/mangohud/libMangoHud.so` - `~/.local/lib/mangohud/libMangoHud.so` 4. Check Flatpak: `flatpak list 2>/dev/null | grep -i mangohud` **GPU detection:** 1. Read `/sys/class/drm/card*/device/vendor` — `0x1002`=AMD, `0x10de`=NVIDIA, `0x8086`=Intel 2. Read `/sys/class/drm/card*/device/device` for device ID 3. Cross-reference with `lspci -nn 2>/dev/null` for name **Display server detection:** 1. Check `$WAYLAND_DISPLAY` — if set → Wayland 2. Check `$DISPLAY` — if set → X11 (or XwaylandUnderWayland if also Wayland) 3. Check `$XDG_SESSION_TYPE` **Tool detection:** `which` for each tool via `std::process::Command` --- ## src/config/resolver.rs Full implementation of the config layer discovery system. See `docs/config_resolution.md` for the complete algorithm. ```rust use crate::config::types::*; use crate::system::paths::XdgPaths; use std::path::PathBuf; #[derive(Debug, Clone)] pub struct ConfigLayer { pub path: Option, pub source_type: LayerSource, pub priority: u8, pub exists: bool, pub is_editable: bool, pub config: Option, } #[derive(Debug, Clone)] pub enum LayerSource { CompiledDefault, GlobalXdg, PerAppXdg(String), AppLocal(PathBuf), EnvFile(PathBuf), EnvInline(String), } /// A conflict: one option set in multiple layers. #[derive(Debug, Clone)] pub struct ConfigConflict { pub key: String, pub winning_layer_priority: u8, pub winning_value: ConfigValue, pub shadowed: Vec<(u8, ConfigValue)>, // (priority, value) of shadowed layers } pub struct Resolver; impl Resolver { /// Discover all config layers. Reads env vars and filesystem. pub async fn discover(xdg: &XdgPaths) -> anyhow::Result> /// Build conflict map from a resolved layer stack. /// Returns only options that appear in more than one layer with different values. pub fn find_conflicts(layers: &[ConfigLayer]) -> Vec /// Return the effective value for a key (highest priority layer that sets it). pub fn effective_value(key: &str, layers: &[ConfigLayer]) -> ConfigValue /// Return a display label for a layer source. pub fn layer_label(source: &LayerSource) -> String /// Create a new per-app config at the XDG path. pub fn create_per_app_config(app_name: &str, xdg: &XdgPaths) -> anyhow::Result /// Scan common game directories for app-local MangoHud.conf files. async fn scan_game_dirs() -> Vec } ``` ### Discovery order (implement exactly as in docs/config_resolution.md): 1. `$MANGOHUD_CONFIGFILE` → EnvFile (priority 5) 2. `$MANGOHUD_CONFIG` → EnvInline (priority 5, wins over EnvFile) 3. `{xdg}/MangoHud/MangoHud.conf` → GlobalXdg (priority 2) 4. `{xdg}/MangoHud/*.conf` (excluding MangoHud.conf) → PerAppXdg (priority 3) 5. Steam/game dirs scan → AppLocal (priority 4) ### Tests required - discover: correctly reads $XDG_CONFIG_HOME override - discover: env var $MANGOHUD_CONFIG parsed as inline layer at priority 5 - find_conflicts: detects option set in both global and per-app configs - effective_value: returns env value when set, file value otherwise - create_per_app_config: creates file with correct header comment --- ## src/system/mod.rs ```rust pub mod detect; pub mod paths; pub use detect::{SystemInfo, MangoHudInfo, GpuInfo, DisplayServer, AvailableTools}; pub use paths::XdgPaths; ``` --- ## Acceptance Criteria - [ ] `cargo test --lib` passes all new tests - [ ] Resolver correctly discovers layers in priority order - [ ] Env var layers detected with correct priority (5 = highest) - [ ] Conflict detection identifies options shadowed by higher-priority layers - [ ] System detection identifies GPU vendor from `/sys/class/drm` - [ ] MangoHud version extracted from `--version` output - [ ] Display server (Wayland/X11) correctly detected from environment - [ ] All paths resolve via XDG (no hardcoded `/home/username`) - [ ] No `.unwrap()` in production code