Initial import

This commit is contained in:
2026-03-30 22:51:56 -04:00
commit 08e2910b9d
103 changed files with 35475 additions and 0 deletions
+628
View File
@@ -0,0 +1,628 @@
# 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