231 lines
6.7 KiB
Markdown
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.
|