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

235 lines
6.8 KiB
Markdown

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