Files
mangotune/docs/plan/all_module_specs.md
T
2026-03-30 23:06:06 -04:00

6.7 KiB

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)

static COLOR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[0-9A-Fa-f]{6}$").unwrap());
static PCI_DEV_RE: Lazy<Regex> = 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<Regex> = Lazy::new(|| {
    Regex::new(r"^((Shift|Control|Alt|Super)_[LR]\+)*(F[1-9]|F1[0-2]|[A-Z])$").unwrap()
});
static FTRACE_RE: Lazy<Regex> = 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<GpuInfo> 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.

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

pub struct CascadeViewModel {
    pub layers: Vec<LayerViewModel>,
    pub filter: CascadeFilter,
}

pub struct LayerViewModel {
    pub source: LayerSource,
    pub priority: u8,
    pub label: String,
    pub is_editable: bool,
    pub options: Vec<OptionViewModel>,
}

pub struct OptionViewModel {
    pub key: String,
    pub value: String,
    pub state: OptionState,
    pub overridden_by: Option<String>,   // 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

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.