629 lines
22 KiB
Markdown
629 lines
22 KiB
Markdown
# 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<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 (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<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
|