# 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> to all pages. pub struct AppState { pub config: AnnotatedConfig, pub validation: HashMap, pub dirty: bool, } ``` Use `glib::MainContext::channel()` or `Arc>` + 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>) -> 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, 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, 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 /// 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