235 lines
6.8 KiB
Markdown
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
|