Initial import
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# Game Config DB
|
||||
|
||||
This document describes MangoTune's bundled game/executable hint database used by the
|
||||
`Create New Per-App Config…` flow.
|
||||
|
||||
## Purpose
|
||||
|
||||
The database exists to make per-app MangoHud config creation easier without pretending
|
||||
MangoTune can always infer the correct process name automatically.
|
||||
|
||||
It is used for:
|
||||
|
||||
- searching by game title or alias
|
||||
- suggesting likely MangoHud config names / executable stems
|
||||
- prefilling the per-app config name when the user clicks a result
|
||||
|
||||
It is **not** the source of truth for MangoHud runtime matching. MangoHud still matches
|
||||
the real process or executable name.
|
||||
|
||||
## File Location
|
||||
|
||||
Bundled database:
|
||||
|
||||
- `data/game_config_db.toml`
|
||||
|
||||
Maintenance script:
|
||||
|
||||
- `scripts/build_game_config_db.py`
|
||||
|
||||
Loader/search logic:
|
||||
|
||||
- `src/integrations/game_db.rs`
|
||||
|
||||
UI integration:
|
||||
|
||||
- `src/window.rs`
|
||||
|
||||
## Format
|
||||
|
||||
The database is TOML and uses repeated `[[game]]` tables.
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
[[game]]
|
||||
appid = 730
|
||||
title = "Counter-Strike 2"
|
||||
aliases = ["cs2", "counter strike 2", "counter-strike 2", "csgo"]
|
||||
candidates = ["cs2", "csgo", "wine-cs2"]
|
||||
preferred = "cs2"
|
||||
verification = "verified"
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `appid`
|
||||
- Steam app ID when known
|
||||
- `title`
|
||||
- game display name
|
||||
- `aliases`
|
||||
- extra search terms or alternate names
|
||||
- `candidates`
|
||||
- possible MangoHud config names / executable stems
|
||||
- `preferred`
|
||||
- the main suggested config name the UI fills when clicked
|
||||
- `verification`
|
||||
- trust level for the entry
|
||||
|
||||
## Verification Levels
|
||||
|
||||
Allowed values today:
|
||||
|
||||
- `verified`
|
||||
- manually confirmed from a trustworthy source such as SteamDB launch config pages
|
||||
- `heuristic`
|
||||
- derived from local Steam library scanning and should be treated as a suggestion
|
||||
|
||||
The app currently uses the same search/display behavior for both, but the field exists so
|
||||
maintainers can gradually improve the database quality over time.
|
||||
|
||||
## How The Database Is Built
|
||||
|
||||
The maintenance script currently combines:
|
||||
|
||||
1. Local Steam library data
|
||||
- reads Steam library roots from `libraryfolders.vdf`
|
||||
- reads installed games from `appmanifest_*.acf`
|
||||
- scans install directories for likely executable stems
|
||||
|
||||
2. Curated overrides
|
||||
- hand-maintained entries in `scripts/build_game_config_db.py`
|
||||
- these are where known-good preferred names should be corrected and verified
|
||||
|
||||
The generated file is committed to the repo and used at runtime. MangoTune does not fetch
|
||||
online data at runtime.
|
||||
|
||||
## Updating It
|
||||
|
||||
When adding or refreshing entries:
|
||||
|
||||
1. Edit the curated overrides in `scripts/build_game_config_db.py`
|
||||
2. Run:
|
||||
|
||||
```bash
|
||||
python scripts/build_game_config_db.py
|
||||
```
|
||||
|
||||
3. Review the generated `data/game_config_db.toml`
|
||||
4. Commit both the script changes and the TOML changes
|
||||
|
||||
## Curation Guidance
|
||||
|
||||
Prefer adding or correcting entries when:
|
||||
|
||||
- a game's local-scan preferred name is obviously wrong
|
||||
- SteamDB or another reliable source exposes a clear launch executable
|
||||
- a game has known Proton/native naming differences worth surfacing as candidates
|
||||
|
||||
Do not add contributor names to the data file. Git history already tracks authorship.
|
||||
|
||||
## UX Contract
|
||||
|
||||
The per-app config creation dialog should continue to support both:
|
||||
|
||||
- direct manual typing of the exact executable name
|
||||
- database-backed search by game title / alias
|
||||
|
||||
The database is there to help the user get to the right name faster, not to replace manual
|
||||
control.
|
||||
@@ -0,0 +1,240 @@
|
||||
# MangoHud Option Behavior Reference
|
||||
|
||||
Reference for MangoTune, verified against a local MangoHud source snapshot (`f2e45e9`) on 2026-03-24 and cross-checked against upstream `README.md` and `data/MangoHud.conf`.
|
||||
|
||||
Primary source files:
|
||||
- `/tmp/MangoHud-src/src/overlay_params.h` for the authoritative option list (`OVERLAY_PARAMS`)
|
||||
- `/tmp/MangoHud-src/src/overlay_params.cpp` for parsing, defaults, preset handling, and env/config precedence
|
||||
- `/tmp/MangoHud-src/src/overlay.cpp` for positioning, margin, and horizontal sizing behavior
|
||||
- `/tmp/MangoHud-src/src/config.cpp` for config-file line parsing
|
||||
|
||||
## Parsing Rules
|
||||
|
||||
- Config-file lines are trimmed and `# ...` comments are stripped before value parsing.
|
||||
- A bare key with no `=` is treated as value `1`.
|
||||
- `OVERLAY_PARAM_BOOL(...)` options are effectively flags: bare key or `=1` enables, `=0` disables.
|
||||
- `offset_x` and `offset_y` are parsed as unsigned integers, even though upstream stores them in signed `int` fields later.
|
||||
- Some custom options are still boolean-like in practice, such as `no_display`, `full`, and `help`.
|
||||
- Upstream currently declares `io_read` and `io_write` twice. In normal parsing the earlier boolean branch wins, so they behave as flags in practice.
|
||||
|
||||
## Positioning And Layout Rules
|
||||
|
||||
- Base edge margin is `10px` in `position_layer()`.
|
||||
- That margin becomes `0px` if `hud_no_margin` is enabled or either `offset_x > 0` or `offset_y > 0`.
|
||||
- Right-side anchors (`top-right`, `middle-right`, `bottom-right`) are native in MangoHud.
|
||||
- `horizontal_stretch` defaults to `true`.
|
||||
- Horizontal layouts start with full display width and may shrink after layout when `horizontal_stretch` is disabled.
|
||||
- `top-center` and `bottom-center` get an extra horizontal shift in horizontal non-stretch mode.
|
||||
|
||||
## Precedence And Special Cases
|
||||
|
||||
- Base defaults come from `set_param_defaults()`.
|
||||
- Built-in presets are applied before later explicit options.
|
||||
- `preset` is an upstream-supported special option handled before normal option assignment, even though it does not live inside the main `OVERLAY_PARAMS` macro list.
|
||||
- If `MANGOHUD_CONFIG` is set, MangoHud normally uses env options directly.
|
||||
- `read_cfg=1` tells MangoHud to also read the config file even when env config is present.
|
||||
- `full=1` is a bulk enable mode, not a simple single-feature toggle.
|
||||
- `full=1` immediately disables a curated exclusion list before later explicit options are applied again, including `histogram`, `fps_only`, `horizontal`, `hud_no_margin`, `hud_compact`, `mangoapp_steam`, `dynamic_frame_timing`, `hide_engine_names`, and `hide_fps_superscript`.
|
||||
- `fps_only=1` later forces `legacy_layout` off.
|
||||
- Non-horizontal auto width (`width=0`) is derived later from font size, scale, and column count, with extra widening for I/O rows and `no_small_font`.
|
||||
|
||||
## MangoTune Coverage Audit
|
||||
|
||||
- Verified upstream options in `OVERLAY_PARAMS`: `186`
|
||||
- Additional upstream special options handled outside `OVERLAY_PARAMS`: `preset`, `inherit`
|
||||
- Duplicate upstream declarations: `io_read`, `io_write`
|
||||
- MangoTune schema entries today: `187`
|
||||
- MangoTune now represents the full currently documented option surface in its live schema, including the special-case keys `preset`, `inherit`, and `help`
|
||||
- Some of those options intentionally live in advanced UI sections because they are niche directives or special-case upstream behaviors rather than normal daily tuning knobs
|
||||
|
||||
## Verified Option Table
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `af` | Signed integer | `-1` | Anisotropic filtering; -1=unset |
|
||||
| `alpha` | Float | `1.0` | Overall HUD transparency |
|
||||
| `arch` | Flag (bare key or =1/=0) | `false` | MangoHud architecture |
|
||||
| `autostart_log` | Unsigned integer | | Seconds before auto-start |
|
||||
| `background_alpha` | Float | `0.5` | Background transparency for the HUD window. |
|
||||
| `background_color` | Hex color (RRGGBB) | `020202` | Hex color used for the Background readout or accent. |
|
||||
| `battery` | Flag (bare key or =1/=0) | `false` | Shows battery status when available. |
|
||||
| `battery_color` | Hex color (RRGGBB) | `FF9078` | Hex color used for the Battery readout or accent. |
|
||||
| `battery_icon` | Flag (bare key or =1/=0) | `false` | DEPENDS ON: `battery` |
|
||||
| `battery_time` | Flag (bare key or =1/=0) | `false` | DEPENDS ON: `battery` |
|
||||
| `battery_watt` | Flag (bare key or =1/=0) | `false` | DEPENDS ON: `battery` |
|
||||
| `benchmark_percentiles` | String list (legacy / deprecated) | `"97","AVG"` in current source defaults | Current source seeds `97,AVG`, but the parser logs that this option is obsolete and recommends `fps_metrics`. Upstream README still documents a broader default set (`97,AVG,1,0.1`), so published docs and current source are not fully aligned. |
|
||||
| `bicubic` | Flag (bare key or =1/=0) | `false` | Force bicubic filtering |
|
||||
| `blacklist` | Delimited string list | | App names to suppress overlay |
|
||||
| `cellpadding_y` | Float | `-0.085` | Vertical cell padding tweak used by the ImGui table layout. |
|
||||
| `control` | String | `-1` | Creates/listens on a control socket path; `%p` is expanded to the current process ID. |
|
||||
| `core_bars` | Flag (bare key or =1/=0) | `false` | Graphical core bars |
|
||||
| `core_load` | Flag (bare key or =1/=0) | `false` | Per-core load bars |
|
||||
| `core_load_change` | Flag (bare key or =1/=0) | `false` | Color per-core load |
|
||||
| `core_type` | Flag (bare key or =1/=0) | `false` | Show P/E core type labels |
|
||||
| `cpu_color` | Hex color (RRGGBB) | `2E97CB` | Hex color used for the Cpu readout or accent. |
|
||||
| `cpu_custom_temp_sensor` | hwmon name,input pair | | Custom CPU temperature source in `hwmon_name,hwmon_input` form. |
|
||||
| `cpu_efficiency` | Flag (bare key or =1/=0) | `false` | Shows CPU efficiency / perf-per-watt style data when available. |
|
||||
| `cpu_load_change` | Flag (bare key or =1/=0) | `false` | Color CPU load |
|
||||
| `cpu_load_color` | List of 3 hex colors | `0x39f900,0xfdfd09,0xb22222` | Three hex colors |
|
||||
| `cpu_load_value` | Comma-separated integers | `60,90` | Two thresholds |
|
||||
| `cpu_mhz` | Flag (bare key or =1/=0) | `false` | Average MHz across cores |
|
||||
| `cpu_power` | Flag (bare key or =1/=0) | `false` | Shows CPU package or socket power draw when available. |
|
||||
| `cpu_stats` | Flag (bare key or =1/=0) | `true` | Master CPU section toggle |
|
||||
| `cpu_temp` | Flag (bare key or =1/=0) | `false` | Shows CPU temperature. |
|
||||
| `cpu_text` | String | | Custom CPU label |
|
||||
| `custom_text` | String-bearing special option | `""` / unset | Declared in the macro list like a bool, but in practice MangoHud parses and uses it as a text payload line. |
|
||||
| `custom_text_center` | String-bearing special option | `""` / unset | Declared in the macro list like a bool, but in practice MangoHud parses and uses it as centered text. |
|
||||
| `debug` | Flag (bare key or =1/=0) | `false` | Gamescope app frametimes graph |
|
||||
| `device_battery` | Delimited string list | | List of device battery kinds such as `controller`, `mouse`, or `headset`. |
|
||||
| `device_battery_icon` | Flag (bare key or =1/=0) | `false` | Adds icons to device battery entries. |
|
||||
| `display_server` | Flag (bare key or =1/=0) | `false` | Wayland/X11 indicator |
|
||||
| `duration` | Flag (bare key or =1/=0) | `false` | Enables benchmark-duration style tracking; base default is off even under `full`. |
|
||||
| `dx_api` | Flag (bare key or =1/=0) | `false` | Displays the DirectX API/backend label where supported. |
|
||||
| `dynamic_frame_timing` | Flag (bare key or =1/=0) | `false` | Dynamic scale frametime graph |
|
||||
| `engine_color` | Hex color (RRGGBB) | `EB5B5B` | Hex color used for the Engine readout or accent. |
|
||||
| `engine_short_names` | Flag (bare key or =1/=0) | `false` | Uses shorter engine names when engine info is displayed. |
|
||||
| `engine_version` | Flag (bare key or =1/=0) | `false` | Displays the engine version string when MangoHud can detect it. |
|
||||
| `exec` | String-bearing special option | `""` / unset | Runs a shell command and shows its output in the next column; only works with `legacy_layout=0`. Like `custom_text`, it is handled more like a string payload than a normal bool-style flag. |
|
||||
| `exec_name` | Flag (bare key or =1/=0) | `false` | Show executable name |
|
||||
| `fan` | Flag (bare key or =1/=0) | `false` | Steam Deck fan RPM |
|
||||
| `fcat` | Flag (bare key or =1/=0) | `false` | Enable FCAT overlay |
|
||||
| `fcat_overlay_width` | Unsigned integer | `24` | DEPENDS ON: `fcat` |
|
||||
| `fcat_screen_edge` | Unsigned integer | `0` | DEPENDS ON: `fcat` |
|
||||
| `fex_stats` | Option list | | Tokenized option list enabling selected FEX-Emu stat groups when MangoHud is built with FEX support. |
|
||||
| `flip_efficiency` | Flag (bare key or =1/=0) | `false` | Joules per frame |
|
||||
| `font_file` | Filesystem path | | TTF/OTF path — validated to exist if set |
|
||||
| `font_file_text` | Filesystem path | | Custom label text for the Font File section. |
|
||||
| `font_glyph_ranges` | Glyph range flags | | Comma-separated glyph packs such as `japanese`, `cyrillic`, or `latin_ext_a`. |
|
||||
| `font_scale` | Float | `1.0` | Global font scale multiplier. |
|
||||
| `font_scale_media_player` | Float | `0.55` | Separate font scale for media-player text. |
|
||||
| `font_size` | Float | `24` | Main font size in pixels. |
|
||||
| `font_size_secondary` | Float | | Secondary text size used by some secondary labels; parsed separately from the main `font_size`. |
|
||||
| `font_size_text` | Float | | For text elements |
|
||||
| `fps` | Flag (bare key or =1/=0) | `true` | Show FPS counter — enabled by default |
|
||||
| `fps_color` | List of 3 hex colors | `0xb22222,0xfdfd09,0x39f900` | Three hex colors: low,mid,high |
|
||||
| `fps_color_change` | Flag (bare key or =1/=0) | `false` | Enable FPS color thresholds |
|
||||
| `fps_limit` | Comma-separated numbers | `0` | 0 = unlimited; comma-separated list e.g. `0,30,60` |
|
||||
| `fps_limit_method` | Enum (`early` or `late`) | `FPS_LIMIT_METHOD_LATE` | Limit strategy: `late` is the default, `early` waits earlier in the frame. |
|
||||
| `fps_metrics` | Metric list | | e.g. `avg,0.01,1,97` — `AVG` or decimal percentiles |
|
||||
| `fps_only` | Flag (bare key or =1/=0) | `false` | CONFLICTS WITH: all other display params |
|
||||
| `fps_sampling_period` | String | `500ms` | Sampling period in milliseconds in config/env input; MangoHud stores it internally in nanoseconds. |
|
||||
| `fps_text` | String | | Custom label for FPS row |
|
||||
| `fps_value` | Comma-separated integers | `30,60` | Two thresholds: warn,ok |
|
||||
| `frame_count` | Flag (bare key or =1/=0) | `false` | Show frame counter |
|
||||
| `frame_timing` | Flag (bare key or =1/=0) | `true` | Frametime graph — enabled by default |
|
||||
| `frame_timing_detailed` | Flag (bare key or =1/=0) | `false` | More detailed frametime graph |
|
||||
| `frametime` | Flag (bare key or =1/=0) | `true` | Show frametime — enabled by default |
|
||||
| `frametime_color` | Hex color (RRGGBB) | `00FF00` | Hex color used for the Frametime readout or accent. |
|
||||
| `fsr` | Flag (bare key or =1/=0) | `false` | FSR status |
|
||||
| `fsr_steam_sharpness` | Float | `-1` | Float sharpness control for Steam FSR integration. MangoHud default is `-1`, which means “leave unset / use app or Steam default”. |
|
||||
| `ftrace` | Tracepoint spec list | | Tracepoint specification list used only when MangoHud is built with ftrace support. |
|
||||
| `full` | Boolean-like (0/1) | `false` | Enables almost every boolean option, then explicitly disables a curated exclusion list. Use with caution because it can enable many unrelated HUD sections at once. |
|
||||
| `gamemode` | Flag (bare key or =1/=0) | `false` | GameMode running status |
|
||||
| `gl_bind_framebuffer` | Unsigned integer | | Rebind framebuffer before draw |
|
||||
| `gl_dont_flip` | Unsigned integer | | Don't swap origin for GL_UPPER_LEFT |
|
||||
| `gl_size_query` | Enum (`drawable`, `viewport`, `scissorbox`) | | Default: glXQueryDrawable |
|
||||
| `gl_vsync` | Signed integer | `-2` | Signed integer for OpenGL swap interval; `-2` means “unset” in MangoHud defaults. |
|
||||
| `gpu_color` | Hex color (RRGGBB) | `2E9762` | Hex color used for the Gpu readout or accent. |
|
||||
| `gpu_core_clock` | Flag (bare key or =1/=0) | `false` | Shows GPU core clock speed. |
|
||||
| `gpu_efficiency` | Flag (bare key or =1/=0) | `false` | Shows GPU efficiency / perf-per-watt style data when available. |
|
||||
| `gpu_fan` | Flag (bare key or =1/=0) | `false` | RPM on AMD, percent on NVIDIA |
|
||||
| `gpu_junction_temp` | Flag (bare key or =1/=0) | `false` | Shows GPU hotspot / junction temperature when available. |
|
||||
| `gpu_list` | GPU index list | | Comma-separated GPU indices to show. Useful on multi-GPU systems. |
|
||||
| `gpu_load_change` | Flag (bare key or =1/=0) | `false` | Color GPU load |
|
||||
| `gpu_load_color` | List of 3 hex colors | `0x39f900,0xfdfd09,0xb22222` | Three hex colors |
|
||||
| `gpu_load_value` | Comma-separated integers | `60,90` | Two load thresholds |
|
||||
| `gpu_mem_clock` | Flag (bare key or =1/=0) | `false` | DEPENDS ON: `vram` |
|
||||
| `gpu_mem_temp` | Flag (bare key or =1/=0) | `false` | DEPENDS ON: `vram` |
|
||||
| `gpu_name` | Flag (bare key or =1/=0) | `false` | Show GPU model name |
|
||||
| `gpu_power` | Flag (bare key or =1/=0) | `false` | Shows GPU power draw. |
|
||||
| `gpu_power_limit` | Flag (bare key or =1/=0) | `false` | Displays the current GPU power limit. |
|
||||
| `gpu_stats` | Flag (bare key or =1/=0) | `true` | Master GPU section toggle |
|
||||
| `gpu_temp` | Flag (bare key or =1/=0) | `false` | Shows GPU temperature. |
|
||||
| `gpu_text` | Delimited string list | | Parsed as a tokenized string list in source, so multiple GPU labels can be supplied for multi-GPU setups. |
|
||||
| `gpu_voltage` | Flag (bare key or =1/=0) | `false` | AMD ONLY |
|
||||
| `graphs` | Flag (bare key or =1/=0) | `false` | Valid values: `gpu_load,cpu_load,gpu_core_clock,gpu_mem_clock,vram,ram,cpu_temp,gpu_temp` |
|
||||
| `hdr` | Flag (bare key or =1/=0) | `false` | HDR status |
|
||||
| `height` | Unsigned integer | `140` | HUD window height. Horizontal layouts start with this height before any later width adjustments. |
|
||||
| `help` | Boolean-like (prints help and exits) | `0` | Printing helper: causes MangoHud to print supported env-style params to stderr. Not a normal overlay display toggle. |
|
||||
| `hide_engine_names` | Flag (bare key or =1/=0) | `false` | Hides engine names even if engine-related display items are enabled. |
|
||||
| `hide_fps_superscript` | Flag (bare key or =1/=0) | `false` | Removes the small `FPS` superscript styling from the FPS readout. |
|
||||
| `hide_fsr_sharpness` | Flag (bare key or =1/=0) | `false` | DEPENDS ON: `fsr` |
|
||||
| `histogram` | Flag (bare key or =1/=0) | `false` | CONFLICTS WITH frame_timing |
|
||||
| `horizontal` | Flag (bare key or =1/=0) | `false` | Switches the HUD into a horizontal/table layout. MangoHud initially sizes it to full display width, then may shrink it after layout based on content width. |
|
||||
| `horizontal_separator_color` | Hex color (RRGGBB) | `AD64C1` | Hex color used for the Horizontal Separator readout or accent. |
|
||||
| `horizontal_stretch` | Flag (bare key or =1/=0) | `true` | Defaults to true. When disabled, MangoHud shrinks horizontal HUD width after rendering to match content instead of stretching across the screen. |
|
||||
| `hud_compact` | Flag (bare key or =1/=0) | `false` | Compact mode |
|
||||
| `hud_no_margin` | Flag (bare key or =1/=0) | `false` | Removes MangoHud’s normal 10px edge margin. The same margin is also removed automatically whenever `offset_x` or `offset_y` is greater than zero. |
|
||||
| `inherit` | Preset-file directive | `inherit` line inside `presets.conf` | Mainly used inside preset definitions, where a literal `inherit` line re-applies parent preset behavior. It is not a typical everyday overlay toggle. |
|
||||
| `io_color` | Hex color (RRGGBB) | `A491D3` | Hex color used for the Io readout or accent. |
|
||||
| `io_read` | Flag (bare key or =1/=0) | `false` | Shows per-process I/O read throughput. Note: upstream declares this key twice (`BOOL` and `CUSTOM`), but the boolean branch wins first, so practical config behavior is flag-like. |
|
||||
| `io_write` | Flag (bare key or =1/=0) | `false` | Shows per-process I/O write throughput. Note: upstream declares this key twice (`BOOL` and `CUSTOM`), but the boolean branch wins first, so practical config behavior is flag-like. |
|
||||
| `legacy_layout` | Flag (bare key or =1/=0) | `true` | Uses MangoHud’s older default ordering/layout behavior. `fps_only` forces this off. |
|
||||
| `log_duration` | Unsigned integer | | Seconds |
|
||||
| `log_interval` | Unsigned integer | `0` | ms; 0 = default |
|
||||
| `log_versioning` | Flag (bare key or =1/=0) | `false` | Appends version info to generated log filenames. |
|
||||
| `mangoapp_steam` | Flag (bare key or =1/=0) | `false` | mangoapp only |
|
||||
| `media_player` | Flag (bare key or =1/=0) | `false` | Enable media player metadata |
|
||||
| `media_player_color` | Hex color (RRGGBB) | `FFFFFF` | Hex color used for the Media Player readout or accent. |
|
||||
| `media_player_format` | Semicolon-separated format list | `"{title}","{artist}","{album}"` | Semicolon-separated format strings. MangoHud formats `{title}`, `{artist}`, and `{album}` tokens from MPRIS metadata. |
|
||||
| `media_player_name` | String | `""` | e.g. `spotify` — DEPENDS ON: `media_player` |
|
||||
| `network` | Delimited string list | | Comma-separated network interfaces; empty/default means MangoHud decides what to show. |
|
||||
| `network_color` | Hex color (RRGGBB) | `E07B85` | Hex color used for the Network readout or accent. |
|
||||
| `no_display` | Boolean-like (0/1) | `0` | Boolean-style option. Starts MangoHud hidden, but toggle/reload keys still work. |
|
||||
| `no_small_font` | Unsigned integer | | Disable small font for secondary info |
|
||||
| `offset_x` | Unsigned integer | `0` | Unsigned X offset. Positive values push left-anchored HUDs right and right-anchored HUDs farther right. Any positive offset also removes the normal 10px edge margin. |
|
||||
| `offset_y` | Unsigned integer | `0` | Unsigned Y offset. Positive values push the HUD downward. Any positive offset also removes the normal 10px edge margin. |
|
||||
| `output_file` | Filesystem path | | Explicit output filename for benchmark/log output. |
|
||||
| `output_folder` | Filesystem path | | Must be writable directory |
|
||||
| `pci_dev` | String | | Format: `domain:bus:slot.function` e.g. `0000:03:00.0` |
|
||||
| `permit_upload` | Unsigned integer | `0` | Upload to flightlessmango.com |
|
||||
| `picmip` | Signed integer | `-17` | Mip-map LoD bias; negative=sharper |
|
||||
| `preset` | Comma-separated preset list | `-1,0,1,2,3,4` in upstream docs/examples | Upstream special option handled before normal assignment. Supports built-in presets and user presets via `presets.conf`. |
|
||||
| `position` | Enum position | `top-left` | Anchor position. Right anchors are native in MangoHud; `top-center` and `bottom-center` get extra horizontal adjustment when `horizontal` is on and `horizontal_stretch` is off. |
|
||||
| `present_mode` | Flag (bare key or =1/=0) | `false` | Displays the current presentation mode in the HUD. |
|
||||
| `proc_vram` | Flag (bare key or =1/=0) | `false` | Per-process VRAM |
|
||||
| `procmem` | Flag (bare key or =1/=0) | `false` | Per-process resident memory |
|
||||
| `procmem_shared` | Flag (bare key or =1/=0) | `false` | DEPENDS ON: `procmem` |
|
||||
| `procmem_virt` | Flag (bare key or =1/=0) | `false` | DEPENDS ON: `procmem` |
|
||||
| `ram` | Flag (bare key or =1/=0) | `false` | Shows system RAM usage. |
|
||||
| `ram_color` | Hex color (RRGGBB) | `C26693` | Hex color used for the Ram readout or accent. |
|
||||
| `ram_temp` | Flag (bare key or =1/=0) | `false` | Displays RAM temperature if MangoHud can read it from available sensors. |
|
||||
| `read_cfg` | Flag (bare key or =1/=0) | `false` | Only matters when using `MANGOHUD_CONFIG` in the environment: it tells MangoHud to also read the config file instead of using env options alone. |
|
||||
| `refresh_rate` | Flag (bare key or =1/=0) | `false` | Current refresh rate |
|
||||
| `reload_cfg` | Keybind | `Shift_L+F4` on Linux | Reload keybind. Useful for external editors or MangoTune preview sessions. |
|
||||
| `reset_fps_metrics` | Keybind | `Shift_R+F9` on Linux | Keybind that resets FPS metrics / percentiles. |
|
||||
| `resolution` | Flag (bare key or =1/=0) | `false` | Current display resolution |
|
||||
| `retro` | Flag (bare key or =1/=0) | `false` | Disable linear filtering (blocky textures) |
|
||||
| `round_corners` | Unsigned integer | `0` | Rounded-corner radius for the HUD background. |
|
||||
| `show_fps_limit` | Flag (bare key or =1/=0) | `false` | Display current FPS limit value |
|
||||
| `swap` | Flag (bare key or =1/=0) | `false` | Shows swap usage. |
|
||||
| `table_columns` | Unsigned integer | `3` | Number of table columns. MangoHud later clamps it to the range `1..64`. |
|
||||
| `temp_fahrenheit` | Flag (bare key or =1/=0) | `false` | Use °F instead of °C |
|
||||
| `text_color` | Hex color (RRGGBB) | `FFFFFF` | Hex color used for the Text readout or accent. |
|
||||
| `text_outline` | Flag (bare key or =1/=0) | `true` | Draws an outline around text for readability. |
|
||||
| `text_outline_color` | Hex color (RRGGBB) | `000000` | Hex color used for the Text Outline readout or accent. |
|
||||
| `text_outline_thickness` | Float | `1.5` | Thickness of the text outline stroke. |
|
||||
| `throttling_status` | Flag (bare key or =1/=0) | `false` | GPU throttling indicator |
|
||||
| `throttling_status_graph` | Flag (bare key or =1/=0) | `false` | Show throttling on frametime graph |
|
||||
| `time` | Flag (bare key or =1/=0) | `false` | Current time |
|
||||
| `time_format` | String | `"%T"` | strftime format |
|
||||
| `time_no_label` | Flag (bare key or =1/=0) | `false` | DEPENDS ON: `time` |
|
||||
| `toggle_fps_limit` | Keybind | `Shift_L+F1` on Linux | Switches between configured FPS limit entries. |
|
||||
| `toggle_hud` | Keybind | `Shift_R+F12` on Linux | Keybind list parsed via xkb key names on Linux / key codes on Windows. |
|
||||
| `toggle_hud_position` | Keybind | `Shift_R+F11` on Linux | Cycles through positions with a keybind. Upstream README also spells this as `R_Shift+F11`; `data/MangoHud.conf` uses `Shift_R+F11`. |
|
||||
| `toggle_logging` | Keybind | `Shift_L+F2` on Linux | Keybind that starts or stops logging. |
|
||||
| `toggle_preset` | Keybind | `Shift_R+F10` on Linux | Cycles through presets with a keybind. |
|
||||
| `trilinear` | Flag (bare key or =1/=0) | `false` | Force trilinear filtering |
|
||||
| `upload_log` | Keybind | `Shift_L+F3` on Linux | Keybind that uploads the current log. |
|
||||
| `upload_logs` | Keybind | `Control_L+F3` on Linux | DEPENDS ON: `permit_upload=1` |
|
||||
| `version` | Flag (bare key or =1/=0) | `false` | Show MangoHud version in overlay |
|
||||
| `vkbasalt` | Flag (bare key or =1/=0) | `false` | vkBasalt running status |
|
||||
| `vram` | Flag (bare key or =1/=0) | `false` | Required by gpu_mem_clock, gpu_mem_temp |
|
||||
| `vram_color` | Hex color (RRGGBB) | `AD64C1` | Hex color used for the Vram readout or accent. |
|
||||
| `vsync` | Unsigned integer | `-1` | Unsigned value in the parser, but MangoHud seeds the default to `-1` before parsing config/env overrides. |
|
||||
| `vulkan_driver` | Flag (bare key or =1/=0) | `false` | Show Vulkan driver string |
|
||||
| `vulkan_present_mode` | String | | String name for the Vulkan present mode preference; MangoHud accepts names like `fifo`, `mailbox`, or full `VK_PRESENT_MODE_*_KHR`. |
|
||||
| `width` | Unsigned integer | `0` | Explicit window width. `0` means MangoHud decides automatically; for non-horizontal layouts MangoHud later derives a width from font size, scale, and column count. |
|
||||
| `wine` | Flag (bare key or =1/=0) | `false` | Wine/Proton version |
|
||||
| `wine_color` | Hex color (RRGGBB) | `EB5B5B` | Hex color used for the Wine readout or accent. |
|
||||
| `winesync` | Flag (bare key or =1/=0) | `false` | Wine sync method |
|
||||
@@ -0,0 +1,159 @@
|
||||
# MangoHud Position Lab
|
||||
|
||||
This is a direct MangoHud test harness for debugging positioning behavior outside MangoTune's
|
||||
preview pipeline.
|
||||
|
||||
## Why
|
||||
|
||||
When right-aligned horizontal preview behavior looks wrong, the first question is whether the
|
||||
problem is in MangoTune's preview math or MangoHud itself. This lab runs MangoHud directly
|
||||
against a simple test app so the behavior can be compared without preview-only overrides.
|
||||
|
||||
## Script
|
||||
|
||||
Use:
|
||||
|
||||
```bash
|
||||
scripts/mangohud-position-lab.sh <config> [glxgears|vkcube] [output_dir]
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
scripts/mangohud-position-lab.sh ~/.config/mangotune/profiles/zz_test_right_sparse_top.conf glxgears
|
||||
scripts/mangohud-position-lab.sh ~/.config/mangotune/profiles/zz_test_right_full_top.conf vkcube /tmp/mh-lab/full-top
|
||||
```
|
||||
|
||||
The script:
|
||||
- launches MangoHud directly
|
||||
- records stdout/stderr to `run.log`
|
||||
- tries to dump the X window tree to `xwininfo.tree`
|
||||
- tries to capture window geometry and a screenshot when `xdotool`/`import` are available
|
||||
- honors `DISPLAY`, `XAUTHORITY`, `WAIT_SECS`, `WIDTH`, and `HEIGHT` from the environment
|
||||
|
||||
## Matrix runner
|
||||
|
||||
For the standard right-alignment repro set, use:
|
||||
|
||||
```bash
|
||||
scripts/mangohud-position-matrix.sh [vkcube|glxgears] [output_dir] [profile_dir]
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
scripts/mangohud-position-matrix.sh vkcube /tmp/mh-matrix
|
||||
DISPLAY=:1 XAUTHORITY=/root/.Xauthority scripts/mangohud-position-matrix.sh vkcube /tmp/mh-matrix /home/aaron/mangotune-test-profiles
|
||||
```
|
||||
|
||||
This generates both margin-on and margin-off variants for the standard right-alignment test
|
||||
profiles and captures each case directly through MangoHud.
|
||||
|
||||
## Suggested Test Matrix
|
||||
|
||||
Run at least these profile families:
|
||||
|
||||
1. `zz_test_right_full_top.conf`
|
||||
2. `zz_test_right_sparse_top.conf`
|
||||
3. `zz_test_right_sparse_middle.conf`
|
||||
4. `zz_test_right_sparse_compact_top.conf`
|
||||
5. `zz_test_right_sparse_compact_middle.conf`
|
||||
|
||||
Then compare:
|
||||
- `hud_no_margin` on vs off
|
||||
- `hud_compact` on vs off
|
||||
- sparse vs fuller layouts
|
||||
- `top-right` vs `middle-right`
|
||||
|
||||
Recommended validation order:
|
||||
|
||||
1. `full` top-right, margin on
|
||||
2. `full` top-right, margin off
|
||||
3. `sparse` top-right, margin on
|
||||
4. `sparse` top-right, margin off
|
||||
5. `sparse_compact` top-right, margin off
|
||||
6. `sparse` middle-right, margin off
|
||||
7. `sparse_compact` middle-right, margin off
|
||||
|
||||
## Isolated Xorg on arch.lan
|
||||
|
||||
When local testing would interrupt the user desktop, `arch.lan` can run a separate Xorg on
|
||||
another VT and use that for direct MangoHud captures:
|
||||
|
||||
```bash
|
||||
ssh aaron@arch.lan
|
||||
sudo -n xinit /bin/bash -lc 'xsetroot -solid black; while :; do sleep 3600; done' \
|
||||
-- /usr/bin/Xorg :1 vt8 -nolisten tcp -noreset
|
||||
```
|
||||
|
||||
Then from another shell:
|
||||
|
||||
```bash
|
||||
ssh aaron@arch.lan '
|
||||
DISPLAY=:1 XAUTHORITY=/root/.Xauthority \
|
||||
/path/to/mangotune/scripts/mangohud-position-matrix.sh \
|
||||
vkcube /tmp/mh-matrix /home/aaron/mangotune-test-profiles
|
||||
'
|
||||
```
|
||||
|
||||
That keeps the direct MangoHud investigation isolated from the main desktop session.
|
||||
|
||||
## Expected MangoHud Semantics
|
||||
|
||||
From MangoHud's upstream README:
|
||||
- `position=` supports `top-right`, `middle-right`, and `bottom-right` directly
|
||||
- `horizontal` enables horizontal layout
|
||||
- `horizontal_stretch` stretches the background to the screen width in horizontal mode
|
||||
- `hud_no_margin` removes margins around MangoHud
|
||||
- `offset_x` and `offset_y` are generic HUD position offsets
|
||||
- `width=` overrides the automatically computed HUD width
|
||||
|
||||
That means MangoTune should be conservative about inventing extra right-anchor behavior. If
|
||||
MangoHud itself has a quirk here, MangoTune should document or surface it rather than baking in
|
||||
fragile compensations unless absolutely necessary.
|
||||
|
||||
## Upstream source findings
|
||||
|
||||
From MangoHud upstream `src/overlay.cpp` and `src/overlay_params.cpp`:
|
||||
|
||||
- Default edge margin is `10.0f`.
|
||||
- The margin is forced to `0.0f` whenever **any** of these are true:
|
||||
- `offset_x > 0`
|
||||
- `offset_y > 0`
|
||||
- `hud_no_margin` is enabled
|
||||
- For right-side anchors, MangoHud positions the HUD with:
|
||||
|
||||
```cpp
|
||||
x = display_width - window_size.x - margin + offset_x
|
||||
```
|
||||
|
||||
That means:
|
||||
- right anchors are native in MangoHud
|
||||
- positive `offset_x` pushes a right-anchored HUD farther right, not inward
|
||||
- because `offset_x` is parsed as unsigned, MangoHud cannot use a negative right-anchor offset
|
||||
to nudge the HUD left from `top-right` / `middle-right` / `bottom-right`
|
||||
|
||||
Also note:
|
||||
|
||||
- `horizontal_stretch` defaults to `true` upstream, so `horizontal_stretch=0` must be written
|
||||
explicitly when the user disables it
|
||||
- `offset_x` / `offset_y` are parsed as unsigned values upstream
|
||||
|
||||
These semantics should be treated as source-of-truth before adding any MangoTune-side
|
||||
compensation.
|
||||
|
||||
## Capture caveat
|
||||
|
||||
On the isolated remote Xorg lab, direct screenshot methods can still miss MangoHud's overlay
|
||||
entirely while capturing the app window correctly. This includes:
|
||||
|
||||
- ImageMagick `import` against the app window
|
||||
- ImageMagick `import -window root` against the full desktop
|
||||
- `ffmpeg -f x11grab` style X11 captures
|
||||
|
||||
So:
|
||||
|
||||
- direct launch/log/geometry capture is reliable there
|
||||
- direct pixel capture of the overlay is not yet a trustworthy automated oracle
|
||||
- if visual confirmation matters, a manual screenshot on a real desktop session is still the
|
||||
most reliable check
|
||||
@@ -0,0 +1,187 @@
|
||||
# MangoTune Window Geometry Investigation
|
||||
|
||||
This documents the March 28, 2026 investigation into the main-window width drift / off-screen placement issue.
|
||||
|
||||
## Symptom
|
||||
|
||||
- On XFCE/X11, MangoTune can open slightly off-screen on the right and bottom.
|
||||
- Switching pages such as `Live Preview` and `Debug` can make the already-open window grow wider.
|
||||
- XFCE appears to keep the same top-left corner when that happens, so the right edge drifts farther off-screen.
|
||||
|
||||
## Environment Observed
|
||||
|
||||
- Session type: `X11`
|
||||
- Desktop: `XFCE`
|
||||
- Monitor/work area observed during testing:
|
||||
- screen: `3440x1440`
|
||||
- `_NET_WORKAREA`: `3440x1395`
|
||||
|
||||
## Concrete Live Findings
|
||||
|
||||
### Startup geometry
|
||||
|
||||
Observed X11 state from a live MangoTune window:
|
||||
|
||||
- `WM_NORMAL_HINTS` minimum size: `1068 x 650`
|
||||
- actual window geometry at startup:
|
||||
- `x=2397`
|
||||
- `y=311`
|
||||
- `width=1068`
|
||||
- `height=1109`
|
||||
|
||||
That puts the window slightly off-screen already:
|
||||
|
||||
- right edge: about `25 px` off-screen
|
||||
- bottom edge: about `20 px` off-screen
|
||||
|
||||
### Page-switch growth
|
||||
|
||||
Repeated live repro:
|
||||
|
||||
1. start from `Dashboard`
|
||||
2. switch to `Live Preview`
|
||||
3. switch to `Debug`
|
||||
4. switch back to `Dashboard`
|
||||
|
||||
After that sequence:
|
||||
|
||||
- `WM_NORMAL_HINTS` minimum size stays `1068 x 650`
|
||||
- actual window width grows from `1068` to `1288`
|
||||
- top-left position stays fixed at the same `x/y`
|
||||
|
||||
Important conclusion:
|
||||
|
||||
- page switching is **not** changing the X11 minimum-size hint
|
||||
- page switching **is** causing the actual shown window width to grow
|
||||
|
||||
So this is not just a bad startup min-width hint. There is a second issue: post-navigation width growth.
|
||||
|
||||
## What Was Tested
|
||||
|
||||
### 1. Clamp startup size to monitor geometry
|
||||
|
||||
Result:
|
||||
|
||||
- helped startup placement
|
||||
- did **not** stop the post-navigation width growth
|
||||
|
||||
### 2. Shared page clamp / scroller tightening
|
||||
|
||||
Changes tested:
|
||||
|
||||
- Adwaita `Clamp` around page bodies
|
||||
- `ScrolledWindow` with:
|
||||
- `min_content_width = 0`
|
||||
- `propagate_natural_width = false`
|
||||
- `propagate_natural_height = false`
|
||||
|
||||
Result:
|
||||
|
||||
- sensible as a page-system cleanup
|
||||
- did **not** stop the post-navigation width growth
|
||||
|
||||
### 3. Tighten likely culprit pages
|
||||
|
||||
Pages specifically investigated:
|
||||
|
||||
- `Live Preview`
|
||||
- `Debug`
|
||||
|
||||
Changes tested included:
|
||||
|
||||
- wrapping text views by character
|
||||
- `min_content_width = 0`
|
||||
- narrowing preview controls / button grids
|
||||
|
||||
Result:
|
||||
|
||||
- reduced some page pressure
|
||||
- did **not** stop the actual width growth after navigation
|
||||
|
||||
### 4. Reapply current window size after page refresh
|
||||
|
||||
Tried preserving the current window size with `set_default_size(...)` after swapping the page child.
|
||||
|
||||
Result:
|
||||
|
||||
- did **not** stop the width growth in the live XFCE/X11 repro
|
||||
|
||||
### 5. Invalid GTK CSS audit
|
||||
|
||||
Found unsupported CSS properties that GTK was ignoring, such as web-style layout rules:
|
||||
|
||||
- `display`
|
||||
- `flex`
|
||||
- `gap`
|
||||
- `align-items`
|
||||
- `overflow`
|
||||
- `@media`
|
||||
|
||||
Result after cleanup:
|
||||
|
||||
- real improvement
|
||||
- startup min width dropped in one rebuilt run from about `1068` to about `848`
|
||||
- startup centering improved
|
||||
- but the post-navigation width drift still remained
|
||||
|
||||
So invalid CSS was a real issue, but not the full root cause.
|
||||
|
||||
### 6. Lazy-page / placeholder experiment
|
||||
|
||||
Tried making `NavigationView` keep placeholder children and attach only the visible real page, to test whether Adwaita was sizing itself to the widest visited page.
|
||||
|
||||
Result:
|
||||
|
||||
- **no improvement**
|
||||
- the same page-switch sequence still produced the same width jump
|
||||
|
||||
This experiment was reverted.
|
||||
|
||||
### 7. Temporary startup-width mitigation
|
||||
|
||||
A temporary mitigation was added after the investigation:
|
||||
|
||||
- MangoTune now starts with a wider baseline window width of about `1300 px`
|
||||
- this is intentionally around the widest observed page width (`Debug`/post-navigation width was about `1288 px`)
|
||||
- goal: make the width drift much less noticeable in normal use
|
||||
|
||||
Important:
|
||||
|
||||
- this is **not** a root-cause fix
|
||||
- it only reduces how visible the drift is by starting near the eventual grown width
|
||||
- it should be treated as a stopgap until the actual GTK/Adwaita page-transition sizing cause is understood
|
||||
|
||||
## Best Current Diagnosis
|
||||
|
||||
What is known:
|
||||
|
||||
- startup sizing had real app-side issues and was partially improved
|
||||
- the remaining drift is **not** explained by the X11 minimum-size hint changing
|
||||
- the remaining drift is **not** explained by previously visited `NavigationView` pages staying alive
|
||||
|
||||
Most likely remaining cause:
|
||||
|
||||
- a lower-level GTK/Adwaita toplevel layout negotiation behavior on page transitions
|
||||
- with XFCE preserving the window's top-left position instead of re-centering or clamping it fully back on-screen
|
||||
|
||||
In short:
|
||||
|
||||
- MangoTune still has a real app-side geometry issue
|
||||
- but the current remaining one appears deeper than a single page/widget declaring a large min width
|
||||
|
||||
## Current Recommendation
|
||||
|
||||
Do not keep churning speculative layout changes blindly.
|
||||
|
||||
If this issue is revisited later, the next investigation should focus on:
|
||||
|
||||
1. whether Adwaita `NavigationView` / `NavigationPage` transitions are causing the toplevel to renegotiate size after the page becomes visible
|
||||
2. whether GTK4 exposes a stronger post-show geometry control than `set_default_size(...)`
|
||||
3. whether this needs an explicit X11-specific geometry preservation path on page navigation
|
||||
|
||||
## Status
|
||||
|
||||
- startup placement: improved
|
||||
- page-switch width drift: unresolved
|
||||
- temporary mitigation: start the window near the widest observed page width so the drift is less visible
|
||||
- lazy-page workaround: tested and reverted
|
||||
@@ -0,0 +1,159 @@
|
||||
# MangoTune — Agent Master Plan
|
||||
|
||||
## Project Summary
|
||||
|
||||
MangoTune is a GTK4 + libadwaita desktop application written in Rust for Linux.
|
||||
It is a superior replacement for GOverlay — a GUI configurator for MangoHud,
|
||||
with first-class support for config conflict detection, strict validation, visual
|
||||
config-layer stacking (like CSS cascade), live preview via test launchers, and
|
||||
integrations with GameMode, Steam, Lutris, and Heroic Games Launcher.
|
||||
|
||||
## Agent Instructions — READ FIRST
|
||||
|
||||
1. Read this file completely before doing anything.
|
||||
2. Read `docs/architecture.md` for the full module map and dependency graph.
|
||||
3. Read `docs/mangohud_schema.md` for the complete MangoHud option reference.
|
||||
4. Read `docs/design_system.md` for all UI/UX rules and GTK4 widget patterns.
|
||||
5. Read `phases/phase_XX.md` for the specific phase you are implementing.
|
||||
6. Each module has its own spec file in `modules/`. Read the relevant spec before writing code.
|
||||
7. Never modify files outside your assigned phase without noting it in a comment block.
|
||||
8. After completing a phase, verify against the acceptance criteria in that phase file.
|
||||
|
||||
## Directory Layout (this plan repo)
|
||||
|
||||
```
|
||||
mangotune-plan/
|
||||
├── README.md ← YOU ARE HERE — read first
|
||||
├── docs/
|
||||
│ ├── architecture.md ← module map, crate deps, file tree
|
||||
│ ├── mangohud_schema.md ← every MangoHud config option, types, constraints
|
||||
│ ├── config_resolution.md ← how MangoHud config files are discovered & prioritized
|
||||
│ ├── design_system.md ← GTK4/libadwaita UI rules, widget patterns, UX decisions
|
||||
│ └── integrations.md ← GameMode, Steam, Lutris, Heroic specs
|
||||
├── modules/
|
||||
│ ├── config_parser.md ← parser/writer module spec
|
||||
│ ├── config_schema.md ← schema/type system module spec
|
||||
│ ├── config_validator.md ← validation engine module spec
|
||||
│ ├── config_resolver.md ← multi-file conflict resolver module spec
|
||||
│ ├── system_detect.md ← system detection module spec
|
||||
│ ├── launcher.md ← test launcher module spec
|
||||
│ └── ui_pages.md ← all UI page/widget specs
|
||||
├── phases/
|
||||
│ ├── phase_01.md ← Project scaffold, Cargo.toml, build system
|
||||
│ ├── phase_02.md ← Config parser + schema + validator (no UI)
|
||||
│ ├── phase_03.md ← Config resolver + system detection (no UI)
|
||||
│ ├── phase_04.md ← GTK4 app skeleton, main window, navigation
|
||||
│ ├── phase_05.md ← Performance & GPU/CPU pages (core UI)
|
||||
│ ├── phase_06.md ← Appearance, layout, colors, typography pages
|
||||
│ ├── phase_07.md ← Conflict resolver UI (cascade stack view)
|
||||
│ ├── phase_08.md ← Keybindings, logging, FPS limits pages
|
||||
│ ├── phase_09.md ← Test launcher (vkcube, glxgears, custom)
|
||||
│ ├── phase_10.md ← Integrations (GameMode, Steam, Lutris, Heroic)
|
||||
│ └── phase_11.md ← Polish, packaging, .desktop file, final QA
|
||||
└── ui/
|
||||
├── widget_toggle.md ← toggle switch widget spec
|
||||
├── widget_color.md ← color picker widget spec
|
||||
├── widget_hotkey.md ← hotkey capture widget spec
|
||||
├── widget_cascade.md ← config cascade/layer stack widget spec
|
||||
└── widget_validation.md ← inline validation error display spec
|
||||
```
|
||||
|
||||
## Target Source Tree (the actual Rust project)
|
||||
|
||||
```
|
||||
mangotune/
|
||||
├── Cargo.toml
|
||||
├── Cargo.lock
|
||||
├── build.rs
|
||||
├── data/
|
||||
│ ├── com.mangotune.MangoTune.gschema.xml
|
||||
│ ├── com.mangotune.MangoTune.desktop
|
||||
│ └── icons/
|
||||
│ └── com.mangotune.MangoTune.svg
|
||||
├── src/
|
||||
│ ├── main.rs ← entry point only — app init + run
|
||||
│ ├── app.rs ← Application struct, GtkApplication setup
|
||||
│ ├── window.rs ← MainWindow: AdwApplicationWindow
|
||||
│ ├── config/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── parser.rs ← read/write .conf files, preserve comments
|
||||
│ │ ├── schema.rs ← typed schema: all ~120 MangoHud options
|
||||
│ │ ├── validator.rs ← validation logic, dependency checks
|
||||
│ │ ├── resolver.rs ← discover all config files, build priority stack
|
||||
│ │ └── types.rs ← shared enums/structs (ConfigValue, OptionType, etc.)
|
||||
│ ├── system/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── detect.rs ← detect MangoHud version, tools, GPU, display server
|
||||
│ │ └── paths.rs ← XDG path resolution, known config locations
|
||||
│ ├── launcher/
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── runner.rs ← spawn vkcube/glxgears/custom with MANGOHUD=1
|
||||
│ ├── integrations/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── gamemode.rs
|
||||
│ │ ├── steam.rs
|
||||
│ │ ├── lutris.rs
|
||||
│ │ └── heroic.rs
|
||||
│ └── ui/
|
||||
│ ├── mod.rs
|
||||
│ ├── pages/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── overview.rs
|
||||
│ │ ├── performance.rs
|
||||
│ │ ├── gpu.rs
|
||||
│ │ ├── cpu.rs
|
||||
│ │ ├── memory.rs
|
||||
│ │ ├── io_network.rs
|
||||
│ │ ├── media_player.rs
|
||||
│ │ ├── battery.rs
|
||||
│ │ ├── appearance.rs
|
||||
│ │ ├── colors.rs
|
||||
│ │ ├── typography.rs
|
||||
│ │ ├── keybindings.rs
|
||||
│ │ ├── fps_limits.rs
|
||||
│ │ ├── logging.rs
|
||||
│ │ ├── blacklist.rs
|
||||
│ │ ├── opengl_quirks.rs
|
||||
│ │ ├── raw_editor.rs
|
||||
│ │ └── conflicts.rs
|
||||
│ └── widgets/
|
||||
│ ├── mod.rs
|
||||
│ ├── toggle_row.rs
|
||||
│ ├── color_row.rs
|
||||
│ ├── hotkey_row.rs
|
||||
│ ├── cascade_view.rs
|
||||
│ ├── validation_label.rs
|
||||
│ └── launch_bar.rs
|
||||
```
|
||||
|
||||
## Core Design Principles
|
||||
|
||||
- **Strict validation** — the Save button is disabled if any field contains an invalid value.
|
||||
Inline error labels appear below each offending field. No config is written to disk in an
|
||||
invalid state under any circumstances.
|
||||
- **Comment preservation** — the parser round-trips existing files preserving all comments,
|
||||
whitespace, and ordering. Only changed lines are modified.
|
||||
- **Dependency awareness** — the schema encodes option dependencies (e.g. `gpu_mem_clock`
|
||||
requires `vram=1`). Enabling a dependent option auto-enables its parent and shows a notice.
|
||||
- **Config cascade visibility** — the conflict page shows all detected config files as a
|
||||
vertical stack ordered by MangoHud's actual priority (env > app-local > per-app XDG > global).
|
||||
Each option that is overridden shows which file wins and which files are shadowed.
|
||||
- **No hardcoded paths** — all paths resolve via XDG spec, `$HOME`, and runtime detection.
|
||||
- **Graceful degradation** — if MangoHud is not installed, the app opens but shows a clear
|
||||
install prompt. Missing optional tools (vkcube, glxgears, GameMode) are indicated per-feature,
|
||||
not as global errors.
|
||||
|
||||
## Key Differentiators vs GOverlay
|
||||
|
||||
| Feature | GOverlay | MangoTune |
|
||||
|-------------------------------|----------------|---------------|
|
||||
| Config conflict detection | None | Visual cascade|
|
||||
| Option dependency validation | None | Full schema |
|
||||
| Comment preservation | Destroys them | Preserved |
|
||||
| Multi-file editing | Global only | All layers |
|
||||
| Strict type validation | None | Blocks save |
|
||||
| Wayland support | Broken | Native GTK4 |
|
||||
| HiDPI | Buggy | GTK4 native |
|
||||
| XDG compliance | Fixed in v1.7 | From day one |
|
||||
| Toolkit | Qt (AppImage) | GTK4/Adwaita |
|
||||
| Live test launcher | vkcube only | vkcube+glx+custom |
|
||||
@@ -0,0 +1,230 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,184 @@
|
||||
# Architecture — MangoTune
|
||||
|
||||
## Rust Edition & MSRV
|
||||
|
||||
- Rust edition: **2021**
|
||||
- Minimum Supported Rust Version: **1.75.0**
|
||||
- Build target: `x86_64-unknown-linux-gnu` (primary), `aarch64-unknown-linux-gnu` (secondary)
|
||||
|
||||
## Cargo.toml Dependencies
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "mangotune"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["MangoTune Contributors"]
|
||||
description = "A modern MangoHud configurator for Linux"
|
||||
license = "GPL-3.0"
|
||||
repository = "https://github.com/your-org/mangotune"
|
||||
|
||||
[[bin]]
|
||||
name = "mangotune"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# GUI
|
||||
gtk4 = { version = "0.9", features = ["v4_12"] }
|
||||
libadwaita = { version = "0.7", features = ["v1_5"] }
|
||||
glib = "0.20"
|
||||
gio = "0.20"
|
||||
|
||||
# Async runtime (for subprocess management, file watching)
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "process", "fs", "sync", "time"] }
|
||||
|
||||
# Serialization (for GSettings schema, internal state)
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# Config file parsing
|
||||
indexmap = "2" # preserve insertion order in parsed configs
|
||||
|
||||
# Error handling
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
|
||||
# Filesystem watching (live reload when config changes externally)
|
||||
notify = "6"
|
||||
|
||||
# XDG base directory resolution
|
||||
xdg = "2"
|
||||
|
||||
# Regex (for config line parsing)
|
||||
regex = "1"
|
||||
once_cell = "1"
|
||||
|
||||
# Process detection (checking if gamemode is running, etc.)
|
||||
sysinfo = "0.31"
|
||||
|
||||
# Logging/tracing
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[build-dependencies]
|
||||
# For compiling GSettings schema and other build-time assets
|
||||
glib-build-tools = "0.20"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
assert_fs = "1"
|
||||
```
|
||||
|
||||
## System Dependencies (must be present at build time)
|
||||
|
||||
| Package | Ubuntu/Debian | Fedora/RHEL | Arch |
|
||||
|--------------------------|---------------------------|---------------------------------|----------------|
|
||||
| GTK4 dev headers | `libgtk-4-dev` | `gtk4-devel` | `gtk4` |
|
||||
| libadwaita dev headers | `libadwaita-1-dev` | `libadwaita-devel` | `libadwaita` |
|
||||
| GLib dev headers | `libglib2.0-dev` | `glib2-devel` | `glib2` |
|
||||
| pkg-config | `pkg-config` | `pkgconf` | `pkgconf` |
|
||||
|
||||
Runtime (optional, detected at launch):
|
||||
- `mangohud` — the actual overlay
|
||||
- `vkcube` — from `vulkan-tools` package
|
||||
- `glxgears` — from `mesa-utils` package
|
||||
- `gamemoded` — from `gamemode` package
|
||||
- `gamemodectl` — from `gamemode` package
|
||||
|
||||
## Module Dependency Graph
|
||||
|
||||
```
|
||||
main.rs
|
||||
└── app.rs (GtkApplication)
|
||||
└── window.rs (AdwApplicationWindow)
|
||||
├── ui/pages/*.rs ← all pages
|
||||
│ ├── config/resolver.rs ← discovers config stack
|
||||
│ ├── config/validator.rs ← validates on every change
|
||||
│ ├── config/parser.rs ← reads/writes files
|
||||
│ └── config/schema.rs ← option definitions
|
||||
├── ui/widgets/*.rs ← reusable widgets
|
||||
├── system/detect.rs ← run at startup
|
||||
├── system/paths.rs ← XDG resolution
|
||||
├── launcher/runner.rs ← test process management
|
||||
└── integrations/*.rs ← GameMode/Steam/Lutris/Heroic
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
Startup:
|
||||
system::detect::run()
|
||||
→ SystemInfo { mangohud_version, gpu_vendor, display_server, available_tools }
|
||||
|
||||
config::resolver::discover()
|
||||
→ Vec<ConfigLayer> { path, source_type, priority, exists }
|
||||
|
||||
For each ConfigLayer:
|
||||
config::parser::read(path)
|
||||
→ RawConfig { lines: Vec<ConfigLine> }
|
||||
|
||||
config::schema::annotate(raw)
|
||||
→ AnnotatedConfig { options: IndexMap<key, ConfigEntry> }
|
||||
|
||||
User edits a field:
|
||||
ui::widgets::* emits change signal
|
||||
→ config::validator::check(key, value, &schema)
|
||||
→ ValidationResult::Ok | ValidationResult::Error(msg) | ValidationResult::Warning(msg)
|
||||
|
||||
If Ok: update in-memory AnnotatedConfig
|
||||
check for dependency side-effects
|
||||
update cascade_view to show which layer owns the value
|
||||
enable Save button if no errors anywhere
|
||||
|
||||
Save:
|
||||
config::validator::check_all(&config) ← full pass before any write
|
||||
→ if any Error: abort, show error summary toast
|
||||
→ if all Ok: config::parser::write(path, config)
|
||||
preserving all comment lines unchanged
|
||||
```
|
||||
|
||||
## Threading Model
|
||||
|
||||
- **Main thread**: GTK4 event loop only. No blocking calls.
|
||||
- **Tokio thread pool**: file I/O, subprocess spawning, filesystem watcher.
|
||||
- Communication: `glib::MainContext::channel()` for sending results back to GTK main thread.
|
||||
- Never call GTK functions from tokio threads.
|
||||
|
||||
## GSettings Schema
|
||||
|
||||
Used for persisting app preferences (window size, last-opened config path, theme preference).
|
||||
NOT used for MangoHud config itself — that is always written directly to .conf files.
|
||||
|
||||
Schema ID: `com.mangotune.MangoTune`
|
||||
Keys:
|
||||
- `last-config-path` (string)
|
||||
- `window-width` (int, default 1200)
|
||||
- `window-height` (int, default 780)
|
||||
- `active-page` (string, default "performance")
|
||||
- `show-raw-editor` (bool, default false)
|
||||
|
||||
## Error Handling Strategy
|
||||
|
||||
- All I/O operations return `anyhow::Result`.
|
||||
- UI layer converts errors to `AdwToast` notifications (non-blocking).
|
||||
- Critical startup errors (GTK init failure) use `eprintln!` + `process::exit(1)`.
|
||||
- Validation errors are `thiserror` enums, displayed inline, never panicked on.
|
||||
- Never use `.unwrap()` or `.expect()` in production paths. Use `?` or match.
|
||||
|
||||
## Config File Format Notes
|
||||
|
||||
MangoHud .conf files follow these rules (the parser must handle all of them):
|
||||
1. Lines starting with `#` are comments — preserve verbatim.
|
||||
2. Empty lines — preserve verbatim.
|
||||
3. `key=value` — option with value.
|
||||
4. `key` alone (no `=`) — boolean flag, presence = enabled.
|
||||
5. `# key` — commented-out option (disabled).
|
||||
6. `# key=value` — commented-out option with default value shown.
|
||||
7. Inline comments after values are NOT standard and should be treated as part of value.
|
||||
8. Duplicate keys: last occurrence wins (MangoHud behavior).
|
||||
9. Encoding: UTF-8.
|
||||
|
||||
The parser must distinguish between:
|
||||
- An option that is absent from the file (use MangoHud's compiled default)
|
||||
- An option explicitly set to 0 or empty (user explicitly disabled)
|
||||
- An option present as a bare key (user enabled a flag)
|
||||
@@ -0,0 +1,149 @@
|
||||
# Config Resolution & Priority System
|
||||
|
||||
## MangoHud's Config Priority Order (highest to lowest)
|
||||
|
||||
When MangoHud loads, it resolves configuration from multiple sources. Later sources
|
||||
override earlier ones. **Highest priority wins for any given option.**
|
||||
|
||||
```
|
||||
Priority 5 (HIGHEST) — Environment variable override
|
||||
$MANGOHUD_CONFIG="key=value,key2=value2"
|
||||
Also: $MANGOHUD_CONFIGFILE="/path/to/custom.conf"
|
||||
|
||||
Priority 4 — App-local config (same directory as the game executable)
|
||||
{game_directory}/MangoHud.conf
|
||||
|
||||
Priority 3 — Per-app XDG config (named after the process)
|
||||
$XDG_CONFIG_HOME/MangoHud/{appname}.conf
|
||||
(default: ~/.config/MangoHud/{appname}.conf)
|
||||
|
||||
Priority 2 — Global XDG user config
|
||||
$XDG_CONFIG_HOME/MangoHud/MangoHud.conf
|
||||
(default: ~/.config/MangoHud/MangoHud.conf)
|
||||
|
||||
Priority 1 (LOWEST) — MangoHud compiled defaults
|
||||
(no file, built into the library)
|
||||
```
|
||||
|
||||
## Discovery Algorithm for `config::resolver`
|
||||
|
||||
```
|
||||
fn discover() -> Vec<ConfigLayer>:
|
||||
|
||||
1. Check environment:
|
||||
a. Read $MANGOHUD_CONFIGFILE — if set and path exists, record as Priority 5b
|
||||
b. Read $MANGOHUD_CONFIG — if set, parse inline key=value pairs as Priority 5a
|
||||
Note: 5a overrides 5b which overrides all file-based configs
|
||||
|
||||
2. Determine XDG config home:
|
||||
a. Use $XDG_CONFIG_HOME if set and non-empty
|
||||
b. Otherwise use $HOME/.config
|
||||
c. If neither available: warn and skip file-based discovery
|
||||
|
||||
3. Enumerate known config files in priority order:
|
||||
a. {XDG_CONFIG_HOME}/MangoHud/MangoHud.conf (global)
|
||||
b. {XDG_CONFIG_HOME}/MangoHud/*.conf (all per-app configs found)
|
||||
c. Scan common game directories for MangoHud.conf:
|
||||
- $HOME/.steam/steam/steamapps/common/*/
|
||||
- $HOME/.local/share/Steam/steamapps/common/*/
|
||||
- $HOME/Games/*/
|
||||
- $HOME/.var/app/com.valvesoftware.Steam/data/Steam/steamapps/common/*/
|
||||
(Flatpak Steam)
|
||||
|
||||
4. For each discovered config file:
|
||||
- Record: path, source_type, priority_rank, file_exists, last_modified
|
||||
- Parse if exists
|
||||
|
||||
5. Build conflict map:
|
||||
For each option key found in more than one layer:
|
||||
- Record which layer provides the winning value
|
||||
- Record which layers are shadowed
|
||||
- Mark as "conflict" in the UI
|
||||
```
|
||||
|
||||
## ConfigLayer Struct
|
||||
|
||||
```rust
|
||||
pub struct ConfigLayer {
|
||||
pub path: Option<PathBuf>, // None for env-var inline configs
|
||||
pub source_type: LayerSource,
|
||||
pub priority: u8, // 1=lowest (compiled default) to 5=highest (env)
|
||||
pub exists: bool,
|
||||
pub is_editable: bool, // false for env-var layers
|
||||
pub last_modified: Option<SystemTime>,
|
||||
pub config: Option<AnnotatedConfig>,
|
||||
}
|
||||
|
||||
pub enum LayerSource {
|
||||
CompiledDefault,
|
||||
GlobalXdg, // ~/.config/MangoHud/MangoHud.conf
|
||||
PerAppXdg(String), // ~/.config/MangoHud/{appname}.conf — stores appname
|
||||
AppLocal(PathBuf), // {game_dir}/MangoHud.conf
|
||||
EnvFile(PathBuf), // $MANGOHUD_CONFIGFILE
|
||||
EnvInline(String), // $MANGOHUD_CONFIG inline value
|
||||
}
|
||||
```
|
||||
|
||||
## Conflict Detection Rules
|
||||
|
||||
A **conflict** exists when:
|
||||
- An option is explicitly set in two or more layers with different values.
|
||||
- OR an env var (`$MANGOHUD_CONFIG` or `$MANGOHUD_CONFIGFILE`) is set AND any file-based config also sets the same option — the env always wins but the user may not realize it.
|
||||
|
||||
A **shadow** occurs when:
|
||||
- A lower-priority layer sets an option that a higher-priority layer also sets.
|
||||
The lower-priority setting is "shadowed" (has no effect at runtime).
|
||||
|
||||
## UI: Visual Cascade (CSS Specificity Style)
|
||||
|
||||
The Conflicts page (`ui/pages/conflicts.rs`) renders a vertical stack of layers,
|
||||
highest priority at top. For each layer:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔴 ENV: $MANGOHUD_CONFIG [not editable]│
|
||||
│ gpu_stats=0 fps_limit=120 text_color=FF0000 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 🟡 Per-App: ~/.config/MangoHud/cs2.conf [Edit] │
|
||||
│ fps_limit=60 ← SHADOWED by ENV above │
|
||||
│ gpu_temp=1 cpu_temp=1 ram=1 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 🟢 Global: ~/.config/MangoHud/MangoHud.conf [Edit] │
|
||||
│ fps_limit=0 ← SHADOWED by cs2.conf and ENV │
|
||||
│ font_size=24 position=top-left background_alpha=0.5 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Color coding:
|
||||
- 🔴 Red badge = env var override (cannot edit in app, show value only)
|
||||
- 🟡 Yellow badge = per-app or app-local config
|
||||
- 🟢 Green badge = global config
|
||||
- Grey strikethrough text = shadowed (ineffective) option
|
||||
|
||||
Clicking an option in any layer:
|
||||
- If editable layer: jumps to that option in the config editor with that layer selected
|
||||
- If env-var layer: shows tooltip explaining how to unset the env var
|
||||
|
||||
## Creating New Config Files
|
||||
|
||||
When user clicks "+ New Config":
|
||||
1. Ask: Global, Per-App (enter app name), or App-Local (browse for directory)?
|
||||
2. If Per-App: validate app name (alphanumeric + hyphens/underscores only)
|
||||
3. Create file with header comment block:
|
||||
```
|
||||
### MangoHud configuration - managed by MangoTune
|
||||
### Created: {date}
|
||||
### App: {appname or "global"}
|
||||
```
|
||||
4. Add to resolver's layer stack immediately.
|
||||
5. Set as the active editing target.
|
||||
|
||||
## Config File Write Safety
|
||||
|
||||
Before writing any config to disk:
|
||||
1. Run full validation pass — abort if any errors.
|
||||
2. Create a backup: `{original_path}.mangotune.bak` (overwrite if exists).
|
||||
3. Write to `{original_path}.mangotune.tmp`.
|
||||
4. On success: atomically rename tmp → original.
|
||||
5. On failure: restore from backup, show error toast.
|
||||
6. Never write a partial/corrupt file.
|
||||
@@ -0,0 +1,341 @@
|
||||
# Design System — MangoTune GTK4 / libadwaita
|
||||
|
||||
## Guiding Principles
|
||||
|
||||
1. **Libadwaita first** — use `Adw::*` widgets wherever they exist before falling back to GTK4.
|
||||
This ensures correct dark/light mode, accent color, and GNOME HIG compliance automatically.
|
||||
2. **Every field validates on change** — instant inline feedback, never wait for save.
|
||||
3. **Save button state is truth** — it is only sensitive when all fields are valid AND there
|
||||
are unsaved changes. It is insensitive otherwise. Never disable fields; always show why.
|
||||
4. **Contextual help is inline** — use `subtitle` on `AdwActionRow` for brief descriptions.
|
||||
Longer explanations go in an `AdwTooltip`. No separate help dialogs.
|
||||
5. **Destructive actions require confirmation** — deleting a config file uses `AdwAlertDialog`.
|
||||
|
||||
---
|
||||
|
||||
## Main Window Structure
|
||||
|
||||
```
|
||||
AdwApplicationWindow "MangoTune"
|
||||
AdwToolbarView
|
||||
┌── [top] AdwHeaderBar
|
||||
│ Title: "MangoTune"
|
||||
│ Start: AdwSplitButton "Save" (primary action)
|
||||
│ End: menu button (gear icon → preferences, about)
|
||||
│
|
||||
├── [top] ConfigBarWidget (custom, below header)
|
||||
│ Shows: current file being edited
|
||||
│ Dropdown: select from all discovered config layers
|
||||
│ Conflict indicator: if any conflicts detected
|
||||
│
|
||||
└── [content] AdwOverlaySplitView
|
||||
Sidebar: NavigationSidebar (AdwNavigationSidebar or custom ListBox)
|
||||
Content: AdwNavigationView (manages page stack)
|
||||
```
|
||||
|
||||
## Header Bar
|
||||
|
||||
- Title: "MangoTune"
|
||||
- Subtitle: name of current config file being edited (short path)
|
||||
- Primary button: `AdwSplitButton` labeled "Save"
|
||||
- Main click: save current file
|
||||
- Dropdown arrow: "Save As…", "Revert to Saved", "Create Backup"
|
||||
- End: `Gtk::MenuButton` with gear icon
|
||||
- Menu items: Preferences, Keyboard Shortcuts, About MangoTune
|
||||
|
||||
## Config File Selector Bar
|
||||
|
||||
Custom widget rendered between HeaderBar and the sidebar/content split.
|
||||
Appearance: an `AdwBanner` variant or custom `GtkBox` with background `@card_bg_color`.
|
||||
|
||||
Contents (left to right):
|
||||
- Icon indicating layer type (globe for global, app icon for per-app, warning for env)
|
||||
- Dropdown (`GtkDropDown`) showing all discovered layers with their priority
|
||||
- Conflict badge: `GtkLabel` with `.error` or `.warning` CSS class if conflicts exist
|
||||
- Right side: "View All Layers" button → navigates to Conflicts page
|
||||
|
||||
Layer display format in dropdown:
|
||||
```
|
||||
[●] ~/.config/MangoHud/MangoHud.conf (global)
|
||||
[◉] ~/.config/MangoHud/cs2.conf (per-app: cs2) ← currently editing
|
||||
[⚠] $MANGOHUD_CONFIG (env override — read only)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sidebar Navigation
|
||||
|
||||
Use `AdwNavigationSidebar` if available in libadwaita 1.4+, otherwise `GtkListBox` with
|
||||
`.navigation-sidebar` CSS class.
|
||||
|
||||
Sections (use `GtkSeparator` between groups):
|
||||
|
||||
**Config**
|
||||
- Overview (house icon)
|
||||
- Layer Conflicts (warning icon, badge with conflict count if > 0)
|
||||
|
||||
**Display**
|
||||
- Performance (speedometer icon)
|
||||
- GPU (chip icon)
|
||||
- CPU (cpu icon)
|
||||
- Memory (memory icon)
|
||||
- I/O & Network (network icon)
|
||||
- Media Player (music note icon)
|
||||
- Battery (battery icon)
|
||||
|
||||
**Appearance**
|
||||
- Layout & Position (layout icon)
|
||||
- Colors & Theme (palette icon)
|
||||
- Typography (text icon)
|
||||
|
||||
**Behavior**
|
||||
- Keybindings (keyboard icon)
|
||||
- FPS Limits (gauge icon)
|
||||
- Logging (file icon)
|
||||
- Blacklist (block icon)
|
||||
|
||||
**Advanced**
|
||||
- OpenGL Quirks (warning icon)
|
||||
- Raw Editor (code icon)
|
||||
|
||||
**Tools**
|
||||
- Test Launcher (play icon)
|
||||
- Integrations (plugin icon)
|
||||
|
||||
---
|
||||
|
||||
## Page Layout Pattern
|
||||
|
||||
Every config page follows this structure:
|
||||
|
||||
```
|
||||
AdwPreferencesPage
|
||||
title: "GPU Metrics"
|
||||
icon-name: "processor-symbolic" (or custom)
|
||||
|
||||
AdwPreferencesGroup
|
||||
title: "GPU Statistics"
|
||||
description: "Core GPU monitoring options"
|
||||
|
||||
AdwSwitchRow ← for Flag/Bool options
|
||||
AdwSpinRow ← for Int options
|
||||
AdwEntryRow ← for String/Path options
|
||||
AdwComboRow ← for Enum options
|
||||
AdwExpanderRow ← for groups with sub-options (e.g. load color thresholds)
|
||||
└── nested rows inside
|
||||
|
||||
AdwPreferencesGroup
|
||||
title: "Advanced"
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Widget Patterns Per Option Type
|
||||
|
||||
### Flag / Bool → `AdwSwitchRow`
|
||||
```
|
||||
AdwSwitchRow {
|
||||
title: "GPU Temperature",
|
||||
subtitle: "Show GPU core temperature (gpu_temp)",
|
||||
active: <bool>,
|
||||
}
|
||||
```
|
||||
On toggle: validate, update model, check dependencies.
|
||||
|
||||
### Int with range → `AdwSpinRow`
|
||||
```
|
||||
AdwSpinRow {
|
||||
title: "Font Size",
|
||||
subtitle: "font_size — valid range: 8–72",
|
||||
value: 24.0,
|
||||
adjustment: Gtk::Adjustment { lower: 8, upper: 72, step-increment: 1 },
|
||||
}
|
||||
```
|
||||
|
||||
### Float with range → `AdwSpinRow` (digits: 2 or 3)
|
||||
|
||||
### Enum → `AdwComboRow`
|
||||
```
|
||||
AdwComboRow {
|
||||
title: "HUD Position",
|
||||
subtitle: "position",
|
||||
model: StringList ["top-left", "top-right", "bottom-left", ...],
|
||||
}
|
||||
```
|
||||
|
||||
### String (free text) → `AdwEntryRow`
|
||||
```
|
||||
AdwEntryRow {
|
||||
title: "Custom GPU Label",
|
||||
text: "",
|
||||
// validation on ::changed signal
|
||||
}
|
||||
```
|
||||
Validation error: add `.error` CSS class to the row, set subtitle to error message.
|
||||
|
||||
### Path → `AdwEntryRow` + browse button
|
||||
```
|
||||
AdwActionRow {
|
||||
title: "Font File",
|
||||
AdwEntryRow + GtkButton "Browse…"
|
||||
}
|
||||
```
|
||||
Browse opens `GtkFileDialog` filtered to `.ttf,.otf`.
|
||||
Validate path exists after selection.
|
||||
|
||||
### Color → `AdwActionRow` with color swatch button
|
||||
```
|
||||
AdwActionRow {
|
||||
title: "GPU Color",
|
||||
subtitle: "gpu_color — hex RRGGBB",
|
||||
[suffix] GtkButton (color swatch, shows current color)
|
||||
→ opens AdwDialog with color picker
|
||||
→ also shows a GtkEntry for manual hex input
|
||||
}
|
||||
```
|
||||
|
||||
### Hotkey / Keybind → Custom `KeybindRow` widget
|
||||
```
|
||||
AdwActionRow {
|
||||
title: "Toggle HUD",
|
||||
subtitle: "toggle_hud",
|
||||
[suffix] GtkShortcutLabel (shows current binding)
|
||||
[suffix] GtkButton "Edit" → opens capture dialog
|
||||
}
|
||||
```
|
||||
Capture dialog: fullscreen-ish `AdwDialog`, listens for keypress, shows "Press a key combination…",
|
||||
captures and validates the combination, shows preview, OK/Cancel.
|
||||
|
||||
### CommaSeparatedStrings (controlled set) → `AdwExpanderRow` with checkboxes
|
||||
Example: `graphs`, `font_glyph_ranges`, `device_battery`
|
||||
```
|
||||
AdwExpanderRow {
|
||||
title: "Graphs",
|
||||
subtitle: "Select which graphs to display",
|
||||
[child per valid value] AdwSwitchRow or CheckButton row
|
||||
}
|
||||
```
|
||||
|
||||
### CommaSeparatedStrings (free) → `AdwEntryRow` with validation
|
||||
Example: `blacklist`, `network`
|
||||
|
||||
### FpsLimitList → Custom widget
|
||||
A `GtkFlowBox` of chips showing current FPS values (0, 30, 60, etc.)
|
||||
with + button to add and × to remove each. Each value validated as non-negative int.
|
||||
|
||||
---
|
||||
|
||||
## Inline Validation Display
|
||||
|
||||
When a field has an error:
|
||||
1. The `AdwActionRow` or `AdwEntryRow` gets `.error` CSS class applied.
|
||||
2. The row's subtitle changes to the error message (red text via `.error` on a child label).
|
||||
3. A validation summary appears at top of page: `AdwBanner` with "N fields have errors — fix to enable saving".
|
||||
4. The Save button in the header becomes insensitive.
|
||||
|
||||
When a dependency warning fires (e.g. user enables `gpu_mem_clock` without `vram`):
|
||||
1. Show `AdwAlertDialog`: "Enabling 'GPU Memory Clock' also requires 'VRAM display' to be enabled. Enable it now?"
|
||||
2. Buttons: "Enable Both" (suggested-action), "Cancel".
|
||||
|
||||
---
|
||||
|
||||
## Conflict/Layer Cascade Page
|
||||
|
||||
This is the most distinctive page in the app.
|
||||
|
||||
Layout: vertical stack of `AdwPreferencesGroup` cards, one per discovered layer,
|
||||
ordered top-to-bottom = highest-to-lowest priority.
|
||||
|
||||
Each layer card header shows:
|
||||
- Priority badge (e.g. "ENV", "PER-APP", "GLOBAL") with color coding
|
||||
- File path or env var name
|
||||
- "Edit" button (disabled for env layers)
|
||||
- "Open in Files" button (for file layers)
|
||||
|
||||
Inside each layer card: a `GtkListBox` showing every option set in that layer.
|
||||
Options that are shadowed by a higher-priority layer:
|
||||
- Shown with strikethrough text
|
||||
- A label "overridden by {LAYER}" in muted color
|
||||
|
||||
Options that are unique to this layer (no conflict): normal display.
|
||||
Options that this layer wins on (it overrides lower layers): bold text.
|
||||
|
||||
Filter bar at top of page:
|
||||
- "Show all options" / "Show conflicts only" / "Show shadowed only" toggle buttons
|
||||
|
||||
---
|
||||
|
||||
## Test Launcher Panel
|
||||
|
||||
Shown as a persistent bottom bar (collapsed by default) OR as a dedicated page.
|
||||
Decision: dedicated page (cleaner, avoids layout complications).
|
||||
|
||||
Layout:
|
||||
```
|
||||
AdwPreferencesPage "Test Launcher"
|
||||
AdwPreferencesGroup "Quick Test"
|
||||
description: "Launch a test application with MangoHud active to preview your config"
|
||||
|
||||
AdwActionRow "vkcube (Vulkan)"
|
||||
subtitle: "vulkan-tools — tests Vulkan overlay"
|
||||
[suffix] status: "installed" / "not found"
|
||||
[suffix] GtkButton "Launch"
|
||||
|
||||
AdwActionRow "glxgears (OpenGL)"
|
||||
subtitle: "mesa-utils — tests OpenGL overlay"
|
||||
[suffix] status indicator
|
||||
[suffix] GtkButton "Launch"
|
||||
|
||||
AdwActionRow "Custom Application"
|
||||
subtitle: "Launch any app with MangoHud injected"
|
||||
[suffix] GtkEntry (command)
|
||||
[suffix] GtkButton "Launch"
|
||||
|
||||
AdwPreferencesGroup "Launch Options"
|
||||
AdwSwitchRow "Auto-reload config on save"
|
||||
subtitle: "Sends SIGUSR1 to running MangoHud processes on save"
|
||||
AdwSwitchRow "Show terminal output"
|
||||
subtitle: "Opens a terminal window showing app stdout/stderr"
|
||||
|
||||
AdwPreferencesGroup "Running Process"
|
||||
(only visible when a test process is active)
|
||||
AdwActionRow showing process name + PID
|
||||
[suffix] GtkButton "Stop"
|
||||
```
|
||||
|
||||
When Launch is clicked:
|
||||
1. Check tool is installed (which vkcube, which glxgears).
|
||||
2. If not found: `AdwToast` "vkcube not found. Install vulkan-tools package."
|
||||
3. If found: spawn process with `MANGOHUD=1 MANGOHUD_CONFIGFILE={current_path} {command}`.
|
||||
4. Show running process row.
|
||||
5. Monitor process — remove row when it exits.
|
||||
|
||||
---
|
||||
|
||||
## Theming
|
||||
|
||||
- Follow system theme (light/dark) automatically via libadwaita.
|
||||
- Do NOT hardcode colors. Use only named GTK/Adwaita CSS variables:
|
||||
`@accent_color`, `@destructive_color`, `@warning_color`, `@success_color`,
|
||||
`@card_bg_color`, `@window_bg_color`, `@headerbar_bg_color`, etc.
|
||||
- MangoTune-specific CSS: only for the cascade view layer badges and color swatch button.
|
||||
Place in `data/style.css`, loaded at runtime via `GtkCssProvider`.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
- All interactive widgets must have accessible labels.
|
||||
- Color information must never be the sole indicator of state (always pair with icon or text).
|
||||
- Keyboard navigation must work for all pages (GTK4 handles most of this by default).
|
||||
- Use `gtk_accessible_update_property` where needed for dynamic content.
|
||||
|
||||
---
|
||||
|
||||
## Window Size & Responsiveness
|
||||
|
||||
- Default: 1200 × 780
|
||||
- Minimum: 900 × 600
|
||||
- The `AdwOverlaySplitView` collapses the sidebar at narrow widths (< 980px) automatically.
|
||||
- Persist window size via GSettings `window-width` / `window-height`.
|
||||
@@ -0,0 +1,274 @@
|
||||
# Integrations Spec
|
||||
|
||||
## Overview
|
||||
|
||||
MangoTune implements four integrations accessible from the Integrations page.
|
||||
Each integration is independent — a missing tool shows a "not available" state
|
||||
for that section only, without affecting the rest of the app.
|
||||
|
||||
---
|
||||
|
||||
## 1. GameMode Integration
|
||||
|
||||
**What is GameMode?** A daemon by Feral Interactive that applies CPU governor,
|
||||
scheduler, and I/O priority optimizations when games run. MangoHud can display
|
||||
whether GameMode is currently active via the `gamemode=1` config option.
|
||||
|
||||
### Detection
|
||||
```rust
|
||||
// src/integrations/gamemode.rs
|
||||
|
||||
pub struct GameModeStatus {
|
||||
pub daemon_installed: bool, // gamemoded binary found
|
||||
pub ctl_installed: bool, // gamemodectl binary found
|
||||
pub daemon_running: bool, // gamemoded process in process list
|
||||
pub current_clients: u32, // number of active gamemoded clients
|
||||
}
|
||||
|
||||
fn detect() -> GameModeStatus
|
||||
```
|
||||
Detection steps:
|
||||
1. `which gamemoded` — sets `daemon_installed`
|
||||
2. `which gamemodectl` — sets `ctl_installed`
|
||||
3. Check process list via `sysinfo` for `gamemoded` — sets `daemon_running`
|
||||
4. If ctl installed: run `gamemodectl status` and parse client count
|
||||
|
||||
### UI (on Integrations page)
|
||||
```
|
||||
AdwPreferencesGroup "GameMode"
|
||||
description: "Feral Interactive GameMode performance optimization daemon"
|
||||
|
||||
AdwActionRow "Status"
|
||||
subtitle: "gamemoded process"
|
||||
[suffix] label: "Running (3 clients)" / "Stopped" / "Not installed"
|
||||
|
||||
AdwSwitchRow "Show GameMode status in overlay"
|
||||
subtitle: "Sets gamemode=1 in current config"
|
||||
(bound to the gamemode config option)
|
||||
|
||||
AdwActionRow "Enable GameMode for all Steam games"
|
||||
subtitle: "Adds %command% to default Steam launch options helper"
|
||||
[suffix] GtkButton "Configure"
|
||||
```
|
||||
|
||||
### No direct daemon control
|
||||
MangoTune does NOT start/stop gamemoded. It only shows status and helps the user
|
||||
configure the launch options. Provide a tooltip: "Start/stop GameMode via your
|
||||
system service manager (systemctl --user start gamemoded)."
|
||||
|
||||
---
|
||||
|
||||
## 2. Steam Launch Option Helper
|
||||
|
||||
**Purpose:** Generate the correct launch option string for Steam games so that
|
||||
MangoHud (and optionally GameMode) is injected automatically.
|
||||
|
||||
### Detection
|
||||
```rust
|
||||
pub struct SteamStatus {
|
||||
pub installed: bool,
|
||||
pub flatpak: bool, // true if running as Flatpak
|
||||
pub running: bool,
|
||||
pub steam_root: Option<PathBuf>,
|
||||
pub localconfig_path: Option<PathBuf>,
|
||||
}
|
||||
```
|
||||
Detection steps:
|
||||
1. Check `which steam` and `flatpak list | grep com.valvesoftware.Steam`
|
||||
2. Find Steam root:
|
||||
- Native: `~/.steam/steam/` or `~/.local/share/Steam/`
|
||||
- Flatpak: `~/.var/app/com.valvesoftware.Steam/.steam/steam/`
|
||||
3. Find `userdata/{userId}/config/localconfig.vdf`
|
||||
|
||||
### Launch Option Generator UI
|
||||
```
|
||||
AdwPreferencesGroup "Steam Launch Options"
|
||||
|
||||
AdwComboRow "Inject method"
|
||||
options: [
|
||||
"mangohud {command}", ← standard
|
||||
"MANGOHUD=1 %command%", ← env var method
|
||||
"MANGOHUD_CONFIGFILE=~/.config/... %command%", ← explicit config
|
||||
"gamemoderun mangohud %command%", ← with GameMode
|
||||
"gamemoderun mangemoderun mangohud %command%", ← with GameMode (flatpak)
|
||||
]
|
||||
|
||||
AdwEntryRow (read-only)
|
||||
title: "Generated launch option"
|
||||
[Shows generated string based on above selection]
|
||||
[suffix] GtkButton "Copy to clipboard"
|
||||
|
||||
AdwActionRow "Instructions"
|
||||
subtitle: "In Steam: right-click game → Properties → Launch Options → paste above"
|
||||
```
|
||||
|
||||
### Important note for Flatpak Steam
|
||||
If Flatpak Steam is detected, the generated command uses the correct Flatpak-aware
|
||||
prefix and warns the user that MangoHud must also be installed inside the Flatpak
|
||||
sandbox or as a Flatpak extension.
|
||||
|
||||
### DO NOT write to localconfig.vdf
|
||||
MangoTune does NOT modify Steam's localconfig.vdf directly — too fragile and risky.
|
||||
The user copies the generated string manually. This is deliberate and safe.
|
||||
|
||||
---
|
||||
|
||||
## 3. Lutris Integration
|
||||
|
||||
**Purpose:** Help users configure MangoHud for games managed by Lutris.
|
||||
|
||||
### Detection
|
||||
```rust
|
||||
pub struct LutrisStatus {
|
||||
pub installed: bool,
|
||||
pub flatpak: bool,
|
||||
pub config_dir: Option<PathBuf>, // ~/.config/lutris/
|
||||
pub games: Vec<LutrisGame>,
|
||||
}
|
||||
|
||||
pub struct LutrisGame {
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub config_path: PathBuf, // ~/.config/lutris/games/{slug}.yml
|
||||
pub runner: String,
|
||||
}
|
||||
```
|
||||
Detection steps:
|
||||
1. `which lutris` or `flatpak list | grep net.lutris.Lutris`
|
||||
2. Enumerate `~/.config/lutris/games/*.yml` — parse YAML for name, slug, runner fields.
|
||||
Use a simple line-by-line parser (avoid heavy YAML dep — these files are simple).
|
||||
|
||||
### UI
|
||||
```
|
||||
AdwPreferencesGroup "Lutris"
|
||||
|
||||
AdwActionRow "Status"
|
||||
[suffix] "Installed" / "Not found"
|
||||
|
||||
(if installed):
|
||||
AdwComboRow "Game"
|
||||
[lists all detected Lutris games]
|
||||
|
||||
AdwSwitchRow "Enable MangoHud for selected game"
|
||||
subtitle: "Adds mangohud to the game's Lutris runner configuration"
|
||||
|
||||
AdwActionRow "Open game config in Lutris"
|
||||
[suffix] GtkButton "Open Lutris"
|
||||
|
||||
AdwPreferencesGroup (informational)
|
||||
AdwActionRow
|
||||
subtitle: "MangoTune can enable MangoHud in Lutris game configs.
|
||||
Per-game MangoHud config files will be placed at:
|
||||
~/.config/MangoHud/{game-slug}.conf"
|
||||
[suffix] GtkButton "Create per-game config"
|
||||
```
|
||||
|
||||
### Config modification approach for Lutris
|
||||
When "Enable MangoHud for selected game" is toggled ON:
|
||||
1. Read `~/.config/lutris/games/{slug}.yml`
|
||||
2. Find or create the `system:` section
|
||||
3. Set `mangohud: true` under the `system:` key
|
||||
4. Write back (preserve all other content, modify only the mangohud line)
|
||||
5. Show toast: "MangoHud enabled for {game name}. Restart Lutris if it's open."
|
||||
|
||||
When toggled OFF: set `mangohud: false`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Heroic Games Launcher Integration
|
||||
|
||||
**Purpose:** Help users configure MangoHud for games managed by Heroic (Epic Games,
|
||||
GOG, and Amazon Prime on Linux).
|
||||
|
||||
### Detection
|
||||
```rust
|
||||
pub struct HeroicStatus {
|
||||
pub installed: bool,
|
||||
pub flatpak: bool,
|
||||
pub config_dir: Option<PathBuf>,
|
||||
pub games: Vec<HeroicGame>,
|
||||
}
|
||||
|
||||
pub struct HeroicGame {
|
||||
pub title: String,
|
||||
pub app_name: String, // Heroic's internal ID
|
||||
pub store: HeroicStore, // Epic, GOG, Amazon
|
||||
pub config_path: PathBuf, // ~/.config/heroic/GamesConfig/{app_name}.json
|
||||
}
|
||||
|
||||
pub enum HeroicStore { Epic, Gog, Amazon }
|
||||
```
|
||||
Detection steps:
|
||||
1. `which heroic` or `flatpak list | grep com.heroicgameslauncher.hgl`
|
||||
2. Config dirs to check:
|
||||
- Native: `~/.config/heroic/`
|
||||
- Flatpak: `~/.var/app/com.heroicgameslauncher.hgl/config/heroic/`
|
||||
3. Enumerate `GamesConfig/*.json` — each file = one game config.
|
||||
4. Parse JSON: extract `title`, `appName`, store type from file content.
|
||||
|
||||
### Heroic Game Config JSON structure (relevant fields)
|
||||
```json
|
||||
{
|
||||
"appName": "AppId",
|
||||
"title": "Game Title",
|
||||
"enviromentOptions": [
|
||||
{ "key": "MANGOHUD", "value": "1" }
|
||||
],
|
||||
"wrapperOptions": [
|
||||
{ "exe": "mangohud", "args": "" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### UI
|
||||
```
|
||||
AdwPreferencesGroup "Heroic Games Launcher"
|
||||
|
||||
AdwActionRow "Status"
|
||||
[suffix] "Installed (Flatpak)" / "Not found"
|
||||
|
||||
AdwComboRow "Game"
|
||||
[lists games grouped by store if > 5 games]
|
||||
|
||||
AdwSwitchRow "Enable MangoHud via wrapper"
|
||||
subtitle: "Adds mangohud as a wrapper in Heroic game settings"
|
||||
|
||||
AdwSwitchRow "Enable MangoHud via environment"
|
||||
subtitle: "Sets MANGOHUD=1 in game environment variables"
|
||||
|
||||
AdwActionRow "Per-game config"
|
||||
subtitle: "~/.config/MangoHud/{app_name}.conf"
|
||||
[suffix] GtkButton "Create / Edit"
|
||||
```
|
||||
|
||||
### Config modification approach for Heroic
|
||||
When "Enable MangoHud via wrapper" is toggled ON:
|
||||
1. Read `~/.config/heroic/GamesConfig/{app_name}.json` (handle Flatpak path too)
|
||||
2. Parse JSON using `serde_json`
|
||||
3. Add `{ "exe": "mangohud", "args": "" }` to `wrapperOptions` if not present
|
||||
4. Write back with pretty-printing
|
||||
5. Show toast: "MangoHud wrapper enabled. Restart Heroic if it's open."
|
||||
|
||||
When toggled OFF: remove the mangohud entry from `wrapperOptions`.
|
||||
|
||||
Environment method similarly adds/removes `{ "key": "MANGOHUD", "value": "1" }`.
|
||||
|
||||
---
|
||||
|
||||
## Integration Page Layout
|
||||
|
||||
```
|
||||
AdwPreferencesPage "Integrations"
|
||||
icon-name: "insert-object-symbolic"
|
||||
|
||||
[One AdwPreferencesGroup per integration, as described above]
|
||||
|
||||
AdwPreferencesGroup "Global MangoHud Enable"
|
||||
description: "Enable MangoHud system-wide for all applications"
|
||||
AdwExpanderRow "Auto-enable method"
|
||||
AdwSwitchRow "Via ~/.config/environment.d/mangohud.conf (recommended, user-scoped)"
|
||||
AdwSwitchRow "Via ~/.bashrc (shell sessions only)"
|
||||
AdwSwitchRow "Via /etc/environment (system-wide, requires sudo)"
|
||||
[Shows currently active method with green checkmark]
|
||||
[Warning: "System-wide enable may break some applications. Per-game is preferred."]
|
||||
```
|
||||
@@ -0,0 +1,439 @@
|
||||
# MangoHud Schema Reference
|
||||
|
||||
Superseded as the user-facing/help-source reference by
|
||||
[`docs/MANGOHUD_OPTION_BEHAVIOR.md`](/home/aaron/Programming/mangotune/docs/MANGOHUD_OPTION_BEHAVIOR.md).
|
||||
Keep this file only as schema-planning context for `src/config/schema.rs`.
|
||||
|
||||
Source: Official MangoHud repository `data/MangoHud.conf` cross-referenced with
|
||||
the MangoHud README and source code. Last verified against MangoHud 0.7.x.
|
||||
|
||||
This file is no longer the authoritative help text source and does not currently
|
||||
cover every upstream MangoHud option.
|
||||
|
||||
## Schema Entry Structure
|
||||
|
||||
Each option in `schema.rs` must encode:
|
||||
```rust
|
||||
pub struct SchemaEntry {
|
||||
pub key: &'static str,
|
||||
pub option_type: OptionType,
|
||||
pub default: DefaultValue,
|
||||
pub description: &'static str,
|
||||
pub category: Category,
|
||||
pub dependencies: &'static [&'static str], // options that must be enabled for this to work
|
||||
pub conflicts_with: &'static [&'static str], // mutually exclusive options
|
||||
pub gpu_vendor_only: Option<GpuVendor>, // None = all, Some(Amd) = AMD only
|
||||
pub gamescope_only: bool,
|
||||
pub mangoapp_only: bool,
|
||||
}
|
||||
|
||||
pub enum OptionType {
|
||||
Flag, // bare key, no value — presence means enabled
|
||||
Bool, // key=0 or key=1
|
||||
Int { min: i64, max: i64 },
|
||||
Float { min: f64, max: f64 },
|
||||
String { max_len: usize },
|
||||
Color, // 6-char hex RRGGBB, no #
|
||||
Enum { variants: &'static [&'static str] },
|
||||
FpsLimitList, // comma-separated ints e.g. "0,30,60"
|
||||
KeyBind, // e.g. "Shift_R+F12"
|
||||
CommaSeparatedInts,
|
||||
CommaSeparatedFloats,
|
||||
CommaSeparatedStrings,
|
||||
Path, // filesystem path, validated to exist or be writable
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category: PERFORMANCE
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `fps_limit` | FpsLimitList | `0` | 0 = unlimited; comma-separated list e.g. `0,30,60` |
|
||||
| `fps_limit_method` | Enum[`early`,`late`] | `""` | early = wait before present |
|
||||
| `vsync` | Int[-1..3] | `-1` | -1=unset; 0=adaptive; 1=off; 2=mailbox; 3=on |
|
||||
| `gl_vsync` | Int[-2..N] | `-2` | OpenGL only; -2=unset; 0=off; >=1=wait N vblanks |
|
||||
| `picmip` | Int[-17..16] | `-17` | Mip-map LoD bias; negative=sharper |
|
||||
| `af` | Int[-1..16] | `-1` | Anisotropic filtering; -1=unset |
|
||||
| `bicubic` | Flag | absent | Force bicubic filtering |
|
||||
| `trilinear` | Flag | absent | Force trilinear filtering |
|
||||
| `retro` | Flag | absent | Disable linear filtering (blocky textures) |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — FPS & FRAMETIME
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `fps` | Flag | present | Show FPS counter — enabled by default |
|
||||
| `fps_only` | Flag | absent | CONFLICTS WITH: all other display params |
|
||||
| `fps_sampling_period` | Int[100..60000] | `500` | ms |
|
||||
| `fps_color_change` | Flag | absent | Enable FPS color thresholds |
|
||||
| `fps_value` | CommaSeparatedInts | `30,60` | Two thresholds: warn,ok |
|
||||
| `fps_color` | CommaSeparatedStrings | `B22222,FDFD09,39F900` | Three hex colors: low,mid,high |
|
||||
| `fps_text` | String | `""` | Custom label for FPS row |
|
||||
| `fps_metrics` | CommaSeparatedStrings | `""` | e.g. `avg,0.01,1,97` — `AVG` or decimal percentiles |
|
||||
| `frametime` | Flag | present | Show frametime — enabled by default |
|
||||
| `frame_count` | Flag | absent | Show frame counter |
|
||||
| `frame_timing` | Flag | present | Frametime graph — enabled by default |
|
||||
| `frame_timing_detailed` | Flag | absent | More detailed frametime graph |
|
||||
| `dynamic_frame_timing` | Flag | absent | Dynamic scale frametime graph |
|
||||
| `histogram` | Flag | absent | CONFLICTS WITH frame_timing |
|
||||
| `throttling_status` | Flag | present | GPU throttling indicator |
|
||||
| `throttling_status_graph` | Flag | absent | Show throttling on frametime graph |
|
||||
| `show_fps_limit` | Flag | absent | Display current FPS limit value |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — GPU
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `gpu_stats` | Flag | present | Master GPU section toggle |
|
||||
| `gpu_temp` | Flag | absent | |
|
||||
| `gpu_junction_temp` | Flag | absent | |
|
||||
| `gpu_core_clock` | Flag | absent | |
|
||||
| `gpu_mem_temp` | Flag | absent | DEPENDS ON: `vram` |
|
||||
| `gpu_mem_clock` | Flag | absent | DEPENDS ON: `vram` |
|
||||
| `gpu_power` | Flag | absent | |
|
||||
| `gpu_power_limit` | Flag | absent | |
|
||||
| `gpu_text` | String[32] | `""` | Custom GPU label |
|
||||
| `gpu_load_change` | Flag | absent | Color GPU load |
|
||||
| `gpu_load_value` | CommaSeparatedInts | `60,90` | Two load thresholds |
|
||||
| `gpu_load_color` | CommaSeparatedStrings | `39F900,FDFD09,B22222` | Three hex colors |
|
||||
| `gpu_fan` | Flag | absent | RPM on AMD, percent on NVIDIA |
|
||||
| `gpu_voltage` | Flag | absent | AMD ONLY |
|
||||
| `gpu_list` | CommaSeparatedInts | `""` | Select GPUs by index e.g. `0,1` |
|
||||
| `gpu_efficiency` | Flag | absent | |
|
||||
| `gpu_name` | Flag | absent | Show GPU model name |
|
||||
| `vulkan_driver` | Flag | absent | Show Vulkan driver string |
|
||||
| `engine_version` | Flag | absent | |
|
||||
| `engine_short_names` | Flag | absent | |
|
||||
| `present_mode` | Flag | absent | |
|
||||
| `pci_dev` | String | `""` | Format: `domain:bus:slot.function` e.g. `0000:03:00.0` |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — CPU
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `cpu_stats` | Flag | present | Master CPU section toggle |
|
||||
| `cpu_temp` | Flag | absent | |
|
||||
| `cpu_power` | Flag | absent | |
|
||||
| `cpu_text` | String[32] | `""` | Custom CPU label |
|
||||
| `cpu_mhz` | Flag | absent | Average MHz across cores |
|
||||
| `cpu_load_change` | Flag | absent | Color CPU load |
|
||||
| `cpu_load_value` | CommaSeparatedInts | `60,90` | Two thresholds |
|
||||
| `cpu_load_color` | CommaSeparatedStrings | `39F900,FDFD09,B22222` | Three hex colors |
|
||||
| `cpu_efficiency` | Flag | absent | |
|
||||
| `core_load` | Flag | absent | Per-core load bars |
|
||||
| `core_load_change` | Flag | absent | Color per-core load |
|
||||
| `core_bars` | Flag | absent | Graphical core bars |
|
||||
| `core_type` | Flag | absent | Show P/E core type labels |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — MEMORY
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `vram` | Flag | absent | Required by gpu_mem_clock, gpu_mem_temp |
|
||||
| `ram` | Flag | absent | |
|
||||
| `swap` | Flag | absent | |
|
||||
| `procmem` | Flag | absent | Per-process resident memory |
|
||||
| `procmem_shared` | Flag | absent | DEPENDS ON: `procmem` |
|
||||
| `procmem_virt` | Flag | absent | DEPENDS ON: `procmem` |
|
||||
| `proc_vram` | Flag | absent | Per-process VRAM |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — I/O & NETWORK
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `io_read` | Flag | absent | Per-app I/O read |
|
||||
| `io_write` | Flag | absent | Per-app I/O write |
|
||||
| `network` | CommaSeparatedStrings | `""` | Network interfaces e.g. `eth0,wlo1`; empty = all |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — MISC INDICATORS
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `wine` | Flag | absent | Wine/Proton version |
|
||||
| `winesync` | Flag | absent | Wine sync method |
|
||||
| `exec_name` | Flag | absent | Show executable name |
|
||||
| `arch` | Flag | absent | MangoHud architecture |
|
||||
| `gamemode` | Flag | absent | GameMode running status |
|
||||
| `vkbasalt` | Flag | absent | vkBasalt running status |
|
||||
| `engine_version` | Flag | absent | |
|
||||
| `version` | Flag | absent | Show MangoHud version in overlay |
|
||||
| `resolution` | Flag | absent | Current display resolution |
|
||||
| `display_server` | Flag | absent | Wayland/X11 indicator |
|
||||
| `temp_fahrenheit` | Flag | absent | Use °F instead of °C |
|
||||
| `flip_efficiency` | Flag | absent | Joules per frame |
|
||||
| `fex_stats` | Flag | absent | FEX-Emu stats (ARM64 only) |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — GRAPHS
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `graphs` | CommaSeparatedStrings | `""` | Valid values: `gpu_load,cpu_load,gpu_core_clock,gpu_mem_clock,vram,ram,cpu_temp,gpu_temp` |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — BATTERY
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `battery` | Flag | absent | |
|
||||
| `battery_icon` | Flag | absent | DEPENDS ON: `battery` |
|
||||
| `device_battery` | CommaSeparatedStrings | `""` | e.g. `gamepad,mouse` |
|
||||
| `device_battery_icon` | Flag | absent | |
|
||||
| `battery_watt` | Flag | absent | DEPENDS ON: `battery` |
|
||||
| `battery_time` | Flag | absent | DEPENDS ON: `battery` |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — MEDIA PLAYER
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `media_player` | Flag | absent | Enable media player metadata |
|
||||
| `media_player_name` | String | `""` | e.g. `spotify` — DEPENDS ON: `media_player` |
|
||||
| `media_player_format` | String | `""` | e.g. `{title};{artist};{album}` — DEPENDS ON: `media_player` |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — GAMESCOPE
|
||||
|
||||
All options in this category: `gamescope_only = true`
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `fsr` | Flag | absent | FSR status |
|
||||
| `hide_fsr_sharpness` | Flag | absent | DEPENDS ON: `fsr` |
|
||||
| `hdr` | Flag | absent | HDR status |
|
||||
| `refresh_rate` | Flag | absent | Current refresh rate |
|
||||
| `debug` | Flag | absent | Gamescope app frametimes graph |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — STEAM DECK
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `fan` | Flag | absent | Steam Deck fan RPM |
|
||||
| `mangoapp_steam` | Flag | absent | mangoapp only |
|
||||
|
||||
---
|
||||
|
||||
## Category: DISPLAY — TIME & MISC TEXT
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `time` | Flag | absent | Current time |
|
||||
| `time_no_label` | Flag | absent | DEPENDS ON: `time` |
|
||||
| `time_format` | String | `"%T"` | strftime format |
|
||||
| `custom_text_center` | String | `""` | Centered header text |
|
||||
| `custom_text` | String | `""` | Custom text line |
|
||||
| `exec` | String | `""` | Shell command — output shown in next column |
|
||||
|
||||
---
|
||||
|
||||
## Category: APPEARANCE — LAYOUT
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `legacy_layout` | Bool | `0` | |
|
||||
| `preset` | Int[-1..4] | `-1` | -1=default; 0=off; 1=fps only; 2=horizontal; 3=extended; 4=high detail |
|
||||
| `full` | Flag | absent | Enable most toggleable params |
|
||||
| `no_display` | Flag | absent | Start hidden |
|
||||
| `horizontal` | Flag | absent | Horizontal layout |
|
||||
| `horizontal_stretch` | Flag | absent | DEPENDS ON: `horizontal` |
|
||||
| `hud_compact` | Flag | absent | Compact mode |
|
||||
| `hud_no_margin` | Flag | absent | Remove margins |
|
||||
| `position` | Enum[`top-left`,`top-right`,`bottom-left`,`bottom-right`,`top-center`,`middle-left`,`middle-right`,`bottom-center`] | `top-left` | |
|
||||
| `offset_x` | Int[0..9999] | `0` | pixels |
|
||||
| `offset_y` | Int[0..9999] | `0` | pixels |
|
||||
| `width` | Int[0..9999] | `0` | 0 = auto |
|
||||
| `height` | Int[0..9999] | `140` | |
|
||||
| `table_columns` | Int[1..10] | `3` | |
|
||||
| `cellpadding_y` | Float[-2.0..2.0] | `-0.085` | |
|
||||
| `round_corners` | Int[0..50] | `0` | |
|
||||
| `background_alpha` | Float[0.0..1.0] | `0.5` | |
|
||||
| `alpha` | Float[0.0..1.0] | `1.0` | Overall HUD transparency |
|
||||
|
||||
---
|
||||
|
||||
## Category: APPEARANCE — COLORS
|
||||
|
||||
All color values: 6-char hex string RRGGBB (no `#`). Validate: must match `^[0-9A-Fa-f]{6}$`.
|
||||
|
||||
| Key | Default |
|
||||
|-----|---------|
|
||||
| `text_color` | `FFFFFF` |
|
||||
| `gpu_color` | `2E9762` |
|
||||
| `cpu_color` | `2E97CB` |
|
||||
| `vram_color` | `AD64C1` |
|
||||
| `ram_color` | `C26693` |
|
||||
| `engine_color` | `EB5B5B` |
|
||||
| `io_color` | `A491D3` |
|
||||
| `frametime_color` | `00FF00` |
|
||||
| `background_color` | `020202` |
|
||||
| `media_player_color` | `FFFFFF` |
|
||||
| `wine_color` | `EB5B5B` |
|
||||
| `battery_color` | `FF9078` |
|
||||
| `network_color` | `E07B85` |
|
||||
| `horizontal_separator_color` | `AD64C1` |
|
||||
|
||||
Also:
|
||||
| Key | Type | Default |
|
||||
|-----|------|---------|
|
||||
| `text_outline` | Flag | present (default on) |
|
||||
| `text_outline_color` | Color | `000000` |
|
||||
| `text_outline_thickness` | Float[0.5..5.0] | `1.5` |
|
||||
|
||||
---
|
||||
|
||||
## Category: APPEARANCE — TYPOGRAPHY
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `font_size` | Int[8..72] | `24` | |
|
||||
| `font_scale` | Float[0.1..5.0] | `1.0` | |
|
||||
| `font_size_text` | Int[8..72] | `24` | For text elements |
|
||||
| `font_scale_media_player` | Float[0.1..5.0] | `0.55` | |
|
||||
| `no_small_font` | Flag | absent | Disable small font for secondary info |
|
||||
| `font_file` | Path | `""` | TTF/OTF path — validated to exist if set |
|
||||
| `font_file_text` | Path | `""` | |
|
||||
| `font_glyph_ranges` | CommaSeparatedStrings | `""` | Valid: `korean,chinese,chinese_simplified,japanese,cyrillic,thai,vietnamese,latin_ext_a,latin_ext_b` |
|
||||
|
||||
---
|
||||
|
||||
## Category: BEHAVIOR — KEYBINDINGS
|
||||
|
||||
All keybind values: format is `Key` or `Modifier+Key` or `Mod1+Mod2+Key`.
|
||||
Valid modifiers: `Shift_L`, `Shift_R`, `Control_L`, `Control_R`, `Alt_L`, `Alt_R`, `Super_L`, `Super_R`.
|
||||
|
||||
| Key | Default |
|
||||
|-----|---------|
|
||||
| `toggle_hud` | `Shift_R+F12` |
|
||||
| `toggle_hud_position` | `Shift_R+F11` |
|
||||
| `toggle_preset` | `Shift_R+F10` |
|
||||
| `toggle_fps_limit` | `Shift_L+F1` |
|
||||
| `toggle_logging` | `Shift_L+F2` |
|
||||
| `reload_cfg` | `Shift_L+F4` |
|
||||
| `upload_log` | `Shift_L+F3` |
|
||||
| `reset_fps_metrics` | `Shift_R+F9` |
|
||||
|
||||
---
|
||||
|
||||
## Category: BEHAVIOR — LOGGING
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `autostart_log` | Int[0..3600] | absent | Seconds before auto-start |
|
||||
| `log_duration` | Int[1..86400] | absent | Seconds |
|
||||
| `log_interval` | Int[0..10000] | `0` | ms; 0 = default |
|
||||
| `output_folder` | Path | `""` | Must be writable directory |
|
||||
| `output_file` | String | `""` | |
|
||||
| `permit_upload` | Bool | `0` | Upload to flightlessmango.com |
|
||||
| `benchmark_percentiles` | CommaSeparatedStrings | `97,AVG` | |
|
||||
| `log_versioning` | Flag | absent | |
|
||||
| `upload_logs` | Flag | absent | DEPENDS ON: `permit_upload=1` |
|
||||
|
||||
---
|
||||
|
||||
## Category: BEHAVIOR — MISC
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `blacklist` | CommaSeparatedStrings | `""` | App names to suppress overlay |
|
||||
| `control` | String | `-1` | Socket name; -1=disabled; `%p` replaced with PID |
|
||||
|
||||
---
|
||||
|
||||
## Category: WORKAROUNDS — OPENGL
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `gl_size_query` | Enum[`viewport`,`scissorbox`,`disabled`] | `""` | Default: glXQueryDrawable |
|
||||
| `gl_bind_framebuffer` | Int[0..999] | absent | Rebind framebuffer before draw |
|
||||
| `gl_dont_flip` | Bool | absent | Don't swap origin for GL_UPPER_LEFT |
|
||||
|
||||
---
|
||||
|
||||
## Category: ADVANCED — FCAT
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `fcat` | Flag | absent | Enable FCAT overlay |
|
||||
| `fcat_overlay_width` | Int[20..200] | `24` | DEPENDS ON: `fcat` |
|
||||
| `fcat_screen_edge` | Int[0..3] | `0` | DEPENDS ON: `fcat` |
|
||||
|
||||
---
|
||||
|
||||
## Category: ADVANCED — FTRACE
|
||||
|
||||
| Key | Type | Notes |
|
||||
|-----|------|-------|
|
||||
| `ftrace` | String | Complex format: `type/event[+type/event2]`; validated by regex |
|
||||
|
||||
ftrace format regex: `^(histogram|linegraph|label)/[a-zA-Z0-9_]+(\/[a-zA-Z0-9_]+)?(\+(histogram|linegraph|label)/[a-zA-Z0-9_]+(\/[a-zA-Z0-9_]+)?)*$`
|
||||
|
||||
---
|
||||
|
||||
## Validation Rules Summary
|
||||
|
||||
### Cross-option dependencies (enabling B requires A to also be enabled):
|
||||
```
|
||||
gpu_mem_clock → requires vram
|
||||
gpu_mem_temp → requires vram
|
||||
hide_fsr_sharpness → requires fsr
|
||||
battery_icon → requires battery
|
||||
battery_watt → requires battery
|
||||
battery_time → requires battery
|
||||
media_player_name → requires media_player
|
||||
media_player_format → requires media_player
|
||||
procmem_shared → requires procmem
|
||||
procmem_virt → requires procmem
|
||||
upload_logs → requires permit_upload = 1
|
||||
horizontal_stretch → requires horizontal
|
||||
time_no_label → requires time
|
||||
```
|
||||
|
||||
### Mutual exclusions (A and B cannot both be active):
|
||||
```
|
||||
fps_only ↔ (any other display param)
|
||||
histogram ↔ frame_timing
|
||||
```
|
||||
|
||||
### Vendor restrictions:
|
||||
```
|
||||
gpu_voltage → AMD only (warn if non-AMD GPU detected)
|
||||
```
|
||||
|
||||
### Gamescope-only (warn if not running gamescope):
|
||||
```
|
||||
fsr, hide_fsr_sharpness, hdr, refresh_rate, debug
|
||||
```
|
||||
|
||||
### Value format validations:
|
||||
- All Color fields: must match `^[0-9A-Fa-f]{6}$`
|
||||
- All Path fields: if non-empty, path must exist (for input files) or parent dir must be writable (for output)
|
||||
- `fps_limit`: each comma-separated value must be non-negative integer
|
||||
- `font_glyph_ranges`: each value must be in the valid set
|
||||
- `graphs`: each value must be in the valid set
|
||||
- `device_battery`: each value must be in `[gamepad, mouse, controller, headset]`
|
||||
- `fps_metrics`: each value must be `AVG` or a valid decimal between 0.0 and 100.0
|
||||
- `benchmark_percentiles`: same as fps_metrics
|
||||
- `time_format`: must be a valid strftime format string
|
||||
- `pci_dev`: must match `^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$`
|
||||
- `control`: must be `-1` or a valid socket name string
|
||||
- Keybind fields: must match `^(Shift_[LR]\+|Control_[LR]\+|Alt_[LR]\+|Super_[LR]\+)*(F[1-9]|F1[0-2]|[A-Z])$`
|
||||
@@ -0,0 +1,154 @@
|
||||
# Phase 01 — Project Scaffold
|
||||
|
||||
## Goal
|
||||
Create the complete Rust project skeleton with correct Cargo.toml, build system,
|
||||
directory structure, and verify that the project compiles (empty/stub implementations).
|
||||
|
||||
## Prerequisites
|
||||
- Rust toolchain installed (rustup, cargo)
|
||||
- System dependencies installed (see docs/architecture.md — System Dependencies section)
|
||||
- Verify with: `pkg-config --exists gtk4 libadwaita-1 && echo "OK"`
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Create project
|
||||
```bash
|
||||
cargo new --bin mangotune
|
||||
cd mangotune
|
||||
```
|
||||
|
||||
### 2. Create full directory structure
|
||||
```bash
|
||||
mkdir -p src/{config,system,launcher,integrations,ui/{pages,widgets}}
|
||||
mkdir -p data/icons
|
||||
```
|
||||
|
||||
### 3. Write Cargo.toml
|
||||
Copy the exact dependency block from `docs/architecture.md` → Cargo.toml section.
|
||||
Do not add or remove any dependencies without updating docs/architecture.md.
|
||||
|
||||
### 4. Write build.rs
|
||||
```rust
|
||||
// build.rs
|
||||
fn main() {
|
||||
glib_build_tools::compile_schemas("data");
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Create GSettings schema
|
||||
File: `data/com.mangotune.MangoTune.gschema.xml`
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<schemalist>
|
||||
<schema id="com.mangotune.MangoTune" path="/com/mangotune/MangoTune/">
|
||||
<key name="last-config-path" type="s">
|
||||
<default>''</default>
|
||||
<summary>Last edited config file path</summary>
|
||||
</key>
|
||||
<key name="window-width" type="i">
|
||||
<default>1200</default>
|
||||
<summary>Window width</summary>
|
||||
</key>
|
||||
<key name="window-height" type="i">
|
||||
<default>780</default>
|
||||
<summary>Window height</summary>
|
||||
</key>
|
||||
<key name="active-page" type="s">
|
||||
<default>'performance'</default>
|
||||
<summary>Currently active sidebar page</summary>
|
||||
</key>
|
||||
<key name="show-raw-editor" type="b">
|
||||
<default>false</default>
|
||||
<summary>Whether raw editor tab is visible</summary>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
```
|
||||
|
||||
### 6. Create .desktop file
|
||||
File: `data/com.mangotune.MangoTune.desktop`
|
||||
```ini
|
||||
[Desktop Entry]
|
||||
Name=MangoTune
|
||||
Comment=MangoHud Overlay Configurator
|
||||
Exec=mangotune
|
||||
Icon=com.mangotune.MangoTune
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Settings;System;
|
||||
Keywords=MangoHud;overlay;gaming;performance;
|
||||
StartupNotify=true
|
||||
StartupWMClass=mangotune
|
||||
```
|
||||
|
||||
### 7. Create all source stub files
|
||||
Each file listed in docs/architecture.md → Target Source Tree must be created
|
||||
as a stub with correct module declarations and a `todo!()` placeholder where needed.
|
||||
|
||||
Required stub content for each module file:
|
||||
```rust
|
||||
// src/config/parser.rs
|
||||
//! MangoHud config file parser and writer.
|
||||
//! See: mangotune-plan/modules/config_parser.md for full spec.
|
||||
|
||||
pub struct Parser;
|
||||
|
||||
impl Parser {
|
||||
pub fn new() -> Self { todo!() }
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Wire up main.rs
|
||||
```rust
|
||||
// src/main.rs
|
||||
mod app;
|
||||
mod config;
|
||||
mod system;
|
||||
mod launcher;
|
||||
mod integrations;
|
||||
mod ui;
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
let app = app::MangoTuneApp::new();
|
||||
std::process::exit(app.run());
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Wire up app.rs stub
|
||||
```rust
|
||||
// src/app.rs
|
||||
use gtk4::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
|
||||
pub struct MangoTuneApp {
|
||||
app: libadwaita::Application,
|
||||
}
|
||||
|
||||
impl MangoTuneApp {
|
||||
pub fn new() -> Self {
|
||||
let app = libadwaita::Application::builder()
|
||||
.application_id("com.mangotune.MangoTune")
|
||||
.build();
|
||||
MangoTuneApp { app }
|
||||
}
|
||||
|
||||
pub fn run(&self) -> i32 {
|
||||
self.app.run().into()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10. Verify compilation
|
||||
```bash
|
||||
cargo check 2>&1
|
||||
```
|
||||
Must produce zero errors (warnings acceptable at this stage).
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] `cargo check` exits with code 0
|
||||
- [ ] All directories from the target source tree exist
|
||||
- [ ] All source stub files exist with correct module declarations
|
||||
- [ ] `data/` contains gschema.xml and .desktop file
|
||||
- [ ] `build.rs` compiles the schema without error
|
||||
- [ ] No `.unwrap()` calls in non-stub code (stubs with `todo!()` are exempt)
|
||||
@@ -0,0 +1,290 @@
|
||||
# Phase 02 — Config Parser, Schema & Validator
|
||||
|
||||
## Goal
|
||||
Implement the complete non-UI config stack: parser, typed schema, and validation engine.
|
||||
This phase has NO GTK4 code. All modules are pure Rust with full unit tests.
|
||||
|
||||
## Files to implement (fully, not stubs)
|
||||
- `src/config/types.rs`
|
||||
- `src/config/parser.rs`
|
||||
- `src/config/schema.rs`
|
||||
- `src/config/validator.rs`
|
||||
- `src/config/mod.rs`
|
||||
|
||||
---
|
||||
|
||||
## src/config/types.rs
|
||||
|
||||
Define all shared types used across the config subsystem.
|
||||
|
||||
```rust
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A single line from a MangoHud config file, preserving its original text.
|
||||
pub enum ConfigLine {
|
||||
Comment(String), // lines starting with #
|
||||
Blank, // empty lines
|
||||
Option { key: String, value: Option<String>, raw: String },
|
||||
CommentedOption { key: String, value: Option<String>, raw: String },
|
||||
}
|
||||
|
||||
/// The current state of an option in the in-memory config.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ConfigValue {
|
||||
Absent, // not present in file; use MangoHud compiled default
|
||||
Flag, // bare key with no value (presence = enabled)
|
||||
Value(String), // key=value
|
||||
Disabled, // was commented out explicitly
|
||||
}
|
||||
|
||||
/// Type system for schema entries.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum OptionType {
|
||||
Flag,
|
||||
Bool,
|
||||
Int { min: i64, max: i64 },
|
||||
Float { min: f64, max: f64 },
|
||||
Str { max_len: usize },
|
||||
Color,
|
||||
Enum { variants: Vec<String> },
|
||||
FpsLimitList,
|
||||
KeyBind,
|
||||
CommaSepInts,
|
||||
CommaSepFloats,
|
||||
CommaSepStrings { valid_values: Option<Vec<String>> },
|
||||
Path { must_exist: bool, must_be_writable: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum GpuVendor { Any, AmdOnly, NvidiaOnly, IntelOnly }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Category {
|
||||
Performance,
|
||||
DisplayFps,
|
||||
DisplayGpu,
|
||||
DisplayCpu,
|
||||
DisplayMemory,
|
||||
DisplayIoNetwork,
|
||||
DisplayMisc,
|
||||
DisplayGraphs,
|
||||
DisplayBattery,
|
||||
DisplayMediaPlayer,
|
||||
DisplayGamescope,
|
||||
DisplaySteamDeck,
|
||||
DisplayTimeText,
|
||||
AppearanceLayout,
|
||||
AppearanceColors,
|
||||
AppearanceTypography,
|
||||
BehaviorKeybindings,
|
||||
BehaviorFpsLimits,
|
||||
BehaviorLogging,
|
||||
BehaviorMisc,
|
||||
WorkaroundsOpengl,
|
||||
AdvancedFcat,
|
||||
AdvancedFtrace,
|
||||
}
|
||||
|
||||
/// A single schema entry — defines everything about one MangoHud option.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SchemaEntry {
|
||||
pub key: &'static str,
|
||||
pub option_type: OptionType,
|
||||
pub description: &'static str,
|
||||
pub category: Category,
|
||||
pub dependencies: &'static [&'static str],
|
||||
pub conflicts_with: &'static [&'static str],
|
||||
pub gpu_vendor_only: GpuVendor,
|
||||
pub gamescope_only: bool,
|
||||
}
|
||||
|
||||
/// Validation result for a single option.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ValidationResult {
|
||||
Ok,
|
||||
Warning(String), // save is allowed but issue shown
|
||||
Error(String), // save is BLOCKED
|
||||
}
|
||||
|
||||
/// The full in-memory representation of a parsed config file.
|
||||
pub struct AnnotatedConfig {
|
||||
/// Ordered list of lines as they appear in the file.
|
||||
/// Used for writing back to disk (preserves comments/order).
|
||||
pub lines: Vec<ConfigLine>,
|
||||
/// Fast lookup map: key → (line_index, current_value).
|
||||
pub options: indexmap::IndexMap<String, (usize, ConfigValue)>,
|
||||
/// Source path, if backed by a file.
|
||||
pub path: Option<PathBuf>,
|
||||
/// Whether this config has unsaved in-memory changes.
|
||||
pub dirty: bool,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## src/config/parser.rs
|
||||
|
||||
Rules to implement (from docs/architecture.md → Config File Format Notes):
|
||||
1. Lines starting with `#` → `ConfigLine::Comment` (preserve verbatim)
|
||||
2. Empty/whitespace-only lines → `ConfigLine::Blank`
|
||||
3. `key=value` → `ConfigLine::Option { key, value: Some(value) }`
|
||||
4. `key` alone → `ConfigLine::Option { key, value: None }` (flag)
|
||||
5. `# key` → `ConfigLine::CommentedOption { key, value: None }`
|
||||
6. `# key=value` → `ConfigLine::CommentedOption { key, value: Some(value) }`
|
||||
7. On duplicate keys: last occurrence wins (update options map accordingly)
|
||||
8. All parsing: UTF-8, trim trailing whitespace from values
|
||||
|
||||
### Public API
|
||||
```rust
|
||||
impl Parser {
|
||||
/// Parse a config file from disk.
|
||||
pub fn read(path: &Path) -> anyhow::Result<AnnotatedConfig>
|
||||
|
||||
/// Parse config from a string (for env var inline and tests).
|
||||
pub fn parse_str(content: &str, path: Option<PathBuf>) -> AnnotatedConfig
|
||||
|
||||
/// Write an AnnotatedConfig back to disk.
|
||||
/// - Creates backup at {path}.mangotune.bak
|
||||
/// - Writes to {path}.mangotune.tmp
|
||||
/// - Atomically renames to {path}
|
||||
/// - Returns Err if any step fails (restores from backup on failure)
|
||||
pub fn write(config: &AnnotatedConfig) -> anyhow::Result<()>
|
||||
|
||||
/// Update a specific key's value in the config lines.
|
||||
/// If key exists: update that line in-place.
|
||||
/// If key doesn't exist: append to end of file.
|
||||
/// If setting to Absent/Disabled: comment out the line.
|
||||
pub fn set_value(config: &mut AnnotatedConfig, key: &str, value: ConfigValue)
|
||||
|
||||
/// Serialize config to a string (for preview or clipboard copy).
|
||||
pub fn to_string(config: &AnnotatedConfig) -> String
|
||||
}
|
||||
```
|
||||
|
||||
### Tests required (in src/config/parser.rs at bottom, `#[cfg(test)]`)
|
||||
- Parse a file with all line types (comment, blank, key=value, bare key, commented option)
|
||||
- Round-trip: parse → write → parse → values must match
|
||||
- set_value: update existing key preserves surrounding lines
|
||||
- set_value: add new key appends at end
|
||||
- set_value: disable key comments it out
|
||||
- Duplicate key: last value wins
|
||||
- UTF-8 edge cases: file with non-ASCII comments
|
||||
|
||||
---
|
||||
|
||||
## src/config/schema.rs
|
||||
|
||||
Implement `MANGOHUD_SCHEMA: &[SchemaEntry]` — a static slice containing one entry
|
||||
per option documented in `docs/mangohud_schema.md`.
|
||||
|
||||
Every single option in that document must have a corresponding entry here.
|
||||
Count: approximately 120 entries.
|
||||
|
||||
```rust
|
||||
use once_cell::sync::Lazy;
|
||||
pub static MANGOHUD_SCHEMA: Lazy<Vec<SchemaEntry>> = Lazy::new(|| vec![
|
||||
SchemaEntry {
|
||||
key: "fps",
|
||||
option_type: OptionType::Flag,
|
||||
description: "Show FPS counter (enabled by default)",
|
||||
category: Category::DisplayFps,
|
||||
dependencies: &[],
|
||||
conflicts_with: &["fps_only"],
|
||||
gpu_vendor_only: GpuVendor::Any,
|
||||
gamescope_only: false,
|
||||
},
|
||||
// ... all ~120 entries
|
||||
]);
|
||||
|
||||
/// Fast lookup by key.
|
||||
pub fn get_schema_entry(key: &str) -> Option<&'static SchemaEntry>
|
||||
```
|
||||
|
||||
Also implement:
|
||||
```rust
|
||||
/// Return all schema entries for a given category.
|
||||
pub fn entries_for_category(category: &Category) -> Vec<&'static SchemaEntry>
|
||||
|
||||
/// Return all dependency keys for a given key (recursive).
|
||||
pub fn all_dependencies(key: &str) -> Vec<&'static str>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## src/config/validator.rs
|
||||
|
||||
```rust
|
||||
/// Validate a single value against its schema entry.
|
||||
/// Returns Ok, Warning, or Error.
|
||||
pub fn validate_value(
|
||||
key: &str,
|
||||
value: &ConfigValue,
|
||||
schema: &SchemaEntry,
|
||||
) -> ValidationResult
|
||||
|
||||
/// Validate an entire config against the full schema.
|
||||
/// Returns a map of key → ValidationResult for every key that has an issue.
|
||||
/// Keys with ValidationResult::Ok are NOT included (only problems).
|
||||
pub fn validate_all(
|
||||
config: &AnnotatedConfig,
|
||||
) -> HashMap<String, ValidationResult>
|
||||
|
||||
/// Check dependency satisfaction.
|
||||
/// Returns list of (dependent_key, missing_required_key) pairs.
|
||||
pub fn check_dependencies(config: &AnnotatedConfig) -> Vec<(String, String)>
|
||||
|
||||
/// Check for mutual exclusions.
|
||||
/// Returns list of (key_a, key_b) pairs where both are active but they conflict.
|
||||
pub fn check_conflicts(config: &AnnotatedConfig) -> Vec<(String, String)>
|
||||
|
||||
/// True if config is fully valid (no Errors). Warnings are allowed.
|
||||
pub fn is_saveable(config: &AnnotatedConfig) -> bool
|
||||
```
|
||||
|
||||
### Validation logic per type (implement all):
|
||||
- `Flag`: always valid if present; no value to validate
|
||||
- `Bool`: value must be "0" or "1"
|
||||
- `Int { min, max }`: must parse as i64, must be in [min, max]
|
||||
- `Float { min, max }`: must parse as f64, must be in [min, max]
|
||||
- `Str { max_len }`: must be ≤ max_len bytes
|
||||
- `Color`: must match regex `^[0-9A-Fa-f]{6}$`
|
||||
- `Enum { variants }`: value must be in variants list (case-sensitive)
|
||||
- `FpsLimitList`: comma-separated, each part must be non-negative integer
|
||||
- `KeyBind`: must match the keybind regex from mangohud_schema.md
|
||||
- `CommaSepInts`: each comma-part must parse as integer
|
||||
- `CommaSepFloats`: each comma-part must parse as f64
|
||||
- `CommaSepStrings { valid_values: Some(_) }`: each part must be in valid set
|
||||
- `CommaSepStrings { valid_values: None }`: any non-empty string OK
|
||||
- `Path { must_exist, must_be_writable }`: validate with std::fs
|
||||
|
||||
### Tests required
|
||||
- Each validation type: valid input, invalid input, boundary values
|
||||
- Dependency check: config with missing dependency detected
|
||||
- Conflict check: fps_only + fps = conflict detected
|
||||
- is_saveable: returns false if any Error present, true if only Warnings
|
||||
|
||||
---
|
||||
|
||||
## src/config/mod.rs
|
||||
|
||||
```rust
|
||||
pub mod types;
|
||||
pub mod parser;
|
||||
pub mod schema;
|
||||
pub mod validator;
|
||||
pub mod resolver; // stub only in this phase
|
||||
|
||||
pub use types::*;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] `cargo test --lib` passes all tests with 0 failures
|
||||
- [ ] Schema contains entries for ALL ~120 options from docs/mangohud_schema.md
|
||||
- [ ] Parser round-trips without losing comments or blank lines
|
||||
- [ ] Validator blocks saves on invalid Color, out-of-range Int, unknown Enum value
|
||||
- [ ] Dependency checker catches gpu_mem_clock without vram
|
||||
- [ ] Conflict checker catches fps_only with any other display param
|
||||
- [ ] No `.unwrap()` in production code paths
|
||||
- [ ] No `todo!()` remaining in any of the 4 implemented files
|
||||
@@ -0,0 +1,234 @@
|
||||
# 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
|
||||
@@ -0,0 +1,268 @@
|
||||
# Phase 04 — GTK4 App Skeleton & Main Window
|
||||
|
||||
## Goal
|
||||
Build the complete application shell: window, header bar, config selector bar,
|
||||
sidebar navigation, and empty page placeholders. All navigation must work.
|
||||
No option editing yet — pages show "Coming soon" content.
|
||||
|
||||
## Files to implement
|
||||
- `src/app.rs` (replace stub)
|
||||
- `src/window.rs` (replace stub)
|
||||
- `src/ui/mod.rs`
|
||||
- `src/ui/pages/mod.rs`
|
||||
- `src/ui/widgets/mod.rs`
|
||||
- `data/style.css`
|
||||
|
||||
---
|
||||
|
||||
## src/app.rs
|
||||
|
||||
```rust
|
||||
use gtk4::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use crate::window::MainWindow;
|
||||
use crate::system::detect;
|
||||
|
||||
pub struct MangoTuneApp {
|
||||
app: libadwaita::Application,
|
||||
}
|
||||
|
||||
impl MangoTuneApp {
|
||||
pub fn new() -> Self {
|
||||
let app = libadwaita::Application::builder()
|
||||
.application_id("com.mangotune.MangoTune")
|
||||
.flags(gio::ApplicationFlags::FLAGS_NONE)
|
||||
.build();
|
||||
|
||||
let app_clone = app.clone();
|
||||
app.connect_activate(move |_| {
|
||||
// Load CSS
|
||||
let provider = gtk4::CssProvider::new();
|
||||
provider.load_from_data(include_str!("../data/style.css"));
|
||||
gtk4::style_context_add_provider_for_display(
|
||||
&gdk::Display::default().expect("No display"),
|
||||
&provider,
|
||||
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
|
||||
// Run system detection async, then build window
|
||||
let ctx = glib::MainContext::default();
|
||||
ctx.spawn_local(async move {
|
||||
let system_info = detect::detect_system().await
|
||||
.unwrap_or_else(|_| detect::SystemInfo::unknown());
|
||||
let window = MainWindow::new(&app_clone, system_info);
|
||||
window.present();
|
||||
});
|
||||
});
|
||||
|
||||
MangoTuneApp { app }
|
||||
}
|
||||
|
||||
pub fn run(&self) -> i32 {
|
||||
self.app.run().into()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## src/window.rs
|
||||
|
||||
```rust
|
||||
use gtk4::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use crate::system::detect::SystemInfo;
|
||||
use crate::ui::pages;
|
||||
|
||||
pub struct MainWindow {
|
||||
pub window: libadwaita::ApplicationWindow,
|
||||
}
|
||||
|
||||
impl MainWindow {
|
||||
pub fn new(app: &libadwaita::Application, system_info: SystemInfo) -> Self {
|
||||
let window = libadwaita::ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.title("MangoTune")
|
||||
.default_width(1200)
|
||||
.default_height(780)
|
||||
.build();
|
||||
|
||||
// Restore window size from GSettings
|
||||
// Build layout:
|
||||
// AdwToolbarView
|
||||
// top: AdwHeaderBar
|
||||
// top: ConfigBarWidget (custom)
|
||||
// content: AdwOverlaySplitView
|
||||
// sidebar: navigation list
|
||||
// content: AdwNavigationView (page stack)
|
||||
|
||||
let toolbar_view = libadwaita::ToolbarView::new();
|
||||
let header = build_header_bar();
|
||||
let config_bar = build_config_bar(&system_info);
|
||||
let split_view = build_split_view(&system_info);
|
||||
|
||||
toolbar_view.add_top_bar(&header);
|
||||
toolbar_view.add_top_bar(&config_bar);
|
||||
toolbar_view.set_content(Some(&split_view));
|
||||
|
||||
window.set_content(Some(&toolbar_view));
|
||||
MainWindow { window }
|
||||
}
|
||||
|
||||
pub fn present(&self) { self.window.present(); }
|
||||
}
|
||||
```
|
||||
|
||||
### Header Bar implementation
|
||||
```
|
||||
AdwHeaderBar
|
||||
title-widget: AdwWindowTitle { title: "MangoTune", subtitle: "No config loaded" }
|
||||
start: AdwSplitButton "Save" (insensitive by default)
|
||||
dropdown items: "Save As…", "Revert to Saved", "Create Backup"
|
||||
end: GtkMenuButton (gear icon)
|
||||
popover menu: "Preferences", "Keyboard Shortcuts", "About MangoTune"
|
||||
```
|
||||
|
||||
### Config Bar implementation
|
||||
Custom `GtkBox` with `@card_bg_color` background:
|
||||
```
|
||||
GtkBox (horizontal, spacing=8, margin=6)
|
||||
GtkImage (config type icon — globe/per-app/warning)
|
||||
GtkLabel "Editing:"
|
||||
GtkDropDown ← lists all discovered config layers
|
||||
model: StringList populated from resolver
|
||||
GtkLabel "⚠ 2 conflicts" (hidden if no conflicts)
|
||||
GtkButton "View All Layers" → navigate to conflicts page
|
||||
```
|
||||
|
||||
### Split View implementation
|
||||
```
|
||||
AdwOverlaySplitView
|
||||
sidebar-width-fraction: 0.22
|
||||
min-sidebar-width: 180
|
||||
max-sidebar-width: 260
|
||||
show-sidebar: true
|
||||
collapsed at width < 980
|
||||
|
||||
sidebar: NavigationSidebar (GtkListBox with .navigation-sidebar CSS class)
|
||||
content: AdwNavigationView ← all pages pushed here
|
||||
```
|
||||
|
||||
### Navigation Sidebar
|
||||
The sidebar is a `GtkListBox` with rows grouped by sections.
|
||||
See docs/design_system.md → Sidebar Navigation for the full list of sections and items.
|
||||
|
||||
Each row stores the page ID as data. On row activation:
|
||||
- Call `navigation_view.push_by_tag(page_id)`
|
||||
- Highlight the active row
|
||||
|
||||
Section headers: `GtkLabel` with `.heading` CSS class, not selectable.
|
||||
|
||||
### Page Stubs (all pages in this phase return placeholder content)
|
||||
Create all page files listed in `src/ui/pages/` with a stub that returns:
|
||||
```rust
|
||||
pub fn build_page() -> libadwaita::PreferencesPage {
|
||||
let page = libadwaita::PreferencesPage::new();
|
||||
page.set_title("Page Name");
|
||||
let group = libadwaita::PreferencesGroup::new();
|
||||
group.set_title("Coming Soon");
|
||||
group.set_description(Some("This page will be implemented in a future phase."));
|
||||
page.add(&group);
|
||||
page
|
||||
}
|
||||
```
|
||||
|
||||
### About Dialog
|
||||
`AdwAboutDialog` with:
|
||||
```
|
||||
application-name: "MangoTune"
|
||||
application-icon: "com.mangotune.MangoTune"
|
||||
version: env!("CARGO_PKG_VERSION")
|
||||
comments: "A modern, accurate MangoHud configurator for Linux"
|
||||
license-type: Gtk::License::Gpl30
|
||||
website: "https://github.com/your-org/mangotune"
|
||||
issue-url: "https://github.com/your-org/mangotune/issues"
|
||||
developers: ["MangoTune Contributors"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## data/style.css
|
||||
|
||||
Only custom styles that libadwaita/GTK4 don't provide natively.
|
||||
|
||||
```css
|
||||
/* Config layer priority badges */
|
||||
.layer-badge-env {
|
||||
background-color: @destructive_color;
|
||||
color: @destructive_fg_color;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.layer-badge-perapp {
|
||||
background-color: @warning_color;
|
||||
color: @warning_fg_color;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.layer-badge-global {
|
||||
background-color: @success_color;
|
||||
color: @success_fg_color;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Shadowed option text in cascade view */
|
||||
.option-shadowed {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Color swatch button */
|
||||
.color-swatch-button {
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @borders;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Config bar */
|
||||
.config-bar {
|
||||
background-color: @card_bg_color;
|
||||
border-bottom: 1px solid @borders;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
/* Conflict count badge */
|
||||
.conflict-badge {
|
||||
background-color: @destructive_color;
|
||||
color: @destructive_fg_color;
|
||||
border-radius: 8px;
|
||||
padding: 1px 6px;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] App launches without crashing
|
||||
- [ ] Window appears at 1200×780
|
||||
- [ ] Header bar shows "MangoTune" title and (insensitive) Save button
|
||||
- [ ] Config bar renders below header
|
||||
- [ ] Sidebar shows all navigation sections with correct icons
|
||||
- [ ] Clicking each sidebar item navigates to its placeholder page
|
||||
- [ ] About dialog opens from gear menu
|
||||
- [ ] System detection runs on startup (check tracing log output)
|
||||
- [ ] App responds to window resize (sidebar collapses at narrow width)
|
||||
- [ ] No GTK warnings or critical messages in stderr on launch
|
||||
@@ -0,0 +1,628 @@
|
||||
# Phases 05–11 — Implementation Phases
|
||||
|
||||
---
|
||||
|
||||
# Phase 05 — Core Config Pages: Performance, GPU, CPU, Memory
|
||||
|
||||
## Goal
|
||||
Implement the four most-used config pages with full working controls,
|
||||
inline validation, and dependency handling. The Save button becomes functional.
|
||||
|
||||
## Files to implement (replace stubs)
|
||||
- `src/ui/pages/performance.rs`
|
||||
- `src/ui/pages/gpu.rs`
|
||||
- `src/ui/pages/cpu.rs`
|
||||
- `src/ui/pages/memory.rs`
|
||||
- `src/ui/widgets/toggle_row.rs` ← shared AdwSwitchRow wrapper
|
||||
- `src/ui/widgets/validation_label.rs` ← inline error display
|
||||
|
||||
## Application State Architecture
|
||||
|
||||
Before implementing pages, establish the central app state that all pages share.
|
||||
Add to `src/window.rs`:
|
||||
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
use crate::config::types::AnnotatedConfig;
|
||||
use crate::config::validator;
|
||||
|
||||
/// Shared mutable application state, passed via Arc<Mutex<>> to all pages.
|
||||
pub struct AppState {
|
||||
pub config: AnnotatedConfig,
|
||||
pub validation: HashMap<String, ValidationResult>,
|
||||
pub dirty: bool,
|
||||
}
|
||||
```
|
||||
|
||||
Use `glib::MainContext::channel()` or `Arc<Mutex<AppState>>` + GObject signals
|
||||
to communicate changes from widget callbacks to the save button sensitivity.
|
||||
|
||||
## Page Implementation Pattern
|
||||
|
||||
Every page follows this exact pattern:
|
||||
|
||||
```rust
|
||||
// src/ui/pages/gpu.rs
|
||||
|
||||
pub fn build_page(state: Arc<Mutex<AppState>>) -> libadwaita::PreferencesPage {
|
||||
let page = libadwaita::PreferencesPage::new();
|
||||
page.set_title("GPU");
|
||||
page.set_icon_name(Some("computer-symbolic"));
|
||||
|
||||
// ── GROUP: GPU Statistics ──────────────────────────────────────
|
||||
let group = libadwaita::PreferencesGroup::new();
|
||||
group.set_title("GPU Statistics");
|
||||
group.set_description(Some("Core GPU monitoring display options"));
|
||||
|
||||
// gpu_stats (master toggle)
|
||||
let gpu_stats_row = build_switch_row(
|
||||
"GPU Statistics",
|
||||
"gpu_stats — master GPU section toggle",
|
||||
"gpu_stats",
|
||||
&state,
|
||||
);
|
||||
group.add(&gpu_stats_row);
|
||||
|
||||
// gpu_temp
|
||||
let gpu_temp_row = build_switch_row("Temperature", "gpu_temp", "gpu_temp", &state);
|
||||
group.add(&gpu_temp_row);
|
||||
|
||||
// ... etc for every option in Category::DisplayGpu
|
||||
|
||||
page.add(&group);
|
||||
page
|
||||
}
|
||||
```
|
||||
|
||||
## Widget Helpers to implement in this phase
|
||||
|
||||
### `build_switch_row(title, subtitle, key, state)` → AdwSwitchRow
|
||||
1. Read current value from `state.config`
|
||||
2. Set initial active state
|
||||
3. Connect `notify::active`:
|
||||
a. Update `state.config` via `parser::set_value`
|
||||
b. Run `validator::validate_value(key, value, schema_entry)`
|
||||
c. If dependency triggered: show `AdwAlertDialog` "Enable {dep} too?"
|
||||
d. If conflict triggered: show warning toast
|
||||
e. Update save button sensitivity
|
||||
|
||||
### `build_spin_row(title, subtitle, key, min, max, state)` → AdwSpinRow
|
||||
1. Read current value, set initial
|
||||
2. Connect `notify::value`:
|
||||
a. Update state
|
||||
b. Validate
|
||||
c. Show/hide error on the row (add/remove `.error` CSS class)
|
||||
d. Update save button
|
||||
|
||||
### `build_combo_row(title, subtitle, key, variants, state)` → AdwComboRow
|
||||
|
||||
### `build_entry_row(title, subtitle, key, state)` → AdwEntryRow
|
||||
|
||||
### Validation error display on rows
|
||||
```rust
|
||||
fn set_row_error(row: &impl IsA<gtk4::Widget>, error: Option<&str>) {
|
||||
if let Some(msg) = error {
|
||||
row.add_css_class("error");
|
||||
// Update row subtitle to show error message
|
||||
} else {
|
||||
row.remove_css_class("error");
|
||||
// Restore original subtitle
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Save Button Wiring
|
||||
In `window.rs`, connect save button:
|
||||
1. Run `validator::validate_all(&state.config)` — full pass
|
||||
2. If any Error: show `AdwToast` "Fix N errors before saving", abort
|
||||
3. If all Ok: call `parser::write(&state.config)`
|
||||
4. On success: show `AdwToast` "Config saved", set `state.dirty = false`,
|
||||
make save button insensitive
|
||||
|
||||
## GPU-specific notes
|
||||
- `gpu_mem_clock` and `gpu_mem_temp`: when enabled, check if `vram` is also active.
|
||||
If not, show `AdwAlertDialog`: "GPU Memory Clock requires VRAM display to be enabled.
|
||||
Enable VRAM now?" → buttons: "Enable Both" / "Cancel".
|
||||
- `gpu_voltage`: if GPU vendor != AMD, add an `AdwBanner` warning at top of group:
|
||||
"gpu_voltage is only available on AMD GPUs. This option will have no effect."
|
||||
- Color load thresholds (gpu_load_change, gpu_load_value, gpu_load_color):
|
||||
use an `AdwExpanderRow` that expands to show threshold + color sub-rows.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] All four pages render with correct controls for every option in their category
|
||||
- [ ] Toggle switches update in-memory state immediately
|
||||
- [ ] Invalid values (e.g. font_size=999) show inline error and block save
|
||||
- [ ] Save button is insensitive when no changes or when errors exist
|
||||
- [ ] Save writes correct .conf format (key=value or bare key)
|
||||
- [ ] Comments and blank lines preserved after save
|
||||
- [ ] Dependency dialog appears when enabling gpu_mem_clock without vram
|
||||
- [ ] Vendor warning shows for gpu_voltage on non-AMD systems
|
||||
|
||||
---
|
||||
|
||||
# Phase 06 — Appearance, Colors, Typography, Layout Pages
|
||||
|
||||
## Files to implement
|
||||
- `src/ui/pages/appearance.rs`
|
||||
- `src/ui/pages/colors.rs`
|
||||
- `src/ui/pages/typography.rs`
|
||||
- `src/ui/widgets/color_row.rs`
|
||||
|
||||
## Color Row Widget (color_row.rs)
|
||||
|
||||
The color row is a custom widget used for all color options.
|
||||
|
||||
```
|
||||
AdwActionRow {
|
||||
title: "GPU Color"
|
||||
subtitle: "gpu_color — hex RRGGBB (no #)"
|
||||
[suffix] GtkButton (color swatch) ← shows current color as background
|
||||
[suffix] GtkEntry (6-char hex) ← manual entry
|
||||
}
|
||||
```
|
||||
|
||||
Color swatch button click → opens `AdwDialog`:
|
||||
```
|
||||
AdwDialog "Choose Color"
|
||||
GtkColorDialogButton ← native GTK4 color chooser
|
||||
GtkEntry showing hex ← synced with the color chooser
|
||||
[footer] GtkButton "Reset to Default"
|
||||
[footer] GtkButton "Cancel"
|
||||
[footer] GtkButton "Apply" (suggested-action)
|
||||
```
|
||||
|
||||
Validation: hex entry must match `^[0-9A-Fa-f]{6}$`. Show error inline.
|
||||
On Apply: update color swatch background, update state, validate.
|
||||
|
||||
## Colors Page
|
||||
One `AdwPreferencesGroup` per logical color section:
|
||||
- "Text & Background" (text_color, background_color, text_outline*)
|
||||
- "GPU" (gpu_color, gpu_load_color)
|
||||
- "CPU" (cpu_color, cpu_load_color)
|
||||
- "Memory" (vram_color, ram_color)
|
||||
- "Other Components" (engine_color, io_color, frametime_color, etc.)
|
||||
- "Media & Battery" (media_player_color, battery_color, wine_color, network_color)
|
||||
|
||||
At top of page: `AdwBanner` with "Tip: Colors are 6-digit hex without #. Example: FF0000 for red."
|
||||
|
||||
## Typography Page
|
||||
- font_size: AdwSpinRow (8–72)
|
||||
- font_scale: AdwSpinRow (0.1–5.0, 2 decimal digits)
|
||||
- font_size_text: AdwSpinRow
|
||||
- font_scale_media_player: AdwSpinRow
|
||||
- no_small_font: AdwSwitchRow
|
||||
- font_file: AdwEntryRow + "Browse…" button → GtkFileDialog
|
||||
- font_file_text: same
|
||||
- font_glyph_ranges: AdwExpanderRow with checkboxes for each valid range
|
||||
|
||||
## Layout & Position Page
|
||||
- position: AdwComboRow with visual position preview (simple ASCII art grid in subtitle)
|
||||
- offset_x, offset_y: AdwSpinRow
|
||||
- horizontal: AdwSwitchRow (enabling it disables position since horizontal has its own placement)
|
||||
- horizontal_stretch: AdwSwitchRow (depends on horizontal)
|
||||
- hud_compact: AdwSwitchRow
|
||||
- hud_no_margin: AdwSwitchRow
|
||||
- background_alpha: AdwSpinRow (0.0–1.0) + live preview strip showing the alpha
|
||||
- alpha: AdwSpinRow
|
||||
- width, height: AdwSpinRow
|
||||
- table_columns: AdwSpinRow (1–10)
|
||||
- cellpadding_y: AdwSpinRow (-2.0–2.0)
|
||||
- round_corners: AdwSpinRow (0–50)
|
||||
- preset: AdwComboRow with descriptions for each preset value
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] All color rows show correct color swatches
|
||||
- [ ] Invalid hex values blocked with inline error
|
||||
- [ ] File browser for font_file filters to .ttf/.otf
|
||||
- [ ] font_file path validated to exist on disk
|
||||
- [ ] horizontal_stretch disables when horizontal is off
|
||||
- [ ] All layout values saved and round-trip correctly
|
||||
|
||||
---
|
||||
|
||||
# Phase 07 — Config Conflict Cascade View Page
|
||||
|
||||
## Files to implement
|
||||
- `src/ui/pages/conflicts.rs`
|
||||
- `src/ui/widgets/cascade_view.rs`
|
||||
|
||||
## This is the most visually distinctive page in the app.
|
||||
|
||||
### Page Layout
|
||||
```
|
||||
AdwPreferencesPage "Layer Conflicts"
|
||||
|
||||
[top] GtkSearchBar + filter buttons: "All" / "Conflicts Only" / "Shadowed Only"
|
||||
|
||||
[for each layer, ordered highest priority first]:
|
||||
AdwPreferencesGroup
|
||||
title: "{layer_label}" e.g. "ENV: $MANGOHUD_CONFIG" or "~/.config/MangoHud/cs2.conf"
|
||||
header-suffix: GtkBox containing:
|
||||
- GtkLabel badge (ENV / PER-APP / GLOBAL / APP-LOCAL) with CSS class
|
||||
- GtkButton "Edit" (hidden for env layers)
|
||||
- GtkButton "Open Folder" (for file layers)
|
||||
- GtkButton "Delete Config" (destructive, requires AdwAlertDialog confirm)
|
||||
|
||||
[for each option in this layer]:
|
||||
AdwActionRow
|
||||
title: option key (monospace font)
|
||||
subtitle: option value
|
||||
[if shadowed by higher layer]:
|
||||
title gets .option-shadowed CSS class (strikethrough)
|
||||
suffix: GtkLabel "overridden by {higher_layer_name}" with .dim-label
|
||||
[if this layer wins over lower layers]:
|
||||
title: bold
|
||||
suffix: GtkImage "checkmark" icon
|
||||
|
||||
[bottom if no conflicts detected]:
|
||||
AdwStatusPage
|
||||
icon-name: "emblem-ok-symbolic"
|
||||
title: "No Conflicts"
|
||||
description: "All config layers are consistent."
|
||||
```
|
||||
|
||||
### Filter Logic
|
||||
- "All": show every layer with all their options
|
||||
- "Conflicts Only": show only layers that contain conflicting options (hidden = no conflicts)
|
||||
- "Shadowed Only": show only shadowed options across all layers
|
||||
|
||||
### Empty State (no layers found)
|
||||
```
|
||||
AdwStatusPage
|
||||
icon-name: "document-open-symbolic"
|
||||
title: "No Config Files Found"
|
||||
description: "MangoHud will use compiled defaults.\nCreate a config file to get started."
|
||||
child: GtkButton "Create Global Config" (suggested-action)
|
||||
```
|
||||
|
||||
### Clicking a key in any editable layer
|
||||
Navigate to the relevant config page with that option highlighted (scroll to it).
|
||||
Implement by passing a "highlight_key" parameter to page build functions.
|
||||
The targeted option's row briefly flashes with a CSS animation.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] All layers shown in correct priority order (highest at top)
|
||||
- [ ] ENV layers show as non-editable
|
||||
- [ ] Shadowed options have strikethrough text and "overridden by X" label
|
||||
- [ ] Winning options are visually distinct (bold)
|
||||
- [ ] Filter buttons correctly show/hide options
|
||||
- [ ] Delete config prompts for confirmation
|
||||
- [ ] Clicking a key in an editable layer navigates to its editor page
|
||||
|
||||
---
|
||||
|
||||
# Phase 08 — Keybindings, I/O, Network, Media, Battery, Logging, Misc Pages
|
||||
|
||||
## Files to implement
|
||||
- `src/ui/pages/keybindings.rs`
|
||||
- `src/ui/pages/io_network.rs`
|
||||
- `src/ui/pages/media_player.rs`
|
||||
- `src/ui/pages/battery.rs`
|
||||
- `src/ui/pages/fps_limits.rs`
|
||||
- `src/ui/pages/logging.rs`
|
||||
- `src/ui/pages/blacklist.rs`
|
||||
- `src/ui/pages/opengl_quirks.rs`
|
||||
- `src/ui/pages/raw_editor.rs`
|
||||
- `src/ui/widgets/hotkey_row.rs`
|
||||
|
||||
## Hotkey Row Widget
|
||||
|
||||
Custom widget for capturing keybindings.
|
||||
|
||||
```
|
||||
AdwActionRow
|
||||
title: "Toggle HUD"
|
||||
subtitle: "toggle_hud"
|
||||
[suffix] GtkShortcutLabel ← displays current binding e.g. "⇧R + F12"
|
||||
[suffix] GtkButton "Edit" → opens capture dialog
|
||||
[suffix] GtkButton "✕" → clears binding (sets to empty = use default)
|
||||
```
|
||||
|
||||
Capture dialog:
|
||||
```
|
||||
AdwDialog "Capture Keybind"
|
||||
AdwStatusPage
|
||||
icon-name: "input-keyboard-symbolic"
|
||||
title: "Press a key combination"
|
||||
description: "Hold modifier keys (Shift, Ctrl, Alt) then press a key"
|
||||
|
||||
[on keypress captured]:
|
||||
Shows preview: "Shift_R + F12"
|
||||
GtkButton "Accept" (suggested-action)
|
||||
GtkButton "Try Again"
|
||||
GtkButton "Cancel"
|
||||
```
|
||||
|
||||
Validation: MangoHud only supports specific modifier+key combinations.
|
||||
Valid keys: F1–F12 only (MangoHud doesn't accept alphanumeric hotkeys).
|
||||
If invalid combination captured, show error label and keep "Accept" insensitive.
|
||||
|
||||
## FPS Limits Page
|
||||
|
||||
Special widget for fps_limit (comma-separated list of FPS values):
|
||||
|
||||
```
|
||||
AdwPreferencesGroup "FPS Limit Values"
|
||||
description: "Comma-separated list. 0 = unlimited. Toggle between values with Shift_L+F1."
|
||||
|
||||
[custom widget: FpsChipList]
|
||||
GtkFlowBox showing current values as removable chips:
|
||||
[0] [30] [60] [+]
|
||||
Each chip: GtkLabel + GtkButton "×"
|
||||
"+" button: opens inline entry to add new value
|
||||
Validation: each value must be non-negative integer
|
||||
Values auto-sorted ascending when saved
|
||||
|
||||
AdwPreferencesGroup "FPS Limit Method"
|
||||
AdwComboRow "Method" (fps_limit_method: early/late/"")
|
||||
|
||||
AdwPreferencesGroup "VSync"
|
||||
AdwComboRow "Vulkan VSync" (vsync: -1/0/1/2/3 with labels)
|
||||
AdwComboRow "OpenGL VSync" (gl_vsync with labels)
|
||||
```
|
||||
|
||||
## Logging Page
|
||||
|
||||
All logging options with particular attention to:
|
||||
- `output_folder`: path must be validated as an existing writable directory
|
||||
Use AdwEntryRow + "Browse…" button → GtkFileDialog in FOLDER mode
|
||||
- `permit_upload`: when toggled off, also disable `upload_logs`
|
||||
- `output_file`: free string entry
|
||||
|
||||
## Raw Editor Page
|
||||
|
||||
A `GtkTextView` showing the current config file content as raw text.
|
||||
- Monospace font
|
||||
- Syntax highlighting: comments in muted color, keys in accent color, values in text color
|
||||
(use GtkTextTag for basic highlighting — no external syntax highlighter dep)
|
||||
- Changes in raw editor update the in-memory AnnotatedConfig via re-parsing on focus-out
|
||||
- Warning banner at top: "Changes here bypass validation. Errors may prevent MangoHud from loading."
|
||||
- Show line count and option count in footer
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Hotkey capture works and validates MangoHud-compatible combinations
|
||||
- [ ] FPS limit chips can be added and removed
|
||||
- [ ] Logging output_folder validated as writable directory
|
||||
- [ ] Raw editor shows current config content
|
||||
- [ ] Raw editor changes re-parsed on focus-out
|
||||
- [ ] All page options save correctly
|
||||
|
||||
---
|
||||
|
||||
# Phase 09 — Test Launcher
|
||||
|
||||
## Files to implement
|
||||
- `src/launcher/runner.rs` (replace stub)
|
||||
- `src/ui/pages/overview.rs` (implements the overview/dashboard)
|
||||
- (Test launcher UI is part of a dedicated page — see docs/design_system.md)
|
||||
|
||||
## runner.rs
|
||||
|
||||
```rust
|
||||
use tokio::process::{Command, Child};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct LaunchConfig {
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub config_path: PathBuf,
|
||||
pub show_terminal: bool,
|
||||
}
|
||||
|
||||
pub struct RunningProcess {
|
||||
pub child: Child,
|
||||
pub command: String,
|
||||
pub pid: u32,
|
||||
}
|
||||
|
||||
impl Runner {
|
||||
/// Check if a tool is available on PATH.
|
||||
pub fn is_available(tool: &str) -> bool
|
||||
|
||||
/// Launch a tool with MANGOHUD=1 and the specified config file.
|
||||
pub async fn launch(config: LaunchConfig) -> anyhow::Result<RunningProcess>
|
||||
|
||||
/// Stop a running process (SIGTERM, then SIGKILL after 3s).
|
||||
pub async fn stop(process: RunningProcess) -> anyhow::Result<()>
|
||||
|
||||
/// Send SIGUSR1 to reload config in a running MangoHud process.
|
||||
pub async fn reload_config(pid: u32) -> anyhow::Result<()>
|
||||
}
|
||||
```
|
||||
|
||||
Launch environment:
|
||||
```
|
||||
MANGOHUD=1
|
||||
MANGOHUD_CONFIGFILE={config_path}
|
||||
```
|
||||
|
||||
If `show_terminal=true`: spawn via `xterm -e "{command}"` or detect default terminal
|
||||
(`$TERM`, then try: `gnome-terminal`, `konsole`, `xfce4-terminal`, `xterm` in that order).
|
||||
|
||||
## Overview Page
|
||||
|
||||
Dashboard shown on first launch (app startup default page).
|
||||
```
|
||||
AdwPreferencesPage "Overview"
|
||||
|
||||
[if MangoHud not installed]:
|
||||
AdwStatusPage
|
||||
icon-name: "dialog-warning-symbolic"
|
||||
title: "MangoHud Not Found"
|
||||
description: "Install MangoHud to use this app.\n\n
|
||||
Ubuntu/Debian: sudo apt install mangohud\n
|
||||
Fedora: sudo dnf install mangohud\n
|
||||
Arch: sudo pacman -S mangohud"
|
||||
|
||||
[if MangoHud installed]:
|
||||
AdwPreferencesGroup "System Status"
|
||||
AdwActionRow "MangoHud" [suffix: version label + green checkmark]
|
||||
AdwActionRow "Display" [suffix: "Wayland" or "X11"]
|
||||
AdwActionRow "GPU" [suffix: GPU name]
|
||||
AdwActionRow "Active Config" [suffix: current config path]
|
||||
|
||||
AdwPreferencesGroup "Quick Actions"
|
||||
AdwButtonRow "Launch vkcube test" → triggers launcher
|
||||
AdwButtonRow "Open Config Folder" → xdg-open ~/.config/MangoHud/
|
||||
AdwButtonRow "View Config Conflicts" → navigate to conflicts page
|
||||
AdwButtonRow "Reset to Defaults" (destructive — AdwAlertDialog confirm)
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] vkcube launches with MANGOHUD=1 and current config path
|
||||
- [ ] glxgears launches with MANGOHUD=1
|
||||
- [ ] Custom app entry accepts any shell command
|
||||
- [ ] Running process shown with PID and Stop button
|
||||
- [ ] Stop sends SIGTERM, escalates to SIGKILL after 3s
|
||||
- [ ] "Tool not found" toast shown if vkcube/glxgears missing
|
||||
- [ ] Overview shows correct system info from detect::detect_system()
|
||||
|
||||
---
|
||||
|
||||
# Phase 10 — Integrations Page
|
||||
|
||||
## Files to implement
|
||||
- `src/integrations/gamemode.rs` (replace stub)
|
||||
- `src/integrations/steam.rs` (replace stub)
|
||||
- `src/integrations/lutris.rs` (replace stub)
|
||||
- `src/integrations/heroic.rs` (replace stub)
|
||||
- `src/ui/pages/integrations.rs` (new file, not a stub page)
|
||||
|
||||
## Implementation follows docs/integrations.md exactly.
|
||||
|
||||
## Key implementation notes:
|
||||
|
||||
### Threading
|
||||
All file I/O and process detection in integrations must run on tokio, not the GTK main thread.
|
||||
Pattern:
|
||||
```rust
|
||||
let (sender, receiver) = glib::MainContext::channel(glib::Priority::DEFAULT);
|
||||
tokio::spawn(async move {
|
||||
let status = gamemode::detect().await;
|
||||
sender.send(status).ok();
|
||||
});
|
||||
receiver.attach(None, move |status| {
|
||||
update_ui_with_status(status);
|
||||
glib::ControlFlow::Break
|
||||
});
|
||||
```
|
||||
|
||||
### Heroic JSON parsing
|
||||
Parse `~/.config/heroic/GamesConfig/*.json` using serde_json.
|
||||
Handle both native and Flatpak paths (try both, use whichever exists).
|
||||
When writing back: preserve all fields not managed by MangoTune.
|
||||
Use `serde_json::Value` for the full document to avoid losing unknown fields.
|
||||
|
||||
### Lutris YAML parsing
|
||||
Lutris game configs are simple YAML. Do NOT add a full YAML parser dependency.
|
||||
Instead, use targeted line-by-line parsing:
|
||||
- Find the `system:` section
|
||||
- Find or add `mangohud: true/false` under it
|
||||
- For reading: look for `mangohud: true` line in file
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] GameMode section shows daemon running/stopped status
|
||||
- [ ] Steam launch option generator produces correct command strings
|
||||
- [ ] Flatpak Steam detected and generates different command
|
||||
- [ ] Copy to clipboard button works for generated Steam command
|
||||
- [ ] Lutris games enumerated from ~/.config/lutris/games/*.yml
|
||||
- [ ] Enabling MangoHud for Lutris game writes correct YAML
|
||||
- [ ] Heroic games enumerated from GamesConfig/*.json
|
||||
- [ ] Enabling MangoHud for Heroic game writes correct JSON (no data loss)
|
||||
- [ ] All integration sections show "Not installed" gracefully when tool absent
|
||||
|
||||
---
|
||||
|
||||
# Phase 11 — Polish, Packaging & Final QA
|
||||
|
||||
## Goals
|
||||
Final quality pass, packaging, and ensuring the app is ready for distribution.
|
||||
|
||||
## Tasks
|
||||
|
||||
### 1. Window state persistence
|
||||
- Save/restore window width+height via GSettings
|
||||
- Save/restore last-edited config path
|
||||
- Save/restore active sidebar page
|
||||
|
||||
### 2. Keyboard shortcuts
|
||||
Register app-level shortcuts:
|
||||
- `Ctrl+S` → Save
|
||||
- `Ctrl+Z` → Undo last change (basic: revert to last saved state)
|
||||
- `Ctrl+Shift+Z` → Redo
|
||||
- `Ctrl+R` → Reload config from disk
|
||||
- `Ctrl+W` → Close (prompts if unsaved changes)
|
||||
- `Ctrl+,` → Preferences
|
||||
- `F5` → Refresh system detection
|
||||
|
||||
Show shortcuts in AdwShortcutsWindow (accessible from gear menu).
|
||||
|
||||
### 3. Unsaved changes guard
|
||||
On window close attempt with unsaved changes:
|
||||
```
|
||||
AdwAlertDialog "Unsaved Changes"
|
||||
body: "You have unsaved changes to {config_name}. What would you like to do?"
|
||||
buttons:
|
||||
"Discard Changes" (destructive)
|
||||
"Cancel"
|
||||
"Save" (suggested-action)
|
||||
```
|
||||
|
||||
### 4. External config change detection
|
||||
Use the `notify` crate to watch config files for changes.
|
||||
If a watched file changes on disk while app is open:
|
||||
```
|
||||
AdwBanner (persistent until dismissed):
|
||||
"Config file changed externally. Reload to see changes."
|
||||
[button] "Reload"
|
||||
```
|
||||
|
||||
### 5. AppStream metadata
|
||||
Create `data/com.mangotune.MangoTune.metainfo.xml` following AppStream spec.
|
||||
|
||||
### 6. Install targets (for Makefile / meson)
|
||||
```makefile
|
||||
PREFIX ?= /usr
|
||||
BINDIR = $(PREFIX)/bin
|
||||
DATADIR = $(PREFIX)/share
|
||||
|
||||
install:
|
||||
install -Dm755 target/release/mangotune $(DESTDIR)$(BINDIR)/mangotune
|
||||
install -Dm644 data/com.mangotune.MangoTune.desktop \
|
||||
$(DESTDIR)$(DATADIR)/applications/com.mangotune.MangoTune.desktop
|
||||
install -Dm644 data/com.mangotune.MangoTune.gschema.xml \
|
||||
$(DESTDIR)$(DATADIR)/glib-2.0/schemas/com.mangotune.MangoTune.gschema.xml
|
||||
install -Dm644 data/icons/com.mangotune.MangoTune.svg \
|
||||
$(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps/com.mangotune.MangoTune.svg
|
||||
glib-compile-schemas $(DESTDIR)$(DATADIR)/glib-2.0/schemas/
|
||||
```
|
||||
|
||||
### 7. README.md for the actual project
|
||||
Write a user-facing README covering:
|
||||
- What MangoTune is and why it's better than GOverlay
|
||||
- Installation (distro packages + build from source)
|
||||
- How config priority works
|
||||
- How to use the test launcher
|
||||
- How to contribute
|
||||
|
||||
### 8. Final QA checklist
|
||||
- [ ] `cargo clippy -- -D warnings` passes with zero warnings
|
||||
- [ ] `cargo test` passes all tests
|
||||
- [ ] App launches cleanly on X11
|
||||
- [ ] App launches cleanly on Wayland
|
||||
- [ ] All 120+ MangoHud options save and load correctly (write a test config, load it, verify)
|
||||
- [ ] Config with comments round-trips without destroying comments
|
||||
- [ ] Validation blocks all invalid values across all types
|
||||
- [ ] Dependency auto-enable works for all dependency pairs in schema
|
||||
- [ ] Conflict detection finds overlapping options across all layer combinations
|
||||
- [ ] Heroic integration writes JSON without data loss
|
||||
- [ ] Lutris integration writes YAML without data loss
|
||||
- [ ] vkcube + glxgears launch with correct environment
|
||||
- [ ] App handles MangoHud not installed gracefully (no crash)
|
||||
- [ ] Window resize / sidebar collapse works correctly
|
||||
- [ ] All keyboard shortcuts function
|
||||
- [ ] Unsaved changes guard fires on close
|
||||
- [ ] External file change detection triggers reload banner
|
||||
- [ ] About dialog shows correct version
|
||||
- [ ] .desktop file launches app correctly
|
||||
- [ ] GSettings schema installs and compiles correctly
|
||||
Reference in New Issue
Block a user