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
timecrate (addtime = "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.2v0.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:
$MANGOTUNE_TERMINALenv var (user override)$TERM_PROGRAM- Try
whichfor:gnome-terminal,kgx(GNOME Console),konsole,xfce4-terminal,mate-terminal,lxterminal,xterm - 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:
- Waits for process exit via
child.wait() - On exit: sends a message via channel back to UI thread
- 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.