22 KiB
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.rssrc/ui/pages/gpu.rssrc/ui/pages/cpu.rssrc/ui/pages/memory.rssrc/ui/widgets/toggle_row.rs← shared AdwSwitchRow wrappersrc/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:
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:
// 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
- Read current value from
state.config - Set initial active state
- Connect
notify::active: a. Updatestate.configviaparser::set_valueb. Runvalidator::validate_value(key, value, schema_entry)c. If dependency triggered: showAdwAlertDialog"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
- Read current value, set initial
- Connect
notify::value: a. Update state b. Validate c. Show/hide error on the row (add/remove.errorCSS 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
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:
- Run
validator::validate_all(&state.config)— full pass - If any Error: show
AdwToast"Fix N errors before saving", abort - If all Ok: call
parser::write(&state.config) - On success: show
AdwToast"Config saved", setstate.dirty = false, make save button insensitive
GPU-specific notes
gpu_mem_clockandgpu_mem_temp: when enabled, check ifvramis also active. If not, showAdwAlertDialog: "GPU Memory Clock requires VRAM display to be enabled. Enable VRAM now?" → buttons: "Enable Both" / "Cancel".gpu_voltage: if GPU vendor != AMD, add anAdwBannerwarning 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
AdwExpanderRowthat 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.rssrc/ui/pages/colors.rssrc/ui/pages/typography.rssrc/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.rssrc/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.rssrc/ui/pages/io_network.rssrc/ui/pages/media_player.rssrc/ui/pages/battery.rssrc/ui/pages/fps_limits.rssrc/ui/pages/logging.rssrc/ui/pages/blacklist.rssrc/ui/pages/opengl_quirks.rssrc/ui/pages/raw_editor.rssrc/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 modepermit_upload: when toggled off, also disableupload_logsoutput_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
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:
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/falseunder it - For reading: look for
mangohud: trueline 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→ SaveCtrl+Z→ Undo last change (basic: revert to last saved state)Ctrl+Shift+Z→ RedoCtrl+R→ Reload config from diskCtrl+W→ Close (prompts if unsaved changes)Ctrl+,→ PreferencesF5→ 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)
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 warningspasses with zero warningscargo testpasses 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