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

22 KiB
Raw Permalink Blame History

Phases 0511 — 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:

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

  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

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 (872)
  • font_scale: AdwSpinRow (0.15.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.01.0) + live preview strip showing the alpha
  • alpha: AdwSpinRow
  • width, height: AdwSpinRow
  • table_columns: AdwSpinRow (110)
  • cellpadding_y: AdwSpinRow (-2.02.0)
  • round_corners: AdwSpinRow (050)
  • 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: F1F12 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

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/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)

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