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

231 lines
6.7 KiB
Markdown

# 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)
```rust
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.
```rust
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
```rust
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
```rust
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.