Files
2026-03-30 23:06:06 -04:00

6.8 KiB

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

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<Self>
}

/// Return all possible Steam root directories (native + Flatpak variants).
pub fn steam_roots() -> Vec<PathBuf>

/// Return all possible Heroic config directories.
pub fn heroic_config_dirs() -> Vec<PathBuf>

/// Return all possible Lutris config directories.
pub fn lutris_config_dirs() -> Vec<PathBuf>

/// 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.

#[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<String>,       // parsed from `mangohud --version`
    pub lib_path: Option<PathBuf>,     // path to libMangoHud.so
    pub flatpak: bool,
}

#[derive(Debug, Clone)]
pub struct GpuInfo {
    pub vendor: GpuVendor,
    pub name: Option<String>,
    pub pci_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum DisplayServer {
    Wayland,
    X11,
    XwaylandUnderWayland,
    Unknown,
}

#[derive(Debug, Clone)]
pub struct AvailableTools {
    pub vkcube: Option<PathBuf>,
    pub glxgears: Option<PathBuf>,
    pub gamemodectl: Option<PathBuf>,
    pub gamemoded: Option<PathBuf>,
}

#[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<SystemInfo>

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/vendor0x1002=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.

use crate::config::types::*;
use crate::system::paths::XdgPaths;
use std::path::PathBuf;

#[derive(Debug, Clone)]
pub struct ConfigLayer {
    pub path: Option<PathBuf>,
    pub source_type: LayerSource,
    pub priority: u8,
    pub exists: bool,
    pub is_editable: bool,
    pub config: Option<AnnotatedConfig>,
}

#[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<Vec<ConfigLayer>>

    /// 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<ConfigConflict>

    /// 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<PathBuf>

    /// Scan common game directories for app-local MangoHud.conf files.
    async fn scan_game_dirs() -> Vec<PathBuf>
}

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

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