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

629 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`:
```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 (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
```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