Compare commits
4 Commits
a2364b1692
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ad46ad6b14 | |||
| 76ab5351a9 | |||
| e7c3b2eee7 | |||
| f984acf0e3 |
@@ -0,0 +1,10 @@
|
|||||||
|
## v2.0.0 (unreleased)
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
- Profile-to-profile inheritance removed. Profiles are now flat game-specific overrides applied directly on top of global defaults.
|
||||||
|
- `profile tree`, `profile inherit`, and `profile clear-inherit` commands removed.
|
||||||
|
- `profile export` now exports only explicitly configured settings (sparse), not fully resolved settings. Old exports with all settings baked in can still be imported.
|
||||||
|
- Settings renamed: `overlay` → `mangohud`, `performance` → `gamemode` (from prior work).
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
If upgrading from v1: run `gamewrap config migrate` to update renamed setting keys. Profile files exported with v1 can still be imported but will include all resolved settings — re-export after import to get the sparse format.
|
||||||
Generated
+1
-1
@@ -179,7 +179,7 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gamewrap"
|
name = "gamewrap"
|
||||||
version = "0.3.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
|
|||||||
+32
-17
@@ -37,17 +37,19 @@ Representative flow:
|
|||||||
| `src/config/mod.rs` | Config/state I/O | XDG path discovery, TOML load/save, CLI rendering of config/profile views. |
|
| `src/config/mod.rs` | Config/state I/O | XDG path discovery, TOML load/save, CLI rendering of config/profile views. |
|
||||||
| `src/config/schema.rs` | Persistent structs | `Settings`, `ResolvedSettings`, `ProfileConfig`, `ConfigFile`, `ObservedGame`, `StateFile`. |
|
| `src/config/schema.rs` | Persistent structs | `Settings`, `ResolvedSettings`, `ProfileConfig`, `ConfigFile`, `ObservedGame`, `StateFile`. |
|
||||||
| `src/config/keys.rs` | Setting mutation helpers | Friendly key parsing and value application/reset logic. |
|
| `src/config/keys.rs` | Setting mutation helpers | Friendly key parsing and value application/reset logic. |
|
||||||
| `src/profile.rs` | Resolution + validation | Resolves inherited profiles and validates parent chains/binding targets. |
|
| `src/config/migrate.rs` | Config migration logic | Renames retired setting keys in raw config/profile TOML before typed deserialization. |
|
||||||
|
| `src/profile.rs` | Resolution + validation | Applies global defaults plus one named profile and validates binding targets. |
|
||||||
| `src/bindings.rs` | Binding operations | Matching logic, set/remove binding, resolve profile for executable/observed game. |
|
| `src/bindings.rs` | Binding operations | Matching logic, set/remove binding, resolve profile for executable/observed game. |
|
||||||
| `src/detect.rs` | Executable detection | Pulls real game executable out of Steam/Proton wrapper shapes and records observed launches. |
|
| `src/detect.rs` | Executable detection | Pulls real game executable out of Steam/Proton wrapper shapes and records observed launches. |
|
||||||
| `src/launch.rs` | Launch planning/execution | Dependency checks, dry-run rendering, final command execution. |
|
| `src/launch.rs` | Launch planning/execution | Dependency checks, dry-run rendering, final command execution. |
|
||||||
| `src/env.rs` | Env shaping | Steam-context detection, host-library injection decisions, env var construction. |
|
| `src/env.rs` | Env shaping | Steam-context detection, host-library injection decisions, env var construction. |
|
||||||
|
| `src/log.rs` | Decision log | Best-effort timestamped launch, hook, exit, and playtime logging. |
|
||||||
| `src/doctor.rs` | Doctor rendering | Formats preflight results. |
|
| `src/doctor.rs` | Doctor rendering | Formats preflight results. |
|
||||||
| `src/status.rs` | Status rendering | Formats dependency/config/state summary. |
|
| `src/status.rs` | Status rendering | Formats dependency/config/state summary. |
|
||||||
| `src/help.rs` | Long-form help text | Shared topic help and top-level examples. |
|
| `src/help.rs` | Long-form help text | Shared topic help and top-level examples. |
|
||||||
| `src/error.rs` | Error model | Exit-code-bearing error categories and constructors. |
|
| `src/error.rs` | Error model | Exit-code-bearing error categories and constructors. |
|
||||||
| `src/notify.rs` | GUI failure notifier | `zenity` / `kdialog` / `xmessage` / `notify-send` selection and popup logic. |
|
| `src/notify.rs` | GUI failure notifier | `zenity` / `kdialog` / `xmessage` / `notify-send` selection and popup logic. |
|
||||||
| `src/share.rs` | Share/import formats | Resolved full-config and single-profile portable TOML formats. |
|
| `src/share.rs` | Share/import formats | Sparse full-config and single-profile portable TOML formats. |
|
||||||
| `tests/cli_matrix.rs` | Integration CLI coverage | End-to-end command validation in isolated XDG temp envs. |
|
| `tests/cli_matrix.rs` | Integration CLI coverage | End-to-end command validation in isolated XDG temp envs. |
|
||||||
|
|
||||||
Skip `target/` and other generated artifacts when extending this map.
|
Skip `target/` and other generated artifacts when extending this map.
|
||||||
@@ -89,12 +91,12 @@ Skip `target/` and other generated artifacts when extending this map.
|
|||||||
|
|
||||||
## 🔑 Key Concepts & Domain Terms
|
## 🔑 Key Concepts & Domain Terms
|
||||||
- **Defaults**: global baseline settings from config
|
- **Defaults**: global baseline settings from config
|
||||||
- **Profile**: reusable settings bundle; may inherit from another named profile
|
- **Profile**: reusable game-specific settings bundle applied directly over global defaults
|
||||||
- **Binding**: matcher string mapping a known executable/path to a profile
|
- **Binding**: matcher string mapping a known executable/path to a profile
|
||||||
- **Observed game**: recorded launch entry with executable, path, last launched profile, optional note/display name, launch count, and last launch timestamp
|
- **Observed game**: recorded launch entry with executable, path, last launched profile, optional note/display name, launch count, and last launch timestamp
|
||||||
- **Resolved settings**: effective values after applying defaults + inheritance + explicit overrides
|
- **Resolved settings**: effective values after applying global defaults + explicit profile overrides
|
||||||
- **Steam context**: environment where implicit launch mode and GUI failure notifications are enabled
|
- **Steam context**: environment where implicit launch mode and GUI failure notifications are enabled
|
||||||
- **Share format**: portable resolved export file, distinct from the sparse internal config layout
|
- **Share format**: portable sparse export matching the internal defaults/profile layout
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -103,18 +105,20 @@ Skip `target/` and other generated artifacts when extending this map.
|
|||||||
### Internal local files
|
### Internal local files
|
||||||
- Config path: `~/.config/gamewrap/config.toml`
|
- Config path: `~/.config/gamewrap/config.toml`
|
||||||
- State path: `~/.local/state/gamewrap/state.toml`
|
- State path: `~/.local/state/gamewrap/state.toml`
|
||||||
|
- Default decision log path: `~/.local/state/gamewrap/gamewrap.log` when `log-file` is enabled
|
||||||
- Config file is sparse/raw:
|
- Config file is sparse/raw:
|
||||||
- stores only explicit overrides
|
- stores only explicit overrides
|
||||||
- profiles may be empty and may inherit from parents
|
- profiles may be empty and always apply directly over global defaults
|
||||||
- bindings are stored separately
|
- bindings are stored separately
|
||||||
|
- gamescope output width/height accept an integer or `native`; window mode is one of `windowed`, `borderless`, or `fullscreen`
|
||||||
|
|
||||||
### Portable share files
|
### Portable share files
|
||||||
- Full config export suffix: `.gamewrap.toml`
|
- Full config export suffix: `.gamewrap.toml`
|
||||||
- Profile export suffix: `.gamewrap-profile.toml`
|
- Profile export suffix: `.gamewrap-profile.toml`
|
||||||
- Export commands write resolved values, not sparse internal overrides
|
- Export commands preserve sparse defaults and profile overrides
|
||||||
- Profile import creates a standalone explicit profile with no inheritance
|
- Profile import preserves explicitly configured settings; unset values use the receiving config's global defaults
|
||||||
- Per-profile env overrides are stored as an optional `env-vars` map on profile settings and merge through inheritance; child keys override parent keys
|
- Per-profile env overrides are stored as an optional `env-vars` map on profile settings and apply over global env defaults
|
||||||
- Pre/post launch hook commands are stored as optional string settings; pre-launch runs before exec, while post-launch is persisted/displayed but deferred until wrapped launching exists
|
- Pre/post launch hook commands are stored as optional string settings; setting post-launch switches execution to spawn/wait so the hook can run after the game exits
|
||||||
- Config import accepts:
|
- Config import accepts:
|
||||||
- current resolved share format
|
- current resolved share format
|
||||||
- legacy raw config format as fallback
|
- legacy raw config format as fallback
|
||||||
@@ -137,29 +141,28 @@ Skip `target/` and other generated artifacts when extending this map.
|
|||||||
- `completion`
|
- `completion`
|
||||||
|
|
||||||
### `config`
|
### `config`
|
||||||
- `show`
|
- `show [--effective]`
|
||||||
- `edit`
|
- `edit`
|
||||||
- `set <setting> <value>`
|
- `set <setting> <value>`
|
||||||
- `reset <setting>`
|
- `reset <setting>` / `reset --all`
|
||||||
- `export [name-or-path]`
|
- `export [name-or-path]`
|
||||||
- `import <name-or-path>`
|
- `import <name-or-path>`
|
||||||
|
- `migrate`
|
||||||
|
|
||||||
### `profile`
|
### `profile`
|
||||||
- `list`
|
- `list`
|
||||||
- `tree`
|
|
||||||
- `create <name>`
|
- `create <name>`
|
||||||
- `duplicate <src> <dst>`
|
- `duplicate <src> <dst>`
|
||||||
- `show <name>`
|
- `show <name>`
|
||||||
- `export <name> [name-or-path]`
|
- `export <name> [name-or-path]`
|
||||||
- `import <name-or-path>`
|
- `import <name-or-path>`
|
||||||
|
- `migrate <file> [--dry-run]`
|
||||||
- `env set <name> <key> <value>`
|
- `env set <name> <key> <value>`
|
||||||
- `env unset <name> <key>`
|
- `env unset <name> <key>`
|
||||||
- `env list <name>`
|
- `env list <name>`
|
||||||
- `env clear <name>`
|
- `env clear <name>`
|
||||||
- `set <name> <setting> <value>`
|
- `set <name> <setting> <value>`
|
||||||
- `reset <name> <setting>`
|
- `reset <name> <setting>`
|
||||||
- `inherit <name> <parent>`
|
|
||||||
- `clear-inherit <name>`
|
|
||||||
- `delete <name>`
|
- `delete <name>`
|
||||||
|
|
||||||
### `game`
|
### `game`
|
||||||
@@ -190,14 +193,14 @@ If you change:
|
|||||||
- command surface -> update `tests/cli_matrix.rs`
|
- command surface -> update `tests/cli_matrix.rs`
|
||||||
- share/import/export formats -> update `tests/cli_matrix.rs` and `src/share.rs` tests
|
- share/import/export formats -> update `tests/cli_matrix.rs` and `src/share.rs` tests
|
||||||
- completion install/candidates -> update `tests/cli_matrix.rs` and `src/completion.rs` tests
|
- completion install/candidates -> update `tests/cli_matrix.rs` and `src/completion.rs` tests
|
||||||
- profile inheritance logic -> update `src/profile.rs` tests and related CLI matrix cases
|
- profile resolution logic -> update `src/profile.rs` tests and related CLI matrix cases
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧩 Feature Areas & Ownership Map
|
## 🧩 Feature Areas & Ownership Map
|
||||||
- **CLI / UX**: `src/cli.rs`, `src/help.rs`, `README.md`
|
- **CLI / UX**: `src/cli.rs`, `src/help.rs`, `README.md`
|
||||||
- **Persistence**: `src/config/*`, `src/share.rs`
|
- **Persistence**: `src/config/*`, `src/share.rs`
|
||||||
- **Profiles / inheritance**: `src/profile.rs`
|
- **Profiles / resolution**: `src/profile.rs`
|
||||||
- **Game observation / matching / bindings**: `src/detect.rs`, `src/bindings.rs`
|
- **Game observation / matching / bindings**: `src/detect.rs`, `src/bindings.rs`
|
||||||
- **Launch execution**: `src/launch.rs`, `src/env.rs`
|
- **Launch execution**: `src/launch.rs`, `src/env.rs`
|
||||||
- **Diagnostics**: `src/doctor.rs`, `src/status.rs`
|
- **Diagnostics**: `src/doctor.rs`, `src/status.rs`
|
||||||
@@ -235,6 +238,18 @@ Do not update this file for:
|
|||||||
- [2026-05-31] Added TTY/NO_COLOR-aware ANSI output helpers for doctor and status rendering.
|
- [2026-05-31] Added TTY/NO_COLOR-aware ANSI output helpers for doctor and status rendering.
|
||||||
- [2026-05-31] Added game forget, config edit, profile list/tree improvements, and notify test commands.
|
- [2026-05-31] Added game forget, config edit, profile list/tree improvements, and notify test commands.
|
||||||
- [2026-05-31] Added gamescope settings, launch wrapping, dependency diagnostics, completion candidates, and docs.
|
- [2026-05-31] Added gamescope settings, launch wrapping, dependency diagnostics, completion candidates, and docs.
|
||||||
|
- [2026-06-06] Removed retired setting aliases and added automatic and explicit TOML migration for configs and profile exports.
|
||||||
- [2026-05-31] Added observed-game display names, launch timestamps/counts, `last`, `game rename`, and `fps-cap`.
|
- [2026-05-31] Added observed-game display names, launch timestamps/counts, `last`, `game rename`, and `fps-cap`.
|
||||||
- [2026-05-31] Added per-profile env overrides, vkBasalt support, and Proton esync/fsync compatibility controls.
|
- [2026-05-31] Added per-profile env overrides, vkBasalt support, and Proton esync/fsync compatibility controls.
|
||||||
- [2026-05-31] Added pre-launch shell hooks plus persisted/dry-run/doctor visibility for deferred post-launch hooks.
|
- [2026-05-31] Added pre-launch shell hooks plus persisted/dry-run/doctor visibility for deferred post-launch hooks.
|
||||||
|
- [2026-06-05] Expanded gamescope config surface: nested-width/height, unfocused-fps, scaler (-S), filter (-F), sharpness, fullscreen, borderless, adaptive-sync, hdr, steam, expose-wayland. Added gamescope-mangoapp for native MangoHud overlay integration (--mangoapp flag); doctor warns when mangohud prefix is used inside gamescope without mangoapp.
|
||||||
|
- [2026-06-06] Added optional vkbasalt-config profile/default setting and gated VKBASALT_CONFIG_FILE launch environment support.
|
||||||
|
- [2026-06-06] Replaced gamescope fullscreen/borderless toggles with `gamescope-mode` and added native display resolution detection for output width/height.
|
||||||
|
- [2026-06-06] Clarified unset values in `config show` and added `config reset --all` for restoring built-in defaults.
|
||||||
|
- [2026-06-06] Added gamewrap decision logging plus MangoHud session logging and vkBasalt log-level settings.
|
||||||
|
- [2026-06-06] Made `config show` display only explicitly configured defaults, with `--effective` for the full computed view.
|
||||||
|
- [2026-06-06] Added --effective flag to `profile show` and profile sections of `config show`; default now shows only explicitly configured profile settings.
|
||||||
|
- [2026-06-06] Renamed the MangoHud/GameMode settings to `mangohud`/`gamemode` with legacy aliases, and grouped settings help with per-tool filters.
|
||||||
|
- [2026-06-06] Removed profile inheritance, flattened resolution to defaults plus one profile, and made config/profile exports sparse.
|
||||||
|
- [2026-06-14] Added hook-errors setting (warn/fail) to control pre-launch hook failure behavior; ensured post-hook runs after game spawn failures; propagated game exit codes; centralized hook execution via run_hook().
|
||||||
|
- [2026-06-14] Consolidated repeated config string mappings, resolved-setting application, and color helpers with local macros.
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ gamewrap %command%
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Short Steam launch options
|
- Short Steam launch options
|
||||||
- Friendly setting names like `overlay` and `performance`
|
- Friendly setting names like `mangohud` and `gamemode`
|
||||||
- Persistent config outside the Steam UI
|
- Persistent config outside the Steam UI
|
||||||
- Named profiles for reusable setups
|
- Named profiles for reusable setups
|
||||||
- Profile inheritance for layered setups
|
- Flat game-specific profiles layered on global defaults
|
||||||
- Game-specific profile binding through `game bind`
|
- Game-specific profile binding through `game bind`
|
||||||
- Quick access to the last played game through `gamewrap last`
|
- Quick access to the last played game through `gamewrap last`
|
||||||
- Play time and launch count tracking per game
|
- Play time and launch count tracking per game
|
||||||
@@ -32,7 +32,6 @@ gamewrap %command%
|
|||||||
- Config export/import for backup and sharing
|
- Config export/import for backup and sharing
|
||||||
- Direct config editing through `config edit`
|
- Direct config editing through `config edit`
|
||||||
- Profile export/import for sharing one setup at a time
|
- Profile export/import for sharing one setup at a time
|
||||||
- Profile tree view for inheritance checks
|
|
||||||
- Per-profile environment variable overrides
|
- Per-profile environment variable overrides
|
||||||
- gamescope Wayland compositor integration
|
- gamescope Wayland compositor integration
|
||||||
- FPS cap through MangoHud
|
- FPS cap through MangoHud
|
||||||
@@ -97,13 +96,13 @@ $HOME/.cargo/bin/gamewrap %command%
|
|||||||
2. Turn MangoHud on by default:
|
2. Turn MangoHud on by default:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gamewrap config set overlay on
|
gamewrap config set mangohud on
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Turn GameMode on by default:
|
3. Turn GameMode on by default:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gamewrap config set performance on
|
gamewrap config set gamemode on
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Check your setup before launching a real game:
|
4. Check your setup before launching a real game:
|
||||||
@@ -133,13 +132,11 @@ gamewrap profile create benchmark
|
|||||||
gamewrap game bind "Game.exe" benchmark
|
gamewrap game bind "Game.exe" benchmark
|
||||||
```
|
```
|
||||||
|
|
||||||
8. Layer one profile on top of another when you want shared defaults:
|
8. Add game-specific overrides while keeping shared values global:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gamewrap profile create base
|
gamewrap config set mangohud on
|
||||||
gamewrap profile set base overlay on
|
|
||||||
gamewrap profile create benchmark
|
gamewrap profile create benchmark
|
||||||
gamewrap profile inherit benchmark base
|
|
||||||
gamewrap profile set benchmark verbose on
|
gamewrap profile set benchmark verbose on
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -181,6 +178,8 @@ gamewrap run -- /path/to/game/executable
|
|||||||
```bash
|
```bash
|
||||||
gamewrap --help
|
gamewrap --help
|
||||||
gamewrap help settings
|
gamewrap help settings
|
||||||
|
gamewrap help settings gamescope
|
||||||
|
gamewrap help settings mangohud
|
||||||
gamewrap help doctor
|
gamewrap help doctor
|
||||||
gamewrap game list
|
gamewrap game list
|
||||||
gamewrap game list "elden"
|
gamewrap game list "elden"
|
||||||
@@ -196,20 +195,23 @@ gamewrap completion zsh
|
|||||||
gamewrap completion install zsh
|
gamewrap completion install zsh
|
||||||
gamewrap completion path zsh
|
gamewrap completion path zsh
|
||||||
gamewrap config show
|
gamewrap config show
|
||||||
|
gamewrap config show --effective
|
||||||
gamewrap config edit
|
gamewrap config edit
|
||||||
|
gamewrap config reset --all
|
||||||
|
gamewrap config migrate
|
||||||
gamewrap config export shared
|
gamewrap config export shared
|
||||||
gamewrap config import shared
|
gamewrap config import shared
|
||||||
gamewrap last
|
gamewrap last
|
||||||
gamewrap profile list
|
gamewrap profile list
|
||||||
gamewrap profile tree
|
|
||||||
gamewrap profile create benchmark
|
gamewrap profile create benchmark
|
||||||
|
gamewrap profile show benchmark
|
||||||
|
gamewrap profile show benchmark --effective
|
||||||
gamewrap profile duplicate benchmark benchmark-copy
|
gamewrap profile duplicate benchmark benchmark-copy
|
||||||
gamewrap profile inherit benchmark base
|
|
||||||
gamewrap profile clear-inherit benchmark
|
|
||||||
gamewrap profile export benchmark benchmark
|
gamewrap profile export benchmark benchmark
|
||||||
gamewrap profile import benchmark
|
gamewrap profile import benchmark
|
||||||
gamewrap profile set benchmark overlay on
|
gamewrap profile migrate old-benchmark
|
||||||
gamewrap profile reset benchmark overlay
|
gamewrap profile set benchmark mangohud on
|
||||||
|
gamewrap profile reset benchmark mangohud
|
||||||
gamewrap profile env set benchmark DXVK_ASYNC 1
|
gamewrap profile env set benchmark DXVK_ASYNC 1
|
||||||
gamewrap profile env list benchmark
|
gamewrap profile env list benchmark
|
||||||
gamewrap profile env unset benchmark DXVK_ASYNC
|
gamewrap profile env unset benchmark DXVK_ASYNC
|
||||||
@@ -222,15 +224,20 @@ gamewrap game clear-note "eldenring.exe"
|
|||||||
|
|
||||||
## Friendly Settings
|
## Friendly Settings
|
||||||
|
|
||||||
- `overlay`: turns MangoHud on or off
|
- `mangohud`: turns MangoHud on or off
|
||||||
- `performance`: turns GameMode on or off
|
- `gamemode`: turns GameMode on or off
|
||||||
- `steam-host-libs`: prefers host libraries inside Steam runtime environments and sets `STEAM_RUNTIME_PREFER_HOST_LIBRARIES=1`
|
- `steam-host-libs`: prefers host libraries inside Steam runtime environments and sets `STEAM_RUNTIME_PREFER_HOST_LIBRARIES=1`
|
||||||
- `game-libs`: controls whether `gamewrap` injects auto-detected host library directories into `LD_LIBRARY_PATH`
|
- `game-libs`: controls whether `gamewrap` injects auto-detected host library directories into `LD_LIBRARY_PATH`
|
||||||
- `verbose`: shows more detail in diagnostic commands
|
- `verbose`: shows more detail in diagnostic commands
|
||||||
|
- `log-file`, `log-path`: append gamewrap launch decisions to the default state log or a custom path
|
||||||
- `gamescope`: wraps the game in the gamescope Wayland compositor
|
- `gamescope`: wraps the game in the gamescope Wayland compositor
|
||||||
- `gamescope-width`, `gamescope-height`, `gamescope-fps`: pass `-W`, `-H`, and `-r` values to gamescope when `gamescope` is on
|
- `gamescope-width`, `gamescope-height`: set output resolution with a pixel count or `native`; unset uses gamescope's 1280x720 default
|
||||||
- `fps-cap`: caps frame rate through MangoHud when `overlay` is on, for example `gamewrap config set fps-cap 60`
|
- `gamescope-mode`: selects `windowed`, `borderless`, or `fullscreen`
|
||||||
|
- `gamescope-fps`: passes the `-r` target refresh value when gamescope is on
|
||||||
|
- `fps-cap`: caps frame rate through MangoHud when `mangohud` is on, for example `gamewrap config set fps-cap 60`
|
||||||
|
- `mangohud-log`, `mangohud-log-path`: enable full-session MangoHud performance logging and optionally choose its output path
|
||||||
- `vkbasalt`: enables vkBasalt post-processing with `ENABLE_VKBASALT=1`
|
- `vkbasalt`: enables vkBasalt post-processing with `ENABLE_VKBASALT=1`
|
||||||
|
- `vkbasalt-log-level`: sets `VKBASALT_LOG_LEVEL` to `debug`, `info`, `warning`, `error`, or `none` when vkBasalt is on
|
||||||
- `esync`: forces Proton esync on or off with `PROTON_NO_ESYNC`; leave unset to use Steam/Proton defaults
|
- `esync`: forces Proton esync on or off with `PROTON_NO_ESYNC`; leave unset to use Steam/Proton defaults
|
||||||
- `fsync`: forces Proton fsync on or off with `PROTON_NO_FSYNC`; leave unset to use Steam/Proton defaults
|
- `fsync`: forces Proton fsync on or off with `PROTON_NO_FSYNC`; leave unset to use Steam/Proton defaults
|
||||||
- `large-address-aware` / `laa`: sets `PROTON_LARGE_ADDRESS_AWARE=1` for older 32-bit Proton games that need more than 2 GB of address space
|
- `large-address-aware` / `laa`: sets `PROTON_LARGE_ADDRESS_AWARE=1` for older 32-bit Proton games that need more than 2 GB of address space
|
||||||
@@ -238,9 +245,20 @@ gamewrap game clear-note "eldenring.exe"
|
|||||||
- `post-launch`: runs a shell command after the game exits; when set, gamewrap spawns the game and waits for it to finish before running this hook
|
- `post-launch`: runs a shell command after the game exits; when set, gamewrap spawns the game and waits for it to finish before running this hook
|
||||||
- `env-vars`: per-profile environment overrides managed with `gamewrap profile env set/list/unset/clear`
|
- `env-vars`: per-profile environment overrides managed with `gamewrap profile env set/list/unset/clear`
|
||||||
|
|
||||||
|
**hook-errors** (`warn` | `fail`, default: `warn`)
|
||||||
|
|
||||||
|
Controls what happens when a pre-launch hook exits nonzero or cannot be executed.
|
||||||
|
- `warn` — a message is printed to stderr and the game launches anyway.
|
||||||
|
- `fail` — gamewrap aborts with an error and the game is not launched.
|
||||||
|
|
||||||
|
The post-launch hook always runs after the pre-launch hook succeeds, even if the game itself fails to start.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
gamewrap config set hook-errors fail
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
When `gamewrap` launches a game, it resolves your defaults, any inherited profile chain, and any matching game binding, prepares the needed environment variables, and then prefixes the game command with `gamescope`, `mangohud`, and/or `gamemoderun` when those features are enabled.
|
When `gamewrap` launches a game, it applies global defaults, then any matching profile overrides, prepares the needed environment variables, and prefixes the game command with `gamescope`, `mangohud`, and/or `gamemoderun` when those features are enabled.
|
||||||
|
|
||||||
If something important is missing, `gamewrap` is designed to fail clearly instead of silently skipping the requested behavior. The `doctor` and `status` commands help you verify that before launching through Steam.
|
If something important is missing, `gamewrap` is designed to fail clearly instead of silently skipping the requested behavior. The `doctor` and `status` commands help you verify that before launching through Steam.
|
||||||
|
|
||||||
@@ -253,7 +271,6 @@ For terminal usage, `gamewrap` distinguishes between management commands and exp
|
|||||||
- `gamewrap config edit` opens the config file in `$VISUAL`, `$EDITOR`, or `nano`
|
- `gamewrap config edit` opens the config file in `$VISUAL`, `$EDITOR`, or `nano`
|
||||||
- use `gamewrap config export` and `gamewrap config import ...` for full-config backup and sharing
|
- use `gamewrap config export` and `gamewrap config import ...` for full-config backup and sharing
|
||||||
- use `gamewrap profile export` and `gamewrap profile import ...` for sharing one profile
|
- use `gamewrap profile export` and `gamewrap profile import ...` for sharing one profile
|
||||||
- use `gamewrap profile tree` to inspect profile inheritance
|
|
||||||
- use `gamewrap profile env set <profile> <KEY> <VALUE>` for per-profile environment overrides
|
- use `gamewrap profile env set <profile> <KEY> <VALUE>` for per-profile environment overrides
|
||||||
- use `gamewrap notify test` to verify graphical failure notifications
|
- use `gamewrap notify test` to verify graphical failure notifications
|
||||||
- use `gamewrap run <command>` when you want to explicitly launch from the terminal
|
- use `gamewrap run <command>` when you want to explicitly launch from the terminal
|
||||||
@@ -302,9 +319,9 @@ gamewrap profile import benchmark
|
|||||||
|
|
||||||
`gamewrap` automatically adds `.gamewrap.toml` or `.gamewrap-profile.toml` when needed.
|
`gamewrap` automatically adds `.gamewrap.toml` or `.gamewrap-profile.toml` when needed.
|
||||||
|
|
||||||
`gamewrap config export` writes resolved settings, not just the sparse internal overrides. That makes the exported file more predictable when you import it on another machine with different defaults.
|
`gamewrap config export` writes the sparse global defaults and profile overrides exactly as configured.
|
||||||
|
|
||||||
`gamewrap profile export` also writes resolved settings. Imported profiles are brought in as standalone profiles with explicit values, so they keep the same behavior even if the importing machine uses different defaults.
|
`gamewrap profile export` writes only explicitly configured profile settings. Imported profiles use the receiving machine's global defaults for every unset value.
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
|
|||||||
+95
-3
@@ -33,7 +33,7 @@ Implemented features:
|
|||||||
- Steam-first launcher flow with `gamewrap %command%`
|
- Steam-first launcher flow with `gamewrap %command%`
|
||||||
- Default launch mode with MangoHud and GameMode
|
- Default launch mode with MangoHud and GameMode
|
||||||
- Friendly setting names for all launch options
|
- Friendly setting names for all launch options
|
||||||
- Named profiles with full inheritance chains and cycle detection
|
- Named game-specific profiles (flat two-tier: global defaults + per-game overrides)
|
||||||
- Per-profile environment variable overrides (`profile env set/unset/list/clear`)
|
- Per-profile environment variable overrides (`profile env set/unset/list/clear`)
|
||||||
- Manual executable-to-profile bindings
|
- Manual executable-to-profile bindings
|
||||||
- Observed game history with launch count, timestamps, and display names
|
- Observed game history with launch count, timestamps, and display names
|
||||||
@@ -51,14 +51,13 @@ Implemented features:
|
|||||||
- `dry-run` launch plan preview
|
- `dry-run` launch plan preview
|
||||||
- `config edit` to open config in `$EDITOR`
|
- `config edit` to open config in `$EDITOR`
|
||||||
- Config and profile export/import for sharing
|
- Config and profile export/import for sharing
|
||||||
- `profile tree` for visualizing inheritance chains
|
|
||||||
- `notify test` for graphical notification verification
|
- `notify test` for graphical notification verification
|
||||||
- ANSI color output (respects NO_COLOR, CLICOLOR, CLICOLOR_FORCE)
|
- ANSI color output (respects NO_COLOR, CLICOLOR, CLICOLOR_FORCE)
|
||||||
- Shell completion with live candidates (profiles, games, settings)
|
- Shell completion with live candidates (profiles, games, settings)
|
||||||
- Clear runtime dependency checks and actionable error messages
|
- Clear runtime dependency checks and actionable error messages
|
||||||
|
|
||||||
Friendly settings supported:
|
Friendly settings supported:
|
||||||
- `overlay`, `performance`, `steam-host-libs`, `game-libs`, `verbose`
|
- `mangohud`, `gamemode`, `steam-host-libs`, `game-libs`, `verbose`
|
||||||
- `gamescope`, `gamescope-width`, `gamescope-height`, `gamescope-fps`
|
- `gamescope`, `gamescope-width`, `gamescope-height`, `gamescope-fps`
|
||||||
- `fps-cap`, `vkbasalt`
|
- `fps-cap`, `vkbasalt`
|
||||||
- `esync`, `fsync`, `large-address-aware`
|
- `esync`, `fsync`, `large-address-aware`
|
||||||
@@ -132,6 +131,7 @@ Features that were deferred from the initial scope and are now implemented:
|
|||||||
Features still deferred:
|
Features still deferred:
|
||||||
- Benchmark and recording profile presets (named templates that bundle several settings)
|
- Benchmark and recording profile presets (named templates that bundle several settings)
|
||||||
- Additional launcher helpers beyond the current set
|
- Additional launcher helpers beyond the current set
|
||||||
|
- **Profile description field for community sharing** — add `description: Option<String>` to `ProfileConfig` and `SharedProfileFile`, shown at the top of exported `.gamewrap-profile.toml` files so ProtonDB/forum posts are self-documenting. CLI: `gamewrap profile describe <name> <text>` and `profile clear-describe <name>`. Preserved through import and shown in `profile show`/`profile list`. No tags, AppIDs, or author fields — those belong on the posting platform, not in the file.
|
||||||
|
|
||||||
## Recommended Expansion Order
|
## Recommended Expansion Order
|
||||||
|
|
||||||
@@ -245,3 +245,95 @@ Before starting packaging or deferred features, re-check:
|
|||||||
- whether the project is going public
|
- whether the project is going public
|
||||||
- whether package maintenance is worth the overhead
|
- whether package maintenance is worth the overhead
|
||||||
- whether v1 behavior is stable enough to freeze the main CLI/config model
|
- whether v1 behavior is stable enough to freeze the main CLI/config model
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-v1 Backlog
|
||||||
|
|
||||||
|
Logged 2026-06-06. Priority order reflects dependencies — B1 sets naming used by B5, and JSON output (B6 Phase 1) is prerequisite for any GUI work.
|
||||||
|
|
||||||
|
### B1 + B4 — Settings grouping, tool-prefixed names, help system overhaul
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
- `gamewrap help settings` outputs 36 settings in a flat, ungrouped list with no visual separation by tool
|
||||||
|
- `overlay` (MangoHud) and `performance` (GameMode) have no tool prefix while every gamescope/vkbasalt/mangohud-log setting already does
|
||||||
|
- A gamescope configuration note currently sits orphaned between log options and the gamescope settings block in help output
|
||||||
|
|
||||||
|
**Proposed changes:**
|
||||||
|
- Rename `overlay` → `mangohud` (the MangoHud enable/disable toggle), consistent with `mangohud-log`
|
||||||
|
- Rename `performance` → `gamemode` (the GameMode enable/disable toggle)
|
||||||
|
- Group `gamewrap help settings` into tool-based sections: **Core**, **MangoHud**, **GameMode**, **Gamescope**, **vkBasalt**, **Proton/Wine**, **Hooks & Logging**
|
||||||
|
- Move the gamescope configuration note into the Gamescope section header (fixes B4)
|
||||||
|
- Enhance `topic_text()` in `src/help.rs` to accept an optional tool filter: `gamewrap help settings gamescope`
|
||||||
|
- Propagate renames everywhere: `Settings` struct fields, CLI parser, `config/keys.rs`, TOML serialization, shell completion, tests, README, docs
|
||||||
|
- Add a migration path for existing configs using the old names (clear error pointing to new name, or silent rename on load)
|
||||||
|
|
||||||
|
**Note:** Breaking change — existing config files and profiles using `overlay` or `performance` will need migration. Sets the naming foundation for B5.
|
||||||
|
|
||||||
|
**Key files:** `src/help.rs`, `src/config/schema.rs`, `src/config/keys.rs`, `src/config/mod.rs`, `src/cli.rs`, `src/completion.rs`, `tests/`, `README.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B2 — Large game list management
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
- After using gamewrap on many games (demos, uninstalled games, one-offs), `game list` grows without bound
|
||||||
|
- The only removal option is `game forget` (hard deletion); no way to soft-hide entries
|
||||||
|
|
||||||
|
**Proposed changes:**
|
||||||
|
- Add `game archive <name-or-exe>` / `game unarchive <name-or-exe>` — sets an `archived` flag on the state entry, excluded from default `game list`
|
||||||
|
- `game list --all` shows archived entries alongside active ones (visually marked)
|
||||||
|
- `game forget` remains for permanent removal
|
||||||
|
- `game list` shows an entry count and a hint if the list exceeds a threshold (e.g. 20 entries)
|
||||||
|
|
||||||
|
**Key files:** `src/cli.rs`, state file handling in `src/config/mod.rs`, `src/detect.rs`, tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B3 — Profile export semantics
|
||||||
|
|
||||||
|
**Question:** When a profile is exported to share:
|
||||||
|
- **Option A — sparse:** Export only what was explicitly set in the profile. Recipient's globals fill the rest at their end. Simple and portable; behavior depends on recipient's global config.
|
||||||
|
- **Option B — effective:** Export fully resolved settings (profile + globals baked in). Recipient gets a "known good" snapshot but must manually strip their own preferences.
|
||||||
|
- **Option C — sparse + informational context:** Export sparse profile, plus a `# context:` comment block showing what the exporter's globals were at export time. Informational only — not interpreted by import.
|
||||||
|
|
||||||
|
**Current behavior:** Option A (sparse).
|
||||||
|
|
||||||
|
**Resolved:** Sparse export implemented. Profiles are now flat (no inheritance). Export produces only explicitly set settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B5 — Grouped, cleaner `config show` / `profile show` display
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
- `config show` output (especially `--effective`) has no visual grouping — 30+ settings appear in struct field order
|
||||||
|
- No section headers separating e.g. gamescope settings from MangoHud settings
|
||||||
|
|
||||||
|
**Proposed changes:**
|
||||||
|
- Refactor `render_resolved_settings()` and `render_profile_settings_with_indent()` to emit settings under dimmed section headers using the same tool groups as B1
|
||||||
|
- Configured mode (default): only show a section header if that section has at least one configured value
|
||||||
|
- Effective mode (`--effective`): always show section headers; show `(nothing configured)` per-section if empty
|
||||||
|
|
||||||
|
**Dependency:** Best done after B1, which defines the canonical section groupings. Can be scoped independently by hardcoding groups if B1 is deferred.
|
||||||
|
|
||||||
|
**Key files:** `src/config/mod.rs`, tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B6 — JSON output flag + GUI (discussion needed)
|
||||||
|
|
||||||
|
**Phase 1 — JSON flag (independent):**
|
||||||
|
- Add `--json` flag to read commands: `config show`, `profile show`, `game list`, `status`, `dry-run`
|
||||||
|
- Write/mutating commands return `{"ok": true}` or the updated state on success
|
||||||
|
- Useful for scripting independently of any GUI
|
||||||
|
|
||||||
|
**Phase 2 — GUI (dependent on Phase 1):**
|
||||||
|
- Build a GUI that invokes `gamewrap` as a subprocess and renders the JSON output
|
||||||
|
- CLI remains the authoritative source of truth; GUI is a frontend only
|
||||||
|
|
||||||
|
**Discussion points before Phase 2:**
|
||||||
|
- Framework: egui (Rust, self-contained), iced (Rust async), Tauri (web frontend, heavier), GTK4 (gtk-rs)?
|
||||||
|
- Scope: full settings editor, profile manager, game list browser, or read-only status overlay?
|
||||||
|
- Packaging: GUI as an optional Cargo feature flag to keep the base CLI install slim?
|
||||||
|
|
||||||
|
**Recommended starting point:** Phase 1 JSON flag only.
|
||||||
|
|||||||
+486
-182
File diff suppressed because it is too large
Load Diff
+12
-41
@@ -12,53 +12,24 @@ pub fn enabled() -> bool {
|
|||||||
std::env::var("TERM").as_deref() != Ok("dumb")
|
std::env::var("TERM").as_deref() != Ok("dumb")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ok(text: &str) -> String {
|
macro_rules! color_fn {
|
||||||
|
($name:ident, $method:ident) => {
|
||||||
|
pub fn $name(text: &str) -> String {
|
||||||
if enabled() {
|
if enabled() {
|
||||||
text.green().to_string()
|
text.$method().to_string()
|
||||||
} else {
|
} else {
|
||||||
text.to_string()
|
text.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn warn(text: &str) -> String {
|
color_fn!(ok, green);
|
||||||
if enabled() {
|
color_fn!(warn, yellow);
|
||||||
text.yellow().to_string()
|
color_fn!(fail, red);
|
||||||
} else {
|
color_fn!(bold, bold);
|
||||||
text.to_string()
|
color_fn!(accent, cyan);
|
||||||
}
|
color_fn!(dim, dimmed);
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fail(text: &str) -> String {
|
|
||||||
if enabled() {
|
|
||||||
text.red().to_string()
|
|
||||||
} else {
|
|
||||||
text.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bold(text: &str) -> String {
|
|
||||||
if enabled() {
|
|
||||||
text.bold().to_string()
|
|
||||||
} else {
|
|
||||||
text.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn accent(text: &str) -> String {
|
|
||||||
if enabled() {
|
|
||||||
text.cyan().to_string()
|
|
||||||
} else {
|
|
||||||
text.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dim(text: &str) -> String {
|
|
||||||
if enabled() {
|
|
||||||
text.dimmed().to_string()
|
|
||||||
} else {
|
|
||||||
text.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_off(value: bool) -> String {
|
pub fn on_off(value: bool) -> String {
|
||||||
if value { ok("on") } else { dim("off") }
|
if value { ok("on") } else { dim("off") }
|
||||||
|
|||||||
+191
-21
@@ -58,7 +58,7 @@ pub fn install(shell: Shell, paths: &AppPaths) -> Result<InstallOutcome, AppErro
|
|||||||
|
|
||||||
let mut message = format!(
|
let mut message = format!(
|
||||||
"Installed {} completion to `{}`.",
|
"Installed {} completion to `{}`.",
|
||||||
shell.to_string(),
|
shell,
|
||||||
shell_files.script_path.display()
|
shell_files.script_path.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -105,22 +105,78 @@ pub fn path(shell: Shell, paths: &AppPaths) -> Result<String, AppError> {
|
|||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setting_name_candidates() -> Vec<CompletionCandidate> {
|
pub const SETTING_NAME_HINTS: &[(&str, &str)] = &[
|
||||||
[
|
("mangohud", "Turn MangoHud on or off"),
|
||||||
("overlay", "Turn MangoHud on or off"),
|
("gamemode", "Turn GameMode on or off"),
|
||||||
("performance", "Turn GameMode on or off"),
|
|
||||||
(
|
(
|
||||||
"steam-host-libs",
|
"steam-host-libs",
|
||||||
"Prefer host libraries inside the Steam runtime",
|
"Prefer host libraries inside the Steam runtime",
|
||||||
),
|
),
|
||||||
("game-libs", "Control host library path injection for games"),
|
("game-libs", "Control host library path injection for games"),
|
||||||
("verbose", "Show extra diagnostic detail"),
|
("verbose", "Show extra diagnostic detail"),
|
||||||
|
("log-file", "Write a launch decision log to a file"),
|
||||||
|
("log-path", "Path to gamewrap log file"),
|
||||||
("gamescope", "Wrap launches in the gamescope compositor"),
|
("gamescope", "Wrap launches in the gamescope compositor"),
|
||||||
("gamescope-width", "Set the gamescope target width"),
|
(
|
||||||
("gamescope-height", "Set the gamescope target height"),
|
"gamescope-width",
|
||||||
("gamescope-fps", "Set the gamescope target FPS"),
|
"Set the gamescope output width (pixels or native)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gamescope-height",
|
||||||
|
"Set the gamescope output height (pixels or native)",
|
||||||
|
),
|
||||||
|
("gamescope-fps", "Set the gamescope target FPS (-r)"),
|
||||||
|
(
|
||||||
|
"gamescope-nested-width",
|
||||||
|
"Set the game render width (-w, for upscaling)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gamescope-nested-height",
|
||||||
|
"Set the game render height (-h, for upscaling)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gamescope-unfocused-fps",
|
||||||
|
"Set FPS limit when gamescope window is unfocused",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gamescope-scaler",
|
||||||
|
"Set the gamescope scaling mode (auto, integer, fit, fill, stretch)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gamescope-filter",
|
||||||
|
"Set the gamescope upscale filter (linear, nearest, fsr, nis, pixel)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gamescope-sharpness",
|
||||||
|
"Set upscale sharpness 0–20 (0=max, 20=min, fsr/nis only)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gamescope-mode",
|
||||||
|
"Set the gamescope window mode (windowed, borderless, fullscreen)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gamescope-adaptive-sync",
|
||||||
|
"Enable adaptive sync / VRR in gamescope",
|
||||||
|
),
|
||||||
|
("gamescope-hdr", "Enable HDR output in gamescope"),
|
||||||
|
("gamescope-steam", "Enable Steam integration in gamescope"),
|
||||||
|
(
|
||||||
|
"gamescope-expose-wayland",
|
||||||
|
"Expose Wayland socket for native Wayland clients",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gamescope-mangoapp",
|
||||||
|
"Use gamescope native MangoHud overlay instead of mangohud prefix",
|
||||||
|
),
|
||||||
("fps-cap", "Cap frame rate when MangoHud overlay is enabled"),
|
("fps-cap", "Cap frame rate when MangoHud overlay is enabled"),
|
||||||
|
("mangohud-log", "Enable MangoHud session performance log"),
|
||||||
|
("mangohud-log-path", "Path for MangoHud log output"),
|
||||||
("vkbasalt", "Enable the vkBasalt post-processing layer"),
|
("vkbasalt", "Enable the vkBasalt post-processing layer"),
|
||||||
|
(
|
||||||
|
"vkbasalt-config",
|
||||||
|
"Path to a vkBasalt config file (sets VKBASALT_CONFIG_FILE)",
|
||||||
|
),
|
||||||
|
("vkbasalt-log-level", "vkBasalt log verbosity"),
|
||||||
("esync", "Force Proton esync on or off"),
|
("esync", "Force Proton esync on or off"),
|
||||||
("fsync", "Force Proton fsync on or off"),
|
("fsync", "Force Proton fsync on or off"),
|
||||||
(
|
(
|
||||||
@@ -136,30 +192,147 @@ pub fn setting_name_candidates() -> Vec<CompletionCandidate> {
|
|||||||
"post-launch",
|
"post-launch",
|
||||||
"Store a shell command for a future wrapped post-game hook",
|
"Store a shell command for a future wrapped post-game hook",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"hook-errors",
|
||||||
|
"Control pre-launch hook failure behavior (warn/fail)",
|
||||||
|
),
|
||||||
("host-libs", "Alias for steam-host-libs"),
|
("host-libs", "Alias for steam-host-libs"),
|
||||||
]
|
];
|
||||||
.into_iter()
|
|
||||||
.map(|(value, help)| CompletionCandidate::new(value).help(Some(help.into())))
|
pub fn setting_name_candidates() -> Vec<CompletionCandidate> {
|
||||||
|
SETTING_NAME_HINTS
|
||||||
|
.iter()
|
||||||
|
.map(|(value, help)| CompletionCandidate::new(*value).help(Some((*help).into())))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setting_value_candidates() -> Vec<CompletionCandidate> {
|
pub fn setting_value_candidates() -> Vec<CompletionCandidate> {
|
||||||
[
|
// In completion mode, gamewrap is invoked with the full command line as process args.
|
||||||
|
// The setting name is always the second-to-last arg (last is the partial value being typed).
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
let setting = args
|
||||||
|
.len()
|
||||||
|
.checked_sub(2)
|
||||||
|
.and_then(|i| args.get(i))
|
||||||
|
.map(String::as_str);
|
||||||
|
candidates_for_setting(setting)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidates_for_setting(setting: Option<&str>) -> Vec<CompletionCandidate> {
|
||||||
|
let c = |v: &'static str, h: &'static str| CompletionCandidate::new(v).help(Some(h.into()));
|
||||||
|
let toggle = || {
|
||||||
|
vec![
|
||||||
|
c("on", "Enable the setting"),
|
||||||
|
c("off", "Disable the setting"),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
match setting {
|
||||||
|
Some(
|
||||||
|
"mangohud"
|
||||||
|
| "gamemode"
|
||||||
|
| "steam-host-libs"
|
||||||
|
| "host-libs"
|
||||||
|
| "verbose"
|
||||||
|
| "log-file"
|
||||||
|
| "gamescope"
|
||||||
|
| "gamescope-adaptive-sync"
|
||||||
|
| "gamescope-hdr"
|
||||||
|
| "gamescope-steam"
|
||||||
|
| "gamescope-expose-wayland"
|
||||||
|
| "gamescope-mangoapp"
|
||||||
|
| "mangohud-log"
|
||||||
|
| "vkbasalt"
|
||||||
|
| "esync"
|
||||||
|
| "fsync"
|
||||||
|
| "large-address-aware"
|
||||||
|
| "laa",
|
||||||
|
) => toggle(),
|
||||||
|
Some("game-libs") => vec![
|
||||||
|
c("auto", "Automatic library path handling"),
|
||||||
|
c("keep", "Keep current library path unchanged"),
|
||||||
|
c("gamemode", "Always add GameMode library paths"),
|
||||||
|
],
|
||||||
|
Some("gamescope-scaler") => vec![
|
||||||
|
c("auto", "Automatic scaling"),
|
||||||
|
c("integer", "Integer scale mode"),
|
||||||
|
c("fit", "Fit to output resolution"),
|
||||||
|
c("fill", "Fill output resolution"),
|
||||||
|
c("stretch", "Stretch to output resolution"),
|
||||||
|
],
|
||||||
|
Some("gamescope-filter") => vec![
|
||||||
|
c("linear", "Linear filter"),
|
||||||
|
c("nearest", "Nearest-neighbor filter"),
|
||||||
|
c("fsr", "AMD FidelityFX Super Resolution upscaling"),
|
||||||
|
c("nis", "NVIDIA Image Scaling upscaling"),
|
||||||
|
c("pixel", "Pixel-art filter"),
|
||||||
|
],
|
||||||
|
Some("gamescope-mode") => vec![
|
||||||
|
c("windowed", "Windowed mode"),
|
||||||
|
c("borderless", "Borderless windowed mode"),
|
||||||
|
c("fullscreen", "Fullscreen mode"),
|
||||||
|
],
|
||||||
|
Some("vkbasalt-log-level") => vec![
|
||||||
|
c("debug", "Debug logging"),
|
||||||
|
c("info", "Informational logging"),
|
||||||
|
c("warning", "Warning logging"),
|
||||||
|
c("error", "Error logging"),
|
||||||
|
c("none", "Disable logging"),
|
||||||
|
],
|
||||||
|
Some("hook-errors") => vec![
|
||||||
|
c("warn", "Log hook failure and continue launching (default)"),
|
||||||
|
c("fail", "Abort launch when pre-launch hook fails"),
|
||||||
|
],
|
||||||
|
// Numeric and free-text settings have no useful static candidates
|
||||||
|
Some(
|
||||||
|
"gamescope-width"
|
||||||
|
| "gamescope-height"
|
||||||
|
| "gamescope-nested-width"
|
||||||
|
| "gamescope-nested-height"
|
||||||
|
| "gamescope-fps"
|
||||||
|
| "gamescope-unfocused-fps"
|
||||||
|
| "gamescope-sharpness"
|
||||||
|
| "fps-cap"
|
||||||
|
| "log-path"
|
||||||
|
| "mangohud-log-path"
|
||||||
|
| "vkbasalt-config"
|
||||||
|
| "pre-launch"
|
||||||
|
| "post-launch",
|
||||||
|
) => vec![],
|
||||||
|
// Unknown setting or no context — show all possible values
|
||||||
|
_ => [
|
||||||
("on", "Enable the setting"),
|
("on", "Enable the setting"),
|
||||||
("off", "Disable the setting"),
|
("off", "Disable the setting"),
|
||||||
("auto", "Use automatic behavior"),
|
("auto", "Use automatic behavior"),
|
||||||
("keep", "Keep the current value unchanged"),
|
("keep", "Keep current value unchanged"),
|
||||||
("gamemode", "Always add GameMode library paths"),
|
("gamemode", "Always add GameMode library paths"),
|
||||||
|
("fsr", "AMD FidelityFX Super Resolution upscaling"),
|
||||||
|
("nis", "NVIDIA Image Scaling upscaling"),
|
||||||
|
("linear", "Linear filter"),
|
||||||
|
("nearest", "Nearest-neighbor filter"),
|
||||||
|
("pixel", "Pixel-art filter"),
|
||||||
|
("integer", "Integer scale mode"),
|
||||||
|
("fit", "Fit to output resolution"),
|
||||||
|
("fill", "Fill output resolution"),
|
||||||
|
("stretch", "Stretch to output resolution"),
|
||||||
|
("windowed", "Windowed mode"),
|
||||||
|
("borderless", "Borderless windowed mode"),
|
||||||
|
("fullscreen", "Fullscreen mode"),
|
||||||
|
("debug", "Enable debug logging"),
|
||||||
|
("info", "Informational logging"),
|
||||||
|
("warning", "Warning logging"),
|
||||||
|
("error", "Error logging"),
|
||||||
|
("none", "Disable tool logging"),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(value, help)| CompletionCandidate::new(value).help(Some(help.into())))
|
.map(|(v, h)| c(v, h))
|
||||||
.collect()
|
.collect(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn named_profile_candidates() -> Vec<CompletionCandidate> {
|
pub fn named_profile_candidates() -> Vec<CompletionCandidate> {
|
||||||
load_named_profiles()
|
load_named_profiles()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|name| CompletionCandidate::new(name))
|
.map(CompletionCandidate::new)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,11 +474,8 @@ fn startup_block(shell: Shell, script_path: &Path) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shell_path_literal(shell: Shell, path: &Path) -> String {
|
fn shell_path_literal(_shell: Shell, path: &Path) -> String {
|
||||||
match shell {
|
format!("'{}'", escape_single_quotes(path))
|
||||||
Shell::PowerShell => format!("'{}'", escape_single_quotes(path)),
|
|
||||||
_ => format!("'{}'", escape_single_quotes(path)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape_single_quotes(path: &Path) -> Cow<'_, str> {
|
fn escape_single_quotes(path: &Path) -> Cow<'_, str> {
|
||||||
|
|||||||
+223
-23
@@ -1,48 +1,89 @@
|
|||||||
use crate::config::{GameLibsMode, Settings};
|
use crate::config::{
|
||||||
|
GameLibsMode, GamescopeFilter, GamescopeScaler, GamescopeSize, GamescopeWindowMode, Settings,
|
||||||
|
VkbasaltLogLevel,
|
||||||
|
};
|
||||||
use crate::error::{AppError, config_error};
|
use crate::error::{AppError, config_error};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SettingKey {
|
pub enum SettingKey {
|
||||||
Overlay,
|
Mangohud,
|
||||||
Performance,
|
Gamemode,
|
||||||
SteamHostLibs,
|
SteamHostLibs,
|
||||||
GameLibs,
|
GameLibs,
|
||||||
Verbose,
|
Verbose,
|
||||||
|
LogFile,
|
||||||
|
LogPath,
|
||||||
Gamescope,
|
Gamescope,
|
||||||
GamescopeWidth,
|
GamescopeWidth,
|
||||||
GamescopeHeight,
|
GamescopeHeight,
|
||||||
GamescopeFps,
|
GamescopeFps,
|
||||||
|
GamescopeNestedWidth,
|
||||||
|
GamescopeNestedHeight,
|
||||||
|
GamescopeUnfocusedFps,
|
||||||
|
GamescopeScaler,
|
||||||
|
GamescopeFilter,
|
||||||
|
GamescopeSharpness,
|
||||||
|
GamescopeMode,
|
||||||
|
GamescopeAdaptiveSync,
|
||||||
|
GamescopeHdr,
|
||||||
|
GamescopeSteam,
|
||||||
|
GamescopeExposeWayland,
|
||||||
|
GamescopeMangoapp,
|
||||||
FpsCap,
|
FpsCap,
|
||||||
|
MangohudLog,
|
||||||
|
MangohudLogPath,
|
||||||
Vkbasalt,
|
Vkbasalt,
|
||||||
|
VkbasaltConfig,
|
||||||
|
VkbasaltLogLevel,
|
||||||
Esync,
|
Esync,
|
||||||
Fsync,
|
Fsync,
|
||||||
LargeAddressAware,
|
LargeAddressAware,
|
||||||
PreLaunch,
|
PreLaunch,
|
||||||
PostLaunch,
|
PostLaunch,
|
||||||
|
HookErrors,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SettingKey {
|
impl SettingKey {
|
||||||
pub fn parse(value: &str) -> Result<Self, AppError> {
|
pub fn parse(value: &str) -> Result<Self, AppError> {
|
||||||
match value {
|
match value {
|
||||||
"overlay" => Ok(Self::Overlay),
|
"mangohud" => Ok(Self::Mangohud),
|
||||||
"performance" => Ok(Self::Performance),
|
"gamemode" => Ok(Self::Gamemode),
|
||||||
"steam-host-libs" | "host-libs" => Ok(Self::SteamHostLibs),
|
"steam-host-libs" | "host-libs" => Ok(Self::SteamHostLibs),
|
||||||
"game-libs" => Ok(Self::GameLibs),
|
"game-libs" => Ok(Self::GameLibs),
|
||||||
"verbose" => Ok(Self::Verbose),
|
"verbose" => Ok(Self::Verbose),
|
||||||
|
"log-file" => Ok(Self::LogFile),
|
||||||
|
"log-path" => Ok(Self::LogPath),
|
||||||
"gamescope" => Ok(Self::Gamescope),
|
"gamescope" => Ok(Self::Gamescope),
|
||||||
"gamescope-width" => Ok(Self::GamescopeWidth),
|
"gamescope-width" => Ok(Self::GamescopeWidth),
|
||||||
"gamescope-height" => Ok(Self::GamescopeHeight),
|
"gamescope-height" => Ok(Self::GamescopeHeight),
|
||||||
"gamescope-fps" => Ok(Self::GamescopeFps),
|
"gamescope-fps" => Ok(Self::GamescopeFps),
|
||||||
|
"gamescope-nested-width" => Ok(Self::GamescopeNestedWidth),
|
||||||
|
"gamescope-nested-height" => Ok(Self::GamescopeNestedHeight),
|
||||||
|
"gamescope-unfocused-fps" => Ok(Self::GamescopeUnfocusedFps),
|
||||||
|
"gamescope-scaler" => Ok(Self::GamescopeScaler),
|
||||||
|
"gamescope-filter" => Ok(Self::GamescopeFilter),
|
||||||
|
"gamescope-sharpness" => Ok(Self::GamescopeSharpness),
|
||||||
|
"gamescope-mode" => Ok(Self::GamescopeMode),
|
||||||
|
"gamescope-adaptive-sync" => Ok(Self::GamescopeAdaptiveSync),
|
||||||
|
"gamescope-hdr" => Ok(Self::GamescopeHdr),
|
||||||
|
"gamescope-steam" => Ok(Self::GamescopeSteam),
|
||||||
|
"gamescope-expose-wayland" => Ok(Self::GamescopeExposeWayland),
|
||||||
|
"gamescope-mangoapp" => Ok(Self::GamescopeMangoapp),
|
||||||
"fps-cap" => Ok(Self::FpsCap),
|
"fps-cap" => Ok(Self::FpsCap),
|
||||||
|
"mangohud-log" => Ok(Self::MangohudLog),
|
||||||
|
"mangohud-log-path" => Ok(Self::MangohudLogPath),
|
||||||
"vkbasalt" => Ok(Self::Vkbasalt),
|
"vkbasalt" => Ok(Self::Vkbasalt),
|
||||||
|
"vkbasalt-config" => Ok(Self::VkbasaltConfig),
|
||||||
|
"vkbasalt-log-level" => Ok(Self::VkbasaltLogLevel),
|
||||||
"esync" => Ok(Self::Esync),
|
"esync" => Ok(Self::Esync),
|
||||||
"fsync" => Ok(Self::Fsync),
|
"fsync" => Ok(Self::Fsync),
|
||||||
"large-address-aware" | "laa" => Ok(Self::LargeAddressAware),
|
"large-address-aware" | "laa" => Ok(Self::LargeAddressAware),
|
||||||
"pre-launch" => Ok(Self::PreLaunch),
|
"pre-launch" => Ok(Self::PreLaunch),
|
||||||
"post-launch" => Ok(Self::PostLaunch),
|
"post-launch" => Ok(Self::PostLaunch),
|
||||||
|
"hook-errors" => Ok(Self::HookErrors),
|
||||||
_ => Err(config_error(
|
_ => Err(config_error(
|
||||||
format!("`{value}` is not a known setting."),
|
format!("`{value}` is not a known setting."),
|
||||||
"Valid settings: overlay, performance, steam-host-libs, game-libs, verbose, gamescope, gamescope-width, gamescope-height, gamescope-fps, fps-cap, vkbasalt, esync, fsync, large-address-aware, pre-launch, post-launch.",
|
"Run `gamewrap help settings` to see all available settings.",
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,58 +91,114 @@ impl SettingKey {
|
|||||||
|
|
||||||
pub fn set_value(settings: &mut Settings, key: SettingKey, value: &str) -> Result<(), AppError> {
|
pub fn set_value(settings: &mut Settings, key: SettingKey, value: &str) -> Result<(), AppError> {
|
||||||
match key {
|
match key {
|
||||||
SettingKey::Overlay => settings.overlay = Some(parse_toggle(value)?),
|
SettingKey::Mangohud => settings.mangohud = Some(parse_toggle(value)?),
|
||||||
SettingKey::Performance => settings.performance = Some(parse_toggle(value)?),
|
SettingKey::Gamemode => settings.gamemode = Some(parse_toggle(value)?),
|
||||||
SettingKey::SteamHostLibs => settings.steam_host_libs = Some(parse_toggle(value)?),
|
SettingKey::SteamHostLibs => settings.steam_host_libs = Some(parse_toggle(value)?),
|
||||||
SettingKey::GameLibs => settings.game_libs = Some(parse_game_libs(value)?),
|
SettingKey::GameLibs => settings.game_libs = Some(parse_game_libs(value)?),
|
||||||
SettingKey::Verbose => settings.verbose = Some(parse_toggle(value)?),
|
SettingKey::Verbose => settings.verbose = Some(parse_toggle(value)?),
|
||||||
|
SettingKey::LogFile => settings.log_file = Some(parse_toggle(value)?),
|
||||||
|
SettingKey::LogPath => settings.log_path = Some(value.to_string()),
|
||||||
SettingKey::Gamescope => settings.gamescope = Some(parse_toggle(value)?),
|
SettingKey::Gamescope => settings.gamescope = Some(parse_toggle(value)?),
|
||||||
SettingKey::GamescopeWidth => {
|
SettingKey::GamescopeWidth => {
|
||||||
settings.gamescope_width = Some(parse_pixel_count(value)?);
|
settings.gamescope_width = Some(parse_gamescope_size(value)?);
|
||||||
}
|
}
|
||||||
SettingKey::GamescopeHeight => {
|
SettingKey::GamescopeHeight => {
|
||||||
settings.gamescope_height = Some(parse_pixel_count(value)?);
|
settings.gamescope_height = Some(parse_gamescope_size(value)?);
|
||||||
}
|
}
|
||||||
SettingKey::GamescopeFps => {
|
SettingKey::GamescopeFps => {
|
||||||
settings.gamescope_fps = Some(parse_gamescope_fps(value)?);
|
settings.gamescope_fps = Some(parse_fps(value, "gamescope-fps")?);
|
||||||
}
|
}
|
||||||
SettingKey::FpsCap => {
|
SettingKey::GamescopeNestedWidth => {
|
||||||
let n: u32 = value.parse().map_err(|_| {
|
settings.gamescope_nested_width = Some(parse_pixel_count(value)?);
|
||||||
config_error(
|
|
||||||
format!("`{value}` is not a valid FPS cap. Use a number like `60` or `120`."),
|
|
||||||
"Use `gamewrap config set fps-cap 60` for a 60 FPS cap.",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
settings.fps_cap = Some(n);
|
|
||||||
}
|
}
|
||||||
|
SettingKey::GamescopeNestedHeight => {
|
||||||
|
settings.gamescope_nested_height = Some(parse_pixel_count(value)?);
|
||||||
|
}
|
||||||
|
SettingKey::GamescopeUnfocusedFps => {
|
||||||
|
settings.gamescope_unfocused_fps = Some(parse_fps(value, "gamescope-unfocused-fps")?);
|
||||||
|
}
|
||||||
|
SettingKey::GamescopeScaler => {
|
||||||
|
settings.gamescope_scaler = Some(parse_gamescope_scaler(value)?);
|
||||||
|
}
|
||||||
|
SettingKey::GamescopeFilter => {
|
||||||
|
settings.gamescope_filter = Some(parse_gamescope_filter(value)?);
|
||||||
|
}
|
||||||
|
SettingKey::GamescopeSharpness => {
|
||||||
|
settings.gamescope_sharpness = Some(parse_sharpness(value)?);
|
||||||
|
}
|
||||||
|
SettingKey::GamescopeMode => {
|
||||||
|
settings.gamescope_window_mode = Some(parse_gamescope_window_mode(value)?);
|
||||||
|
}
|
||||||
|
SettingKey::GamescopeAdaptiveSync => {
|
||||||
|
settings.gamescope_adaptive_sync = Some(parse_toggle(value)?);
|
||||||
|
}
|
||||||
|
SettingKey::GamescopeHdr => {
|
||||||
|
settings.gamescope_hdr = Some(parse_toggle(value)?);
|
||||||
|
}
|
||||||
|
SettingKey::GamescopeSteam => {
|
||||||
|
settings.gamescope_steam = Some(parse_toggle(value)?);
|
||||||
|
}
|
||||||
|
SettingKey::GamescopeExposeWayland => {
|
||||||
|
settings.gamescope_expose_wayland = Some(parse_toggle(value)?);
|
||||||
|
}
|
||||||
|
SettingKey::GamescopeMangoapp => {
|
||||||
|
settings.gamescope_mangoapp = Some(parse_toggle(value)?);
|
||||||
|
}
|
||||||
|
SettingKey::FpsCap => settings.fps_cap = Some(parse_fps(value, "fps-cap")?),
|
||||||
|
SettingKey::MangohudLog => settings.mangohud_log = Some(parse_toggle(value)?),
|
||||||
|
SettingKey::MangohudLogPath => settings.mangohud_log_path = Some(value.to_string()),
|
||||||
SettingKey::Vkbasalt => settings.vkbasalt = Some(parse_toggle(value)?),
|
SettingKey::Vkbasalt => settings.vkbasalt = Some(parse_toggle(value)?),
|
||||||
|
SettingKey::VkbasaltConfig => settings.vkbasalt_config = Some(value.to_string()),
|
||||||
|
SettingKey::VkbasaltLogLevel => {
|
||||||
|
settings.vkbasalt_log_level = Some(parse_vkbasalt_log_level(value)?);
|
||||||
|
}
|
||||||
SettingKey::Esync => settings.esync = Some(parse_toggle(value)?),
|
SettingKey::Esync => settings.esync = Some(parse_toggle(value)?),
|
||||||
SettingKey::Fsync => settings.fsync = Some(parse_toggle(value)?),
|
SettingKey::Fsync => settings.fsync = Some(parse_toggle(value)?),
|
||||||
SettingKey::LargeAddressAware => settings.large_address_aware = Some(parse_toggle(value)?),
|
SettingKey::LargeAddressAware => settings.large_address_aware = Some(parse_toggle(value)?),
|
||||||
SettingKey::PreLaunch => settings.pre_launch = Some(value.to_string()),
|
SettingKey::PreLaunch => settings.pre_launch = Some(value.to_string()),
|
||||||
SettingKey::PostLaunch => settings.post_launch = Some(value.to_string()),
|
SettingKey::PostLaunch => settings.post_launch = Some(value.to_string()),
|
||||||
|
SettingKey::HookErrors => settings.hook_errors = Some(parse_hook_errors(value)?),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_value(settings: &mut Settings, key: SettingKey) {
|
pub fn reset_value(settings: &mut Settings, key: SettingKey) {
|
||||||
match key {
|
match key {
|
||||||
SettingKey::Overlay => settings.overlay = None,
|
SettingKey::Mangohud => settings.mangohud = None,
|
||||||
SettingKey::Performance => settings.performance = None,
|
SettingKey::Gamemode => settings.gamemode = None,
|
||||||
SettingKey::SteamHostLibs => settings.steam_host_libs = None,
|
SettingKey::SteamHostLibs => settings.steam_host_libs = None,
|
||||||
SettingKey::GameLibs => settings.game_libs = None,
|
SettingKey::GameLibs => settings.game_libs = None,
|
||||||
SettingKey::Verbose => settings.verbose = None,
|
SettingKey::Verbose => settings.verbose = None,
|
||||||
|
SettingKey::LogFile => settings.log_file = None,
|
||||||
|
SettingKey::LogPath => settings.log_path = None,
|
||||||
SettingKey::Gamescope => settings.gamescope = None,
|
SettingKey::Gamescope => settings.gamescope = None,
|
||||||
SettingKey::GamescopeWidth => settings.gamescope_width = None,
|
SettingKey::GamescopeWidth => settings.gamescope_width = None,
|
||||||
SettingKey::GamescopeHeight => settings.gamescope_height = None,
|
SettingKey::GamescopeHeight => settings.gamescope_height = None,
|
||||||
SettingKey::GamescopeFps => settings.gamescope_fps = None,
|
SettingKey::GamescopeFps => settings.gamescope_fps = None,
|
||||||
|
SettingKey::GamescopeNestedWidth => settings.gamescope_nested_width = None,
|
||||||
|
SettingKey::GamescopeNestedHeight => settings.gamescope_nested_height = None,
|
||||||
|
SettingKey::GamescopeUnfocusedFps => settings.gamescope_unfocused_fps = None,
|
||||||
|
SettingKey::GamescopeScaler => settings.gamescope_scaler = None,
|
||||||
|
SettingKey::GamescopeFilter => settings.gamescope_filter = None,
|
||||||
|
SettingKey::GamescopeSharpness => settings.gamescope_sharpness = None,
|
||||||
|
SettingKey::GamescopeMode => settings.gamescope_window_mode = None,
|
||||||
|
SettingKey::GamescopeAdaptiveSync => settings.gamescope_adaptive_sync = None,
|
||||||
|
SettingKey::GamescopeHdr => settings.gamescope_hdr = None,
|
||||||
|
SettingKey::GamescopeSteam => settings.gamescope_steam = None,
|
||||||
|
SettingKey::GamescopeExposeWayland => settings.gamescope_expose_wayland = None,
|
||||||
|
SettingKey::GamescopeMangoapp => settings.gamescope_mangoapp = None,
|
||||||
SettingKey::FpsCap => settings.fps_cap = None,
|
SettingKey::FpsCap => settings.fps_cap = None,
|
||||||
|
SettingKey::MangohudLog => settings.mangohud_log = None,
|
||||||
|
SettingKey::MangohudLogPath => settings.mangohud_log_path = None,
|
||||||
SettingKey::Vkbasalt => settings.vkbasalt = None,
|
SettingKey::Vkbasalt => settings.vkbasalt = None,
|
||||||
|
SettingKey::VkbasaltConfig => settings.vkbasalt_config = None,
|
||||||
|
SettingKey::VkbasaltLogLevel => settings.vkbasalt_log_level = None,
|
||||||
SettingKey::Esync => settings.esync = None,
|
SettingKey::Esync => settings.esync = None,
|
||||||
SettingKey::Fsync => settings.fsync = None,
|
SettingKey::Fsync => settings.fsync = None,
|
||||||
SettingKey::LargeAddressAware => settings.large_address_aware = None,
|
SettingKey::LargeAddressAware => settings.large_address_aware = None,
|
||||||
SettingKey::PreLaunch => settings.pre_launch = None,
|
SettingKey::PreLaunch => settings.pre_launch = None,
|
||||||
SettingKey::PostLaunch => settings.post_launch = None,
|
SettingKey::PostLaunch => settings.post_launch = None,
|
||||||
|
SettingKey::HookErrors => settings.hook_errors = None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +225,91 @@ fn parse_game_libs(value: &str) -> Result<GameLibsMode, AppError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_vkbasalt_log_level(value: &str) -> Result<VkbasaltLogLevel, AppError> {
|
||||||
|
match value {
|
||||||
|
"debug" => Ok(VkbasaltLogLevel::Debug),
|
||||||
|
"info" => Ok(VkbasaltLogLevel::Info),
|
||||||
|
"warning" => Ok(VkbasaltLogLevel::Warning),
|
||||||
|
"error" => Ok(VkbasaltLogLevel::Error),
|
||||||
|
"none" => Ok(VkbasaltLogLevel::None),
|
||||||
|
_ => Err(config_error(
|
||||||
|
format!("`{value}` is not a valid vkBasalt log level."),
|
||||||
|
"Use one of: debug, info, warning, error, none.",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_gamescope_scaler(value: &str) -> Result<GamescopeScaler, AppError> {
|
||||||
|
match value {
|
||||||
|
"auto" => Ok(GamescopeScaler::Auto),
|
||||||
|
"integer" => Ok(GamescopeScaler::Integer),
|
||||||
|
"fit" => Ok(GamescopeScaler::Fit),
|
||||||
|
"fill" => Ok(GamescopeScaler::Fill),
|
||||||
|
"stretch" => Ok(GamescopeScaler::Stretch),
|
||||||
|
_ => Err(config_error(
|
||||||
|
format!("`{value}` is not a valid gamescope scaler."),
|
||||||
|
"Use one of: auto, integer, fit, fill, stretch.",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_gamescope_filter(value: &str) -> Result<GamescopeFilter, AppError> {
|
||||||
|
match value {
|
||||||
|
"linear" => Ok(GamescopeFilter::Linear),
|
||||||
|
"nearest" => Ok(GamescopeFilter::Nearest),
|
||||||
|
"fsr" => Ok(GamescopeFilter::Fsr),
|
||||||
|
"nis" => Ok(GamescopeFilter::Nis),
|
||||||
|
"pixel" => Ok(GamescopeFilter::Pixel),
|
||||||
|
_ => Err(config_error(
|
||||||
|
format!("`{value}` is not a valid gamescope filter."),
|
||||||
|
"Use one of: linear, nearest, fsr, nis, pixel.",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_gamescope_size(value: &str) -> Result<GamescopeSize, AppError> {
|
||||||
|
if value == "native" {
|
||||||
|
return Ok(GamescopeSize::Native);
|
||||||
|
}
|
||||||
|
let n: u32 = value.parse().map_err(|_| {
|
||||||
|
config_error(
|
||||||
|
format!(
|
||||||
|
"`{value}` is not a valid resolution. Use a pixel count like `1920` or `native`."
|
||||||
|
),
|
||||||
|
"Use a whole-number pixel count or `native` to auto-detect the display resolution.",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(GamescopeSize::Pixels(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_gamescope_window_mode(value: &str) -> Result<GamescopeWindowMode, AppError> {
|
||||||
|
match value {
|
||||||
|
"windowed" => Ok(GamescopeWindowMode::Windowed),
|
||||||
|
"borderless" => Ok(GamescopeWindowMode::Borderless),
|
||||||
|
"fullscreen" => Ok(GamescopeWindowMode::Fullscreen),
|
||||||
|
_ => Err(config_error(
|
||||||
|
format!("`{value}` is not a valid window mode."),
|
||||||
|
"Use one of: windowed, borderless, fullscreen.",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_sharpness(value: &str) -> Result<u8, AppError> {
|
||||||
|
let n: u8 = value.parse().map_err(|_| {
|
||||||
|
config_error(
|
||||||
|
format!("`{value}` is not a valid sharpness value. Use a number from 0 to 20."),
|
||||||
|
"0 is maximum sharpness, 20 is minimum sharpness.",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if n > 20 {
|
||||||
|
return Err(config_error(
|
||||||
|
format!("`{value}` is out of range. Sharpness must be 0–20."),
|
||||||
|
"0 is maximum sharpness, 20 is minimum sharpness.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_pixel_count(value: &str) -> Result<u32, AppError> {
|
fn parse_pixel_count(value: &str) -> Result<u32, AppError> {
|
||||||
value.parse::<u32>().map_err(|_| {
|
value.parse::<u32>().map_err(|_| {
|
||||||
config_error(
|
config_error(
|
||||||
@@ -137,11 +319,29 @@ fn parse_pixel_count(value: &str) -> Result<u32, AppError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_gamescope_fps(value: &str) -> Result<u32, AppError> {
|
fn parse_fps(value: &str, setting: &str) -> Result<u32, AppError> {
|
||||||
value.parse::<u32>().map_err(|_| {
|
value.parse::<u32>().map_err(|_| {
|
||||||
|
if setting == "fps-cap" {
|
||||||
|
config_error(
|
||||||
|
format!("`{value}` is not a valid FPS cap. Use a number like `60` or `120`."),
|
||||||
|
"Use `gamewrap config set fps-cap 60` for a 60 FPS cap.",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
config_error(
|
config_error(
|
||||||
format!("`{value}` is not a valid FPS. Use a number like `60`."),
|
format!("`{value}` is not a valid FPS. Use a number like `60`."),
|
||||||
"Use `gamewrap config set gamescope-fps 60` for a 60 FPS gamescope target.",
|
format!("Use `gamewrap config set {setting} 60` for a 60 FPS target."),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_hook_errors(value: &str) -> Result<crate::config::HookErrors, AppError> {
|
||||||
|
match value {
|
||||||
|
"warn" => Ok(crate::config::HookErrors::Warn),
|
||||||
|
"fail" => Ok(crate::config::HookErrors::Fail),
|
||||||
|
_ => Err(config_error(
|
||||||
|
format!("`{value}` is not a valid hook-errors value."),
|
||||||
|
"Use `warn` (log and continue) or `fail` (abort launch on hook failure).",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
/// Rename table: (old_key, new_key). Grows over time as field names change.
|
||||||
|
pub static SETTING_RENAMES: &[(&str, &str)] =
|
||||||
|
&[("overlay", "mangohud"), ("performance", "gamemode")];
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum MigrateAction {
|
||||||
|
/// Old key renamed to new key, value transferred.
|
||||||
|
Renamed { from: String, to: String },
|
||||||
|
/// Old key removed because new key already had a value.
|
||||||
|
Removed { old: String, kept: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply SETTING_RENAMES to a Settings-shaped TOML table in place.
|
||||||
|
/// Returns the actions taken for retired keys.
|
||||||
|
fn migrate_settings_table(table: &mut toml::map::Map<String, toml::Value>) -> Vec<MigrateAction> {
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
for (old, new) in SETTING_RENAMES {
|
||||||
|
if let Some(value) = table.remove(*old) {
|
||||||
|
if !table.contains_key(*new) {
|
||||||
|
table.insert(new.to_string(), value);
|
||||||
|
actions.push(MigrateAction::Renamed {
|
||||||
|
from: old.to_string(),
|
||||||
|
to: new.to_string(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
actions.push(MigrateAction::Removed {
|
||||||
|
old: old.to_string(),
|
||||||
|
kept: new.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply SETTING_RENAMES to a ConfigFile-shaped TOML value.
|
||||||
|
/// Migrates [defaults] and each [profiles.<name>] section.
|
||||||
|
/// Returns the actions taken for retired keys.
|
||||||
|
pub fn migrate_config(value: &mut toml::Value) -> Vec<MigrateAction> {
|
||||||
|
let mut changes = Vec::new();
|
||||||
|
if let Some(table) = value.as_table_mut() {
|
||||||
|
if let Some(toml::Value::Table(defaults)) = table.get_mut("defaults") {
|
||||||
|
changes.extend(migrate_settings_table(defaults));
|
||||||
|
}
|
||||||
|
if let Some(toml::Value::Table(profiles)) = table.get_mut("profiles") {
|
||||||
|
let names: Vec<String> = profiles.keys().cloned().collect();
|
||||||
|
for name in names {
|
||||||
|
if let Some(toml::Value::Table(profile)) = profiles.get_mut(&name) {
|
||||||
|
changes.extend(migrate_settings_table(profile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply SETTING_RENAMES to a SharedProfileFile-shaped TOML value.
|
||||||
|
/// Migrates the [settings] section.
|
||||||
|
/// Returns the actions taken for retired keys.
|
||||||
|
pub fn migrate_profile_export(value: &mut toml::Value) -> Vec<MigrateAction> {
|
||||||
|
if let Some(toml::Value::Table(settings)) =
|
||||||
|
value.as_table_mut().and_then(|t| t.get_mut("settings"))
|
||||||
|
{
|
||||||
|
migrate_settings_table(settings)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check a settings-shaped TOML table for renamed-away setting keys.
|
||||||
|
/// Returns unrecognized key names so profile imports can warn about skipped fields.
|
||||||
|
pub fn unknown_settings_in_table(table: &toml::map::Map<String, toml::Value>) -> Vec<String> {
|
||||||
|
let old_names: HashSet<&str> = SETTING_RENAMES.iter().map(|(old, _)| *old).collect();
|
||||||
|
table
|
||||||
|
.keys()
|
||||||
|
.filter(|key| old_names.contains(key.as_str()))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_migration_preserves_current_values_on_conflict() {
|
||||||
|
let mut value: toml::Value = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[defaults]
|
||||||
|
overlay = false
|
||||||
|
mangohud = true
|
||||||
|
|
||||||
|
[profiles.benchmark]
|
||||||
|
performance = false
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("valid TOML");
|
||||||
|
|
||||||
|
let changes = migrate_config(&mut value);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
changes,
|
||||||
|
vec![
|
||||||
|
MigrateAction::Removed {
|
||||||
|
old: "overlay".to_string(),
|
||||||
|
kept: "mangohud".to_string(),
|
||||||
|
},
|
||||||
|
MigrateAction::Renamed {
|
||||||
|
from: "performance".to_string(),
|
||||||
|
to: "gamemode".to_string(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(value["defaults"]["mangohud"].as_bool(), Some(true));
|
||||||
|
assert!(value["defaults"].get("overlay").is_none());
|
||||||
|
assert_eq!(
|
||||||
|
value["profiles"]["benchmark"]["gamemode"].as_bool(),
|
||||||
|
Some(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+514
-100
@@ -1,4 +1,5 @@
|
|||||||
pub mod keys;
|
pub mod keys;
|
||||||
|
pub mod migrate;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -11,8 +12,9 @@ use crate::detect;
|
|||||||
use crate::error::{AppError, config_error, internal_error, io_to_internal};
|
use crate::error::{AppError, config_error, internal_error, io_to_internal};
|
||||||
|
|
||||||
pub use schema::{
|
pub use schema::{
|
||||||
Binding, ConfigFile, GameLibsMode, ObservedGame, ProfileConfig, ResolvedSettings, Settings,
|
Binding, ConfigFile, GameLibsMode, GamescopeFilter, GamescopeScaler, GamescopeSize,
|
||||||
StateFile,
|
GamescopeWindowMode, HookErrors, ObservedGame, ProfileConfig, ResolvedSettings, Settings,
|
||||||
|
StateFile, VkbasaltLogLevel,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -59,6 +61,7 @@ pub fn load(paths: &AppPaths) -> Result<ConfigFile, AppError> {
|
|||||||
error,
|
error,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
let content = migrate_content_if_needed(content, &paths.config_file);
|
||||||
|
|
||||||
let config = toml::from_str(&content).map_err(|error| {
|
let config = toml::from_str(&content).map_err(|error| {
|
||||||
config_error(
|
config_error(
|
||||||
@@ -70,6 +73,27 @@ pub fn load(paths: &AppPaths) -> Result<ConfigFile, AppError> {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn migrate_content_if_needed(content: String, path: &Path) -> String {
|
||||||
|
let mut value: toml::Value = match toml::from_str(&content) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return content,
|
||||||
|
};
|
||||||
|
let changes = migrate::migrate_config(&mut value);
|
||||||
|
if changes.is_empty() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
if let Ok(migrated) = toml::to_string_pretty(&value) {
|
||||||
|
if let Err(e) = fs::write(path, &migrated) {
|
||||||
|
eprintln!(
|
||||||
|
"warning: could not update config file after migration ({}): {e}",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
pub fn save(paths: &AppPaths, config: &ConfigFile) -> Result<(), AppError> {
|
pub fn save(paths: &AppPaths, config: &ConfigFile) -> Result<(), AppError> {
|
||||||
crate::profile::validate_config(config)?;
|
crate::profile::validate_config(config)?;
|
||||||
ensure_dir(&paths.config_dir)?;
|
ensure_dir(&paths.config_dir)?;
|
||||||
@@ -121,31 +145,33 @@ fn ensure_dir(path: &Path) -> Result<(), AppError> {
|
|||||||
.map_err(|error| internal_error(format!("failed creating {}: {error}", path.display())))
|
.map_err(|error| internal_error(format!("failed creating {}: {error}", path.display())))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_config(config: &ConfigFile) -> String {
|
pub fn render_config(config: &ConfigFile, effective: bool) -> String {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
let mut resolved_defaults = ResolvedSettings::default();
|
let mut resolved_defaults = ResolvedSettings::default();
|
||||||
resolved_defaults.apply(&config.defaults);
|
resolved_defaults.apply(&config.defaults);
|
||||||
|
|
||||||
output.push_str(&format!("{}\n", crate::color::bold("[defaults]")));
|
output.push_str(&format!("{}\n", crate::color::bold("[defaults]")));
|
||||||
output.push_str(&render_resolved_settings(&resolved_defaults));
|
let sparse = if effective {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&config.defaults)
|
||||||
|
};
|
||||||
|
output.push_str(&render_resolved_settings(&resolved_defaults, sparse));
|
||||||
|
|
||||||
output.push_str(&format!("\n{}\n", crate::color::bold("[profiles]")));
|
output.push_str(&format!("\n{}\n", crate::color::bold("[profiles]")));
|
||||||
if config.profiles.is_empty() {
|
if config.profiles.is_empty() {
|
||||||
output.push_str(&format!(" {}\n", crate::color::dim("(none)")));
|
output.push_str(&format!(" {}\n", crate::color::dim("(none)")));
|
||||||
} else {
|
} else {
|
||||||
for (name, profile) in &config.profiles {
|
for (name, profile) in &config.profiles {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(" {}\n", crate::color::accent(name)));
|
||||||
" {}{}\n",
|
let resolved = crate::profile::resolve_named(config, name)
|
||||||
crate::color::accent(name),
|
|
||||||
crate::color::dim(&inherits_suffix(&profile.inherits))
|
|
||||||
));
|
|
||||||
let inherited = crate::profile::resolve_named(config, name)
|
|
||||||
.map(|resolved| resolved.settings)
|
.map(|resolved| resolved.settings)
|
||||||
.unwrap_or_else(|_| resolved_defaults.clone());
|
.unwrap_or_else(|_| resolved_defaults.clone());
|
||||||
output.push_str(&render_profile_settings_with_indent(
|
output.push_str(&render_profile_settings_with_indent(
|
||||||
&profile.settings,
|
&profile.settings,
|
||||||
&inherited,
|
&resolved,
|
||||||
" ",
|
" ",
|
||||||
|
effective,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,41 +193,41 @@ pub fn render_config(config: &ConfigFile) -> String {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_profile(name: &str, profile: &ProfileConfig, inherited: &ResolvedSettings) -> String {
|
pub fn render_profile(
|
||||||
|
name: &str,
|
||||||
|
profile: &ProfileConfig,
|
||||||
|
effective_settings: &ResolvedSettings,
|
||||||
|
effective: bool,
|
||||||
|
) -> String {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
output.push_str(&crate::color::accent(name));
|
output.push_str(&crate::color::accent(name));
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
if let Some(parent) = &profile.inherits {
|
|
||||||
output.push_str(&format!(
|
|
||||||
"{} {}\n",
|
|
||||||
crate::color::dim("inherits ="),
|
|
||||||
crate::color::accent(parent)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
output.push_str(&render_profile_settings_with_indent(
|
output.push_str(&render_profile_settings_with_indent(
|
||||||
&profile.settings,
|
&profile.settings,
|
||||||
inherited,
|
effective_settings,
|
||||||
"",
|
"",
|
||||||
|
effective,
|
||||||
));
|
));
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_profile_settings_with_indent(
|
fn render_profile_settings_with_indent(
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
inherited: &ResolvedSettings,
|
effective_settings: &ResolvedSettings,
|
||||||
indent: &str,
|
indent: &str,
|
||||||
|
effective: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut fields = BTreeMap::new();
|
let mut fields = BTreeMap::new();
|
||||||
fields.insert(
|
fields.insert(
|
||||||
"overlay",
|
"mangohud",
|
||||||
settings
|
settings
|
||||||
.overlay
|
.mangohud
|
||||||
.map(|value| if value { "on" } else { "off" }.to_string()),
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
);
|
);
|
||||||
fields.insert(
|
fields.insert(
|
||||||
"performance",
|
"gamemode",
|
||||||
settings
|
settings
|
||||||
.performance
|
.gamemode
|
||||||
.map(|value| if value { "on" } else { "off" }.to_string()),
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
);
|
);
|
||||||
fields.insert(
|
fields.insert(
|
||||||
@@ -220,67 +246,148 @@ fn render_profile_settings_with_indent(
|
|||||||
.verbose
|
.verbose
|
||||||
.map(|value| if value { "on" } else { "off" }.to_string()),
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
);
|
);
|
||||||
|
fields.insert(
|
||||||
|
"log-file",
|
||||||
|
settings
|
||||||
|
.log_file
|
||||||
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
|
);
|
||||||
|
fields.insert("log-path", settings.log_path.clone());
|
||||||
fields.insert(
|
fields.insert(
|
||||||
"gamescope",
|
"gamescope",
|
||||||
settings
|
settings
|
||||||
.gamescope
|
.gamescope
|
||||||
.map(|value| if value { "on" } else { "off" }.to_string()),
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
);
|
);
|
||||||
if settings.gamescope_width.is_some() || inherited.gamescope_width.is_some() {
|
|
||||||
fields.insert(
|
fields.insert(
|
||||||
"gamescope-width",
|
"gamescope-width",
|
||||||
settings.gamescope_width.map(|value| value.to_string()),
|
settings.gamescope_width.map(|value| value.as_display_str()),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if settings.gamescope_height.is_some() || inherited.gamescope_height.is_some() {
|
|
||||||
fields.insert(
|
fields.insert(
|
||||||
"gamescope-height",
|
"gamescope-height",
|
||||||
settings.gamescope_height.map(|value| value.to_string()),
|
settings
|
||||||
|
.gamescope_height
|
||||||
|
.map(|value| value.as_display_str()),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if settings.gamescope_fps.is_some() || inherited.gamescope_fps.is_some() {
|
|
||||||
fields.insert(
|
fields.insert(
|
||||||
"gamescope-fps",
|
"gamescope-fps",
|
||||||
settings.gamescope_fps.map(|value| value.to_string()),
|
settings.gamescope_fps.map(|value| value.to_string()),
|
||||||
);
|
);
|
||||||
}
|
fields.insert(
|
||||||
if settings.fps_cap.is_some() || inherited.fps_cap.is_some() {
|
"gamescope-nested-width",
|
||||||
|
settings
|
||||||
|
.gamescope_nested_width
|
||||||
|
.map(|value| value.to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-nested-height",
|
||||||
|
settings
|
||||||
|
.gamescope_nested_height
|
||||||
|
.map(|value| value.to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-unfocused-fps",
|
||||||
|
settings
|
||||||
|
.gamescope_unfocused_fps
|
||||||
|
.map(|value| value.to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-scaler",
|
||||||
|
settings
|
||||||
|
.gamescope_scaler
|
||||||
|
.map(|value| value.as_str().to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-filter",
|
||||||
|
settings
|
||||||
|
.gamescope_filter
|
||||||
|
.map(|value| value.as_str().to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-sharpness",
|
||||||
|
settings.gamescope_sharpness.map(|value| value.to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-mode",
|
||||||
|
settings
|
||||||
|
.gamescope_window_mode
|
||||||
|
.map(|value| value.as_str().to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-adaptive-sync",
|
||||||
|
settings
|
||||||
|
.gamescope_adaptive_sync
|
||||||
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-hdr",
|
||||||
|
settings
|
||||||
|
.gamescope_hdr
|
||||||
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-steam",
|
||||||
|
settings
|
||||||
|
.gamescope_steam
|
||||||
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-expose-wayland",
|
||||||
|
settings
|
||||||
|
.gamescope_expose_wayland
|
||||||
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
|
);
|
||||||
|
fields.insert(
|
||||||
|
"gamescope-mangoapp",
|
||||||
|
settings
|
||||||
|
.gamescope_mangoapp
|
||||||
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
|
);
|
||||||
fields.insert("fps-cap", settings.fps_cap.map(|value| value.to_string()));
|
fields.insert("fps-cap", settings.fps_cap.map(|value| value.to_string()));
|
||||||
}
|
fields.insert(
|
||||||
|
"mangohud-log",
|
||||||
|
settings
|
||||||
|
.mangohud_log
|
||||||
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
|
);
|
||||||
|
fields.insert("mangohud-log-path", settings.mangohud_log_path.clone());
|
||||||
fields.insert(
|
fields.insert(
|
||||||
"vkbasalt",
|
"vkbasalt",
|
||||||
settings
|
settings
|
||||||
.vkbasalt
|
.vkbasalt
|
||||||
.map(|value| if value { "on" } else { "off" }.to_string()),
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
);
|
);
|
||||||
if settings.esync.is_some() || inherited.esync.is_some() {
|
fields.insert("vkbasalt-config", settings.vkbasalt_config.clone());
|
||||||
|
fields.insert(
|
||||||
|
"vkbasalt-log-level",
|
||||||
|
settings
|
||||||
|
.vkbasalt_log_level
|
||||||
|
.map(|value| value.as_str().to_string()),
|
||||||
|
);
|
||||||
fields.insert(
|
fields.insert(
|
||||||
"esync",
|
"esync",
|
||||||
settings
|
settings
|
||||||
.esync
|
.esync
|
||||||
.map(|value| if value { "on" } else { "off" }.to_string()),
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if settings.fsync.is_some() || inherited.fsync.is_some() {
|
|
||||||
fields.insert(
|
fields.insert(
|
||||||
"fsync",
|
"fsync",
|
||||||
settings
|
settings
|
||||||
.fsync
|
.fsync
|
||||||
.map(|value| if value { "on" } else { "off" }.to_string()),
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
fields.insert(
|
fields.insert(
|
||||||
"large-address-aware",
|
"large-address-aware",
|
||||||
settings
|
settings
|
||||||
.large_address_aware
|
.large_address_aware
|
||||||
.map(|value| if value { "on" } else { "off" }.to_string()),
|
.map(|value| if value { "on" } else { "off" }.to_string()),
|
||||||
);
|
);
|
||||||
if settings.pre_launch.is_some() || inherited.pre_launch.is_some() {
|
|
||||||
fields.insert("pre-launch", settings.pre_launch.clone());
|
fields.insert("pre-launch", settings.pre_launch.clone());
|
||||||
}
|
|
||||||
if settings.post_launch.is_some() || inherited.post_launch.is_some() {
|
|
||||||
fields.insert("post-launch", settings.post_launch.clone());
|
fields.insert("post-launch", settings.post_launch.clone());
|
||||||
}
|
fields.insert(
|
||||||
|
"hook-errors",
|
||||||
|
settings.hook_errors.map(|value| value.as_str().to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
for (name, value) in fields {
|
for (name, value) in fields {
|
||||||
@@ -295,15 +402,32 @@ fn render_profile_settings_with_indent(
|
|||||||
_ => crate::color::accent(value),
|
_ => crate::color::accent(value),
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
None => output.push_str(&format!(
|
None if effective => {
|
||||||
|
let default_value = effective_value(name, effective_settings);
|
||||||
|
let display = if default_value == "none" {
|
||||||
|
crate::color::dim("(not set)")
|
||||||
|
} else {
|
||||||
|
crate::color::dim(&format!("(default: {})", default_value))
|
||||||
|
};
|
||||||
|
output.push_str(&format!(
|
||||||
"{}{} = {}\n",
|
"{}{} = {}\n",
|
||||||
indent,
|
indent,
|
||||||
crate::color::dim(name),
|
crate::color::dim(name),
|
||||||
crate::color::dim(&format!("(inherits: {})", inherited_value(name, inherited)))
|
display
|
||||||
)),
|
));
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if settings.env_vars.is_some() || !inherited.env_vars.is_empty() {
|
let show_env = if effective {
|
||||||
|
settings.env_vars.is_some() || !effective_settings.env_vars.is_empty()
|
||||||
|
} else {
|
||||||
|
settings
|
||||||
|
.env_vars
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|vars| !vars.is_empty())
|
||||||
|
};
|
||||||
|
if show_env {
|
||||||
output.push_str(&format!("{indent}{}\n", crate::color::dim("env-vars:")));
|
output.push_str(&format!("{indent}{}\n", crate::color::dim("env-vars:")));
|
||||||
match &settings.env_vars {
|
match &settings.env_vars {
|
||||||
Some(vars) if !vars.is_empty() => {
|
Some(vars) if !vars.is_empty() => {
|
||||||
@@ -316,179 +440,469 @@ fn render_profile_settings_with_indent(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ if inherited.env_vars.is_empty() => {
|
_ if effective_settings.env_vars.is_empty() => {
|
||||||
output.push_str(&format!("{} {}\n", indent, crate::color::dim("(none)")));
|
output.push_str(&format!("{} {}\n", indent, crate::color::dim("(none)")));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
for (key, value) in &inherited.env_vars {
|
for (key, value) in &effective_settings.env_vars {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} {} = {} {}\n",
|
"{} {} = {} {}\n",
|
||||||
indent,
|
indent,
|
||||||
crate::color::accent(key),
|
crate::color::accent(key),
|
||||||
value,
|
value,
|
||||||
crate::color::dim("(inherited)")
|
crate::color::dim("(default)")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !effective && output.is_empty() {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} {}\n",
|
||||||
|
indent,
|
||||||
|
crate::color::dim("(no settings configured for this profile)")
|
||||||
|
));
|
||||||
|
}
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_resolved_settings(settings: &ResolvedSettings) -> String {
|
fn render_resolved_settings(settings: &ResolvedSettings, sparse: Option<&Settings>) -> String {
|
||||||
|
macro_rules! show_if {
|
||||||
|
($field:ident) => {
|
||||||
|
sparse.map_or(true, |s| s.$field.is_some())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
|
if show_if!(mangohud) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("overlay"),
|
crate::color::dim("mangohud"),
|
||||||
crate::color::on_off(settings.overlay)
|
crate::color::on_off(settings.mangohud)
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamemode) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("performance"),
|
crate::color::dim("gamemode"),
|
||||||
crate::color::on_off(settings.performance)
|
crate::color::on_off(settings.gamemode)
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
if show_if!(steam_host_libs) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("steam-host-libs"),
|
crate::color::dim("steam-host-libs"),
|
||||||
crate::color::on_off(settings.steam_host_libs)
|
crate::color::on_off(settings.steam_host_libs)
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
if show_if!(game_libs) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("game-libs"),
|
crate::color::dim("game-libs"),
|
||||||
crate::color::accent(settings.game_libs.as_str())
|
crate::color::accent(settings.game_libs.as_str())
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
if show_if!(verbose) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("verbose"),
|
crate::color::dim("verbose"),
|
||||||
crate::color::on_off(settings.verbose)
|
crate::color::on_off(settings.verbose)
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
if show_if!(log_file) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("log-file"),
|
||||||
|
if settings.log_file {
|
||||||
|
crate::color::ok("on")
|
||||||
|
} else {
|
||||||
|
crate::color::dim("off (default)")
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(log_path) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("log-path"),
|
||||||
|
match &settings.log_path {
|
||||||
|
Some(v) => crate::color::accent(v),
|
||||||
|
None => crate::color::dim("(not set — ~/.local/state/gamewrap/gamewrap.log)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("gamescope"),
|
crate::color::dim("gamescope"),
|
||||||
crate::color::on_off(settings.gamescope)
|
crate::color::on_off(settings.gamescope)
|
||||||
));
|
));
|
||||||
if let Some(width) = settings.gamescope_width {
|
}
|
||||||
|
if show_if!(gamescope_width) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("gamescope-width"),
|
crate::color::dim("gamescope-width"),
|
||||||
crate::color::accent(&width.to_string())
|
match settings.gamescope_width {
|
||||||
|
Some(crate::config::GamescopeSize::Pixels(v)) => {
|
||||||
|
crate::color::accent(&v.to_string())
|
||||||
|
}
|
||||||
|
Some(crate::config::GamescopeSize::Native) => crate::color::accent("native"),
|
||||||
|
None => crate::color::dim("(not set — gamescope default: 1280)"),
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(height) = settings.gamescope_height {
|
if show_if!(gamescope_height) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("gamescope-height"),
|
crate::color::dim("gamescope-height"),
|
||||||
crate::color::accent(&height.to_string())
|
match settings.gamescope_height {
|
||||||
|
Some(crate::config::GamescopeSize::Pixels(v)) => {
|
||||||
|
crate::color::accent(&v.to_string())
|
||||||
|
}
|
||||||
|
Some(crate::config::GamescopeSize::Native) => crate::color::accent("native"),
|
||||||
|
None => crate::color::dim("(not set — gamescope default: 720)"),
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(fps) = settings.gamescope_fps {
|
if show_if!(gamescope_fps) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("gamescope-fps"),
|
crate::color::dim("gamescope-fps"),
|
||||||
crate::color::accent(&fps.to_string())
|
match settings.gamescope_fps {
|
||||||
|
Some(v) => crate::color::accent(&v.to_string()),
|
||||||
|
None => crate::color::dim("(not set — unlimited)"),
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(fps_cap) = settings.fps_cap {
|
if show_if!(gamescope_nested_width) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-nested-width"),
|
||||||
|
match settings.gamescope_nested_width {
|
||||||
|
Some(v) => crate::color::accent(&v.to_string()),
|
||||||
|
None => crate::color::dim("(not set — matches output width)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_nested_height) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-nested-height"),
|
||||||
|
match settings.gamescope_nested_height {
|
||||||
|
Some(v) => crate::color::accent(&v.to_string()),
|
||||||
|
None => crate::color::dim("(not set — matches output height)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_unfocused_fps) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-unfocused-fps"),
|
||||||
|
match settings.gamescope_unfocused_fps {
|
||||||
|
Some(v) => crate::color::accent(&v.to_string()),
|
||||||
|
None => crate::color::dim("(not set — no limit)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_scaler) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-scaler"),
|
||||||
|
match settings.gamescope_scaler {
|
||||||
|
Some(v) => crate::color::accent(v.as_str()),
|
||||||
|
None => crate::color::dim("(not set — auto)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_filter) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-filter"),
|
||||||
|
match settings.gamescope_filter {
|
||||||
|
Some(v) => crate::color::accent(v.as_str()),
|
||||||
|
None => crate::color::dim("(not set — linear)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_sharpness) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-sharpness"),
|
||||||
|
match settings.gamescope_sharpness {
|
||||||
|
Some(v) => crate::color::accent(&v.to_string()),
|
||||||
|
None => crate::color::dim("(not set — no sharpening)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_window_mode) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-mode"),
|
||||||
|
match settings.gamescope_window_mode {
|
||||||
|
Some(v) => crate::color::accent(v.as_str()),
|
||||||
|
None => crate::color::dim("windowed (default)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_adaptive_sync) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-adaptive-sync"),
|
||||||
|
crate::color::on_off(settings.gamescope_adaptive_sync)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_hdr) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-hdr"),
|
||||||
|
crate::color::on_off(settings.gamescope_hdr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_steam) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-steam"),
|
||||||
|
crate::color::on_off(settings.gamescope_steam)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_expose_wayland) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-expose-wayland"),
|
||||||
|
crate::color::on_off(settings.gamescope_expose_wayland)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(gamescope_mangoapp) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("gamescope-mangoapp"),
|
||||||
|
crate::color::on_off(settings.gamescope_mangoapp)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(fps_cap) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("fps-cap"),
|
crate::color::dim("fps-cap"),
|
||||||
crate::color::accent(&fps_cap.to_string())
|
match settings.fps_cap {
|
||||||
|
Some(v) => crate::color::accent(&v.to_string()),
|
||||||
|
None => crate::color::dim("(not set — no cap)"),
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if show_if!(mangohud_log) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("mangohud-log"),
|
||||||
|
crate::color::on_off(settings.mangohud_log)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(mangohud_log_path) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("mangohud-log-path"),
|
||||||
|
match &settings.mangohud_log_path {
|
||||||
|
Some(v) => crate::color::accent(v),
|
||||||
|
None => crate::color::dim("(not set — MangoHud default location)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(vkbasalt) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("vkbasalt"),
|
crate::color::dim("vkbasalt"),
|
||||||
crate::color::on_off(settings.vkbasalt)
|
crate::color::on_off(settings.vkbasalt)
|
||||||
));
|
));
|
||||||
if let Some(esync) = settings.esync {
|
}
|
||||||
|
if show_if!(vkbasalt_config) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("vkbasalt-config"),
|
||||||
|
match &settings.vkbasalt_config {
|
||||||
|
Some(v) => crate::color::accent(v),
|
||||||
|
None => crate::color::dim("(not set — ~/.config/vkBasalt/vkBasalt.conf)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(vkbasalt_log_level) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("vkbasalt-log-level"),
|
||||||
|
match settings.vkbasalt_log_level {
|
||||||
|
Some(v) => crate::color::accent(v.as_str()),
|
||||||
|
None => crate::color::dim("(not set — vkBasalt default)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if show_if!(esync) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("esync"),
|
crate::color::dim("esync"),
|
||||||
crate::color::on_off(esync)
|
match settings.esync {
|
||||||
|
Some(value) => crate::color::on_off(value),
|
||||||
|
None => crate::color::dim("(not set — Proton default)"),
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(fsync) = settings.fsync {
|
if show_if!(fsync) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("fsync"),
|
crate::color::dim("fsync"),
|
||||||
crate::color::on_off(fsync)
|
match settings.fsync {
|
||||||
|
Some(value) => crate::color::on_off(value),
|
||||||
|
None => crate::color::dim("(not set — Proton default)"),
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if show_if!(large_address_aware) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("large-address-aware"),
|
crate::color::dim("large-address-aware"),
|
||||||
crate::color::on_off(settings.large_address_aware)
|
crate::color::on_off(settings.large_address_aware)
|
||||||
));
|
));
|
||||||
if let Some(pre_launch) = &settings.pre_launch {
|
}
|
||||||
|
if show_if!(pre_launch) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("pre-launch"),
|
crate::color::dim("pre-launch"),
|
||||||
crate::color::accent(pre_launch)
|
match &settings.pre_launch {
|
||||||
|
Some(v) => crate::color::accent(v),
|
||||||
|
None => crate::color::dim("(not set — none)"),
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(post_launch) = &settings.post_launch {
|
if show_if!(post_launch) {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{} = {}\n",
|
"{} = {}\n",
|
||||||
crate::color::dim("post-launch"),
|
crate::color::dim("post-launch"),
|
||||||
crate::color::accent(post_launch)
|
match &settings.post_launch {
|
||||||
|
Some(v) => crate::color::accent(v),
|
||||||
|
None => crate::color::dim("(not set — none)"),
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !settings.env_vars.is_empty() {
|
if show_if!(hook_errors) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} = {}\n",
|
||||||
|
crate::color::dim("hook-errors"),
|
||||||
|
match settings.hook_errors {
|
||||||
|
HookErrors::Fail => crate::color::warn("fail"),
|
||||||
|
HookErrors::Warn => crate::color::dim("warn (default)"),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if sparse.is_none_or(|s| s.env_vars.is_some()) && !settings.env_vars.is_empty() {
|
||||||
output.push_str(&format!("{}\n", crate::color::dim("env-vars:")));
|
output.push_str(&format!("{}\n", crate::color::dim("env-vars:")));
|
||||||
for (key, value) in &settings.env_vars {
|
for (key, value) in &settings.env_vars {
|
||||||
output.push_str(&format!(" {} = {}\n", crate::color::accent(key), value));
|
output.push_str(&format!(" {} = {}\n", crate::color::accent(key), value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if sparse.is_some() && output.is_empty() {
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {}\n",
|
||||||
|
crate::color::dim(
|
||||||
|
"(no global settings configured — run 'gamewrap config set' to customize)"
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inherited_value(name: &str, inherited: &ResolvedSettings) -> String {
|
fn effective_value(name: &str, settings: &ResolvedSettings) -> String {
|
||||||
match name {
|
match name {
|
||||||
"overlay" => crate::color::on_off(inherited.overlay),
|
"mangohud" => crate::color::on_off(settings.mangohud),
|
||||||
"performance" => crate::color::on_off(inherited.performance),
|
"gamemode" => crate::color::on_off(settings.gamemode),
|
||||||
"steam-host-libs" => crate::color::on_off(inherited.steam_host_libs),
|
"steam-host-libs" => crate::color::on_off(settings.steam_host_libs),
|
||||||
"game-libs" => inherited.game_libs.as_str().to_string(),
|
"game-libs" => settings.game_libs.as_str().to_string(),
|
||||||
"verbose" => crate::color::on_off(inherited.verbose),
|
"verbose" => crate::color::on_off(settings.verbose),
|
||||||
"gamescope" => crate::color::on_off(inherited.gamescope),
|
"log-file" => crate::color::on_off(settings.log_file),
|
||||||
"gamescope-width" => inherited
|
"log-path" => settings.log_path.as_deref().unwrap_or("none").to_string(),
|
||||||
|
"gamescope" => crate::color::on_off(settings.gamescope),
|
||||||
|
"gamescope-width" => settings
|
||||||
.gamescope_width
|
.gamescope_width
|
||||||
.map(|value| value.to_string())
|
.map(|value| value.as_display_str())
|
||||||
.unwrap_or_else(|| "none".to_string()),
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
"gamescope-height" => inherited
|
"gamescope-height" => settings
|
||||||
.gamescope_height
|
.gamescope_height
|
||||||
.map(|value| value.to_string())
|
.map(|value| value.as_display_str())
|
||||||
.unwrap_or_else(|| "none".to_string()),
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
"gamescope-fps" => inherited
|
"gamescope-fps" => settings
|
||||||
.gamescope_fps
|
.gamescope_fps
|
||||||
.map(|value| value.to_string())
|
.map(|value| value.to_string())
|
||||||
.unwrap_or_else(|| "none".to_string()),
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
"fps-cap" => inherited
|
"gamescope-nested-width" => settings
|
||||||
|
.gamescope_nested_width
|
||||||
|
.map(|value| value.to_string())
|
||||||
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"gamescope-nested-height" => settings
|
||||||
|
.gamescope_nested_height
|
||||||
|
.map(|value| value.to_string())
|
||||||
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"gamescope-unfocused-fps" => settings
|
||||||
|
.gamescope_unfocused_fps
|
||||||
|
.map(|value| value.to_string())
|
||||||
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"gamescope-scaler" => settings
|
||||||
|
.gamescope_scaler
|
||||||
|
.map(|value| value.as_str().to_string())
|
||||||
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"gamescope-filter" => settings
|
||||||
|
.gamescope_filter
|
||||||
|
.map(|value| value.as_str().to_string())
|
||||||
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"gamescope-sharpness" => settings
|
||||||
|
.gamescope_sharpness
|
||||||
|
.map(|value| value.to_string())
|
||||||
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"gamescope-mode" => settings
|
||||||
|
.gamescope_window_mode
|
||||||
|
.map(|value| value.as_str().to_string())
|
||||||
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"gamescope-adaptive-sync" => crate::color::on_off(settings.gamescope_adaptive_sync),
|
||||||
|
"gamescope-hdr" => crate::color::on_off(settings.gamescope_hdr),
|
||||||
|
"gamescope-steam" => crate::color::on_off(settings.gamescope_steam),
|
||||||
|
"gamescope-expose-wayland" => crate::color::on_off(settings.gamescope_expose_wayland),
|
||||||
|
"gamescope-mangoapp" => crate::color::on_off(settings.gamescope_mangoapp),
|
||||||
|
"fps-cap" => settings
|
||||||
.fps_cap
|
.fps_cap
|
||||||
.map(|value| value.to_string())
|
.map(|value| value.to_string())
|
||||||
.unwrap_or_else(|| "none".to_string()),
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
"vkbasalt" => crate::color::on_off(inherited.vkbasalt),
|
"mangohud-log" => crate::color::on_off(settings.mangohud_log),
|
||||||
"esync" => crate::color::option_on_off(inherited.esync),
|
"mangohud-log-path" => settings
|
||||||
"fsync" => crate::color::option_on_off(inherited.fsync),
|
.mangohud_log_path
|
||||||
"large-address-aware" => crate::color::on_off(inherited.large_address_aware),
|
.as_deref()
|
||||||
"pre-launch" => inherited
|
.unwrap_or("none")
|
||||||
|
.to_string(),
|
||||||
|
"vkbasalt" => crate::color::on_off(settings.vkbasalt),
|
||||||
|
"vkbasalt-config" => settings
|
||||||
|
.vkbasalt_config
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("none")
|
||||||
|
.to_string(),
|
||||||
|
"vkbasalt-log-level" => settings
|
||||||
|
.vkbasalt_log_level
|
||||||
|
.map(|value| value.as_str().to_string())
|
||||||
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"esync" => settings
|
||||||
|
.esync
|
||||||
|
.map(crate::color::on_off)
|
||||||
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"fsync" => settings
|
||||||
|
.fsync
|
||||||
|
.map(crate::color::on_off)
|
||||||
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"large-address-aware" => crate::color::on_off(settings.large_address_aware),
|
||||||
|
"pre-launch" => settings
|
||||||
.pre_launch
|
.pre_launch
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "none".to_string()),
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
"post-launch" => inherited
|
"post-launch" => settings
|
||||||
.post_launch
|
.post_launch
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "none".to_string()),
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
|
"hook-errors" => settings.hook_errors.as_str().to_string(),
|
||||||
_ => "unknown".to_string(),
|
_ => "unknown".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inherits_suffix(parent: &Option<String>) -> String {
|
|
||||||
parent
|
|
||||||
.as_ref()
|
|
||||||
.map(|parent| format!(" (inherits: {parent})"))
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn test_paths(root: &Path) -> AppPaths {
|
pub fn test_paths(root: &Path) -> AppPaths {
|
||||||
AppPaths {
|
AppPaths {
|
||||||
@@ -510,7 +924,7 @@ mod tests {
|
|||||||
let temp = tempdir().expect("temp dir");
|
let temp = tempdir().expect("temp dir");
|
||||||
let paths = test_paths(temp.path());
|
let paths = test_paths(temp.path());
|
||||||
let mut config = ConfigFile::default();
|
let mut config = ConfigFile::default();
|
||||||
config.defaults.overlay = Some(true);
|
config.defaults.mangohud = Some(true);
|
||||||
config
|
config
|
||||||
.profiles
|
.profiles
|
||||||
.insert("benchmark".to_string(), ProfileConfig::default());
|
.insert("benchmark".to_string(), ProfileConfig::default());
|
||||||
@@ -518,7 +932,7 @@ mod tests {
|
|||||||
save(&paths, &config).expect("save");
|
save(&paths, &config).expect("save");
|
||||||
let loaded = load(&paths).expect("load");
|
let loaded = load(&paths).expect("load");
|
||||||
|
|
||||||
assert_eq!(loaded.defaults.overlay, Some(true));
|
assert_eq!(loaded.defaults.mangohud, Some(true));
|
||||||
assert!(loaded.profiles.contains_key("benchmark"));
|
assert!(loaded.profiles.contains_key("benchmark"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+354
-65
@@ -2,6 +2,45 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
macro_rules! impl_as_str {
|
||||||
|
($type:ty { $($variant:ident => $s:literal),+ $(,)? }) => {
|
||||||
|
impl $type {
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
$(Self::$variant => $s),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// src-field is Option<T (Copy)>, dst-field is T
|
||||||
|
macro_rules! apply_scalar {
|
||||||
|
($dst:expr, $src:expr, $field:ident) => {
|
||||||
|
if let Some(v) = $src.$field {
|
||||||
|
$dst.$field = v;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// src-field is Option<T (Copy)>, dst-field is Option<T>
|
||||||
|
macro_rules! apply_opt {
|
||||||
|
($dst:expr, $src:expr, $field:ident) => {
|
||||||
|
if let Some(v) = $src.$field {
|
||||||
|
$dst.$field = Some(v);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// src-field is Option<String>, dst-field is Option<String>
|
||||||
|
macro_rules! apply_clone {
|
||||||
|
($dst:expr, $src:expr, $field:ident) => {
|
||||||
|
if let Some(ref v) = $src.$field {
|
||||||
|
$dst.$field = Some(v.clone());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum GameLibsMode {
|
pub enum GameLibsMode {
|
||||||
@@ -11,22 +50,163 @@ pub enum GameLibsMode {
|
|||||||
Gamemode,
|
Gamemode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameLibsMode {
|
impl_as_str!(GameLibsMode {
|
||||||
pub fn as_str(self) -> &'static str {
|
Auto => "auto",
|
||||||
|
Keep => "keep",
|
||||||
|
Gamemode => "gamemode",
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum GamescopeScaler {
|
||||||
|
Auto,
|
||||||
|
Integer,
|
||||||
|
Fit,
|
||||||
|
Fill,
|
||||||
|
Stretch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_as_str!(GamescopeScaler {
|
||||||
|
Auto => "auto",
|
||||||
|
Integer => "integer",
|
||||||
|
Fit => "fit",
|
||||||
|
Fill => "fill",
|
||||||
|
Stretch => "stretch",
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum GamescopeFilter {
|
||||||
|
Linear,
|
||||||
|
Nearest,
|
||||||
|
Fsr,
|
||||||
|
Nis,
|
||||||
|
Pixel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_as_str!(GamescopeFilter {
|
||||||
|
Linear => "linear",
|
||||||
|
Nearest => "nearest",
|
||||||
|
Fsr => "fsr",
|
||||||
|
Nis => "nis",
|
||||||
|
Pixel => "pixel",
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Output resolution value for gamescope -W/-H.
|
||||||
|
/// Native = detect display resolution at launch; Pixels = explicit pixel count.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum GamescopeSize {
|
||||||
|
Native,
|
||||||
|
Pixels(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GamescopeSize {
|
||||||
|
pub fn as_display_str(self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::Auto => "auto",
|
Self::Native => "native".to_string(),
|
||||||
Self::Keep => "keep",
|
Self::Pixels(n) => n.to_string(),
|
||||||
Self::Gamemode => "gamemode",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl serde::Serialize for GamescopeSize {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||||
|
match self {
|
||||||
|
Self::Native => s.serialize_str("native"),
|
||||||
|
Self::Pixels(n) => s.serialize_u32(*n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for GamescopeSize {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||||
|
struct Visitor;
|
||||||
|
|
||||||
|
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||||
|
type Value = GamescopeSize;
|
||||||
|
|
||||||
|
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "a pixel count integer or the string \"native\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
|
||||||
|
Ok(GamescopeSize::Pixels(v as u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
|
||||||
|
if v < 0 {
|
||||||
|
Err(E::custom("pixel count cannot be negative"))
|
||||||
|
} else {
|
||||||
|
Ok(GamescopeSize::Pixels(v as u32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||||
|
if v == "native" {
|
||||||
|
Ok(GamescopeSize::Native)
|
||||||
|
} else {
|
||||||
|
v.parse::<u32>().map(GamescopeSize::Pixels).map_err(|_| {
|
||||||
|
E::custom(format!("expected a pixel count or \"native\", got {v:?}"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.deserialize_any(Visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum GamescopeWindowMode {
|
||||||
|
Windowed,
|
||||||
|
Borderless,
|
||||||
|
Fullscreen,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_as_str!(GamescopeWindowMode {
|
||||||
|
Windowed => "windowed",
|
||||||
|
Borderless => "borderless",
|
||||||
|
Fullscreen => "fullscreen",
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum VkbasaltLogLevel {
|
||||||
|
Debug,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_as_str!(VkbasaltLogLevel {
|
||||||
|
Debug => "debug",
|
||||||
|
Info => "info",
|
||||||
|
Warning => "warning",
|
||||||
|
Error => "error",
|
||||||
|
None => "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum HookErrors {
|
||||||
|
#[default]
|
||||||
|
Warn,
|
||||||
|
Fail,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_as_str!(HookErrors {
|
||||||
|
Warn => "warn",
|
||||||
|
Fail => "fail",
|
||||||
|
});
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub overlay: Option<bool>,
|
pub mangohud: Option<bool>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub performance: Option<bool>,
|
pub gamemode: Option<bool>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub steam_host_libs: Option<bool>,
|
pub steam_host_libs: Option<bool>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -34,18 +214,54 @@ pub struct Settings {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub verbose: Option<bool>,
|
pub verbose: Option<bool>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub log_file: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub log_path: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub gamescope: Option<bool>,
|
pub gamescope: Option<bool>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub gamescope_width: Option<u32>,
|
pub gamescope_width: Option<GamescopeSize>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub gamescope_height: Option<u32>,
|
pub gamescope_height: Option<GamescopeSize>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub gamescope_fps: Option<u32>,
|
pub gamescope_fps: Option<u32>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_nested_width: Option<u32>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_nested_height: Option<u32>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_unfocused_fps: Option<u32>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_scaler: Option<GamescopeScaler>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_filter: Option<GamescopeFilter>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_sharpness: Option<u8>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_window_mode: Option<GamescopeWindowMode>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_adaptive_sync: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_hdr: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_steam: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_expose_wayland: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub gamescope_mangoapp: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub fps_cap: Option<u32>,
|
pub fps_cap: Option<u32>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mangohud_log: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mangohud_log_path: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub vkbasalt: Option<bool>,
|
pub vkbasalt: Option<bool>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub vkbasalt_config: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub vkbasalt_log_level: Option<VkbasaltLogLevel>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub esync: Option<bool>,
|
pub esync: Option<bool>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub fsync: Option<bool>,
|
pub fsync: Option<bool>,
|
||||||
@@ -55,50 +271,138 @@ pub struct Settings {
|
|||||||
pub pre_launch: Option<String>,
|
pub pre_launch: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub post_launch: Option<String>,
|
pub post_launch: Option<String>,
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
rename = "hook-errors",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub hook_errors: Option<HookErrors>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub env_vars: Option<BTreeMap<String, String>>,
|
pub env_vars: Option<BTreeMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct ResolvedSettings {
|
pub struct ResolvedSettings {
|
||||||
pub overlay: bool,
|
pub mangohud: bool,
|
||||||
pub performance: bool,
|
pub gamemode: bool,
|
||||||
pub steam_host_libs: bool,
|
pub steam_host_libs: bool,
|
||||||
pub game_libs: GameLibsMode,
|
pub game_libs: GameLibsMode,
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
pub log_file: bool,
|
||||||
|
pub log_path: Option<String>,
|
||||||
pub gamescope: bool,
|
pub gamescope: bool,
|
||||||
pub gamescope_width: Option<u32>,
|
pub gamescope_width: Option<GamescopeSize>,
|
||||||
pub gamescope_height: Option<u32>,
|
pub gamescope_height: Option<GamescopeSize>,
|
||||||
pub gamescope_fps: Option<u32>,
|
pub gamescope_fps: Option<u32>,
|
||||||
|
pub gamescope_nested_width: Option<u32>,
|
||||||
|
pub gamescope_nested_height: Option<u32>,
|
||||||
|
pub gamescope_unfocused_fps: Option<u32>,
|
||||||
|
pub gamescope_scaler: Option<GamescopeScaler>,
|
||||||
|
pub gamescope_filter: Option<GamescopeFilter>,
|
||||||
|
pub gamescope_sharpness: Option<u8>,
|
||||||
|
pub gamescope_window_mode: Option<GamescopeWindowMode>,
|
||||||
|
pub gamescope_adaptive_sync: bool,
|
||||||
|
pub gamescope_hdr: bool,
|
||||||
|
pub gamescope_steam: bool,
|
||||||
|
pub gamescope_expose_wayland: bool,
|
||||||
|
pub gamescope_mangoapp: bool,
|
||||||
pub fps_cap: Option<u32>,
|
pub fps_cap: Option<u32>,
|
||||||
|
pub mangohud_log: bool,
|
||||||
|
pub mangohud_log_path: Option<String>,
|
||||||
pub vkbasalt: bool,
|
pub vkbasalt: bool,
|
||||||
|
pub vkbasalt_config: Option<String>,
|
||||||
|
pub vkbasalt_log_level: Option<VkbasaltLogLevel>,
|
||||||
pub esync: Option<bool>,
|
pub esync: Option<bool>,
|
||||||
pub fsync: Option<bool>,
|
pub fsync: Option<bool>,
|
||||||
pub large_address_aware: bool,
|
pub large_address_aware: bool,
|
||||||
pub pre_launch: Option<String>,
|
pub pre_launch: Option<String>,
|
||||||
pub post_launch: Option<String>,
|
pub post_launch: Option<String>,
|
||||||
|
pub hook_errors: HookErrors,
|
||||||
pub env_vars: BTreeMap<String, String>,
|
pub env_vars: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ResolvedSettings> for Settings {
|
||||||
|
fn from(r: ResolvedSettings) -> Self {
|
||||||
|
Settings {
|
||||||
|
mangohud: Some(r.mangohud),
|
||||||
|
gamemode: Some(r.gamemode),
|
||||||
|
steam_host_libs: Some(r.steam_host_libs),
|
||||||
|
game_libs: Some(r.game_libs),
|
||||||
|
verbose: Some(r.verbose),
|
||||||
|
log_file: Some(r.log_file),
|
||||||
|
log_path: r.log_path,
|
||||||
|
gamescope: Some(r.gamescope),
|
||||||
|
gamescope_width: r.gamescope_width,
|
||||||
|
gamescope_height: r.gamescope_height,
|
||||||
|
gamescope_fps: r.gamescope_fps,
|
||||||
|
gamescope_nested_width: r.gamescope_nested_width,
|
||||||
|
gamescope_nested_height: r.gamescope_nested_height,
|
||||||
|
gamescope_unfocused_fps: r.gamescope_unfocused_fps,
|
||||||
|
gamescope_scaler: r.gamescope_scaler,
|
||||||
|
gamescope_filter: r.gamescope_filter,
|
||||||
|
gamescope_sharpness: r.gamescope_sharpness,
|
||||||
|
gamescope_window_mode: r.gamescope_window_mode,
|
||||||
|
gamescope_adaptive_sync: Some(r.gamescope_adaptive_sync),
|
||||||
|
gamescope_hdr: Some(r.gamescope_hdr),
|
||||||
|
gamescope_steam: Some(r.gamescope_steam),
|
||||||
|
gamescope_expose_wayland: Some(r.gamescope_expose_wayland),
|
||||||
|
gamescope_mangoapp: Some(r.gamescope_mangoapp),
|
||||||
|
fps_cap: r.fps_cap,
|
||||||
|
mangohud_log: Some(r.mangohud_log),
|
||||||
|
mangohud_log_path: r.mangohud_log_path,
|
||||||
|
vkbasalt: Some(r.vkbasalt),
|
||||||
|
vkbasalt_config: r.vkbasalt_config,
|
||||||
|
vkbasalt_log_level: r.vkbasalt_log_level,
|
||||||
|
esync: r.esync,
|
||||||
|
fsync: r.fsync,
|
||||||
|
large_address_aware: Some(r.large_address_aware),
|
||||||
|
pre_launch: r.pre_launch,
|
||||||
|
post_launch: r.post_launch,
|
||||||
|
hook_errors: Some(r.hook_errors),
|
||||||
|
env_vars: (!r.env_vars.is_empty()).then_some(r.env_vars),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for ResolvedSettings {
|
impl Default for ResolvedSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
overlay: true,
|
mangohud: true,
|
||||||
performance: true,
|
gamemode: true,
|
||||||
steam_host_libs: true,
|
steam_host_libs: true,
|
||||||
game_libs: GameLibsMode::Auto,
|
game_libs: GameLibsMode::Auto,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
|
log_file: false,
|
||||||
|
log_path: None,
|
||||||
gamescope: false,
|
gamescope: false,
|
||||||
gamescope_width: None,
|
gamescope_width: None,
|
||||||
gamescope_height: None,
|
gamescope_height: None,
|
||||||
gamescope_fps: None,
|
gamescope_fps: None,
|
||||||
|
gamescope_nested_width: None,
|
||||||
|
gamescope_nested_height: None,
|
||||||
|
gamescope_unfocused_fps: None,
|
||||||
|
gamescope_scaler: None,
|
||||||
|
gamescope_filter: None,
|
||||||
|
gamescope_sharpness: None,
|
||||||
|
gamescope_window_mode: None,
|
||||||
|
gamescope_adaptive_sync: false,
|
||||||
|
gamescope_hdr: false,
|
||||||
|
gamescope_steam: false,
|
||||||
|
gamescope_expose_wayland: false,
|
||||||
|
gamescope_mangoapp: false,
|
||||||
fps_cap: None,
|
fps_cap: None,
|
||||||
|
mangohud_log: false,
|
||||||
|
mangohud_log_path: None,
|
||||||
vkbasalt: false,
|
vkbasalt: false,
|
||||||
|
vkbasalt_config: None,
|
||||||
|
vkbasalt_log_level: None,
|
||||||
esync: None,
|
esync: None,
|
||||||
fsync: None,
|
fsync: None,
|
||||||
large_address_aware: false,
|
large_address_aware: false,
|
||||||
pre_launch: None,
|
pre_launch: None,
|
||||||
post_launch: None,
|
post_launch: None,
|
||||||
|
hook_errors: HookErrors::Warn,
|
||||||
env_vars: BTreeMap::new(),
|
env_vars: BTreeMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,54 +410,41 @@ impl Default for ResolvedSettings {
|
|||||||
|
|
||||||
impl ResolvedSettings {
|
impl ResolvedSettings {
|
||||||
pub fn apply(&mut self, settings: &Settings) {
|
pub fn apply(&mut self, settings: &Settings) {
|
||||||
if let Some(value) = settings.overlay {
|
apply_scalar!(self, settings, mangohud);
|
||||||
self.overlay = value;
|
apply_scalar!(self, settings, gamemode);
|
||||||
}
|
apply_scalar!(self, settings, steam_host_libs);
|
||||||
if let Some(value) = settings.performance {
|
apply_scalar!(self, settings, game_libs);
|
||||||
self.performance = value;
|
apply_scalar!(self, settings, verbose);
|
||||||
}
|
apply_scalar!(self, settings, log_file);
|
||||||
if let Some(value) = settings.steam_host_libs {
|
apply_clone!(self, settings, log_path);
|
||||||
self.steam_host_libs = value;
|
apply_scalar!(self, settings, gamescope);
|
||||||
}
|
apply_opt!(self, settings, gamescope_width);
|
||||||
if let Some(value) = settings.game_libs {
|
apply_opt!(self, settings, gamescope_height);
|
||||||
self.game_libs = value;
|
apply_opt!(self, settings, gamescope_fps);
|
||||||
}
|
apply_opt!(self, settings, gamescope_nested_width);
|
||||||
if let Some(value) = settings.verbose {
|
apply_opt!(self, settings, gamescope_nested_height);
|
||||||
self.verbose = value;
|
apply_opt!(self, settings, gamescope_unfocused_fps);
|
||||||
}
|
apply_opt!(self, settings, gamescope_scaler);
|
||||||
if let Some(value) = settings.gamescope {
|
apply_opt!(self, settings, gamescope_filter);
|
||||||
self.gamescope = value;
|
apply_opt!(self, settings, gamescope_sharpness);
|
||||||
}
|
apply_opt!(self, settings, gamescope_window_mode);
|
||||||
if let Some(value) = settings.gamescope_width {
|
apply_scalar!(self, settings, gamescope_adaptive_sync);
|
||||||
self.gamescope_width = Some(value);
|
apply_scalar!(self, settings, gamescope_hdr);
|
||||||
}
|
apply_scalar!(self, settings, gamescope_steam);
|
||||||
if let Some(value) = settings.gamescope_height {
|
apply_scalar!(self, settings, gamescope_expose_wayland);
|
||||||
self.gamescope_height = Some(value);
|
apply_scalar!(self, settings, gamescope_mangoapp);
|
||||||
}
|
apply_opt!(self, settings, fps_cap);
|
||||||
if let Some(value) = settings.gamescope_fps {
|
apply_scalar!(self, settings, mangohud_log);
|
||||||
self.gamescope_fps = Some(value);
|
apply_clone!(self, settings, mangohud_log_path);
|
||||||
}
|
apply_scalar!(self, settings, vkbasalt);
|
||||||
if let Some(value) = settings.fps_cap {
|
apply_clone!(self, settings, vkbasalt_config);
|
||||||
self.fps_cap = Some(value);
|
apply_opt!(self, settings, vkbasalt_log_level);
|
||||||
}
|
apply_opt!(self, settings, esync);
|
||||||
if let Some(value) = settings.vkbasalt {
|
apply_opt!(self, settings, fsync);
|
||||||
self.vkbasalt = value;
|
apply_scalar!(self, settings, large_address_aware);
|
||||||
}
|
apply_clone!(self, settings, pre_launch);
|
||||||
if let Some(value) = settings.esync {
|
apply_clone!(self, settings, post_launch);
|
||||||
self.esync = Some(value);
|
apply_scalar!(self, settings, hook_errors);
|
||||||
}
|
|
||||||
if let Some(value) = settings.fsync {
|
|
||||||
self.fsync = Some(value);
|
|
||||||
}
|
|
||||||
if let Some(value) = settings.large_address_aware {
|
|
||||||
self.large_address_aware = value;
|
|
||||||
}
|
|
||||||
if let Some(ref value) = settings.pre_launch {
|
|
||||||
self.pre_launch = Some(value.clone());
|
|
||||||
}
|
|
||||||
if let Some(ref value) = settings.post_launch {
|
|
||||||
self.post_launch = Some(value.clone());
|
|
||||||
}
|
|
||||||
if let Some(ref vars) = settings.env_vars {
|
if let Some(ref vars) = settings.env_vars {
|
||||||
for (key, value) in vars {
|
for (key, value) in vars {
|
||||||
self.env_vars.insert(key.clone(), value.clone());
|
self.env_vars.insert(key.clone(), value.clone());
|
||||||
@@ -170,8 +461,6 @@ pub struct Binding {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
pub struct ProfileConfig {
|
pub struct ProfileConfig {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub inherits: Option<String>,
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub settings: Settings,
|
pub settings: Settings,
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
/// Convert days since Unix epoch (1970-01-01) to (year, month, day).
|
||||||
|
pub fn epoch_days_to_ymd(days: i64) -> (i32, u32, u32) {
|
||||||
|
let days = days + 719_468;
|
||||||
|
let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
|
||||||
|
let day_of_era = days - era * 146_097;
|
||||||
|
let year_of_era =
|
||||||
|
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
|
||||||
|
let mut year = year_of_era + era * 400;
|
||||||
|
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
|
||||||
|
let month_prime = (5 * day_of_year + 2) / 153;
|
||||||
|
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
|
||||||
|
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
|
||||||
|
year += if month <= 2 { 1 } else { 0 };
|
||||||
|
(year as i32, month as u32, day as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_epoch_days_to_gregorian_dates() {
|
||||||
|
assert_eq!(epoch_days_to_ymd(0), (1970, 1, 1));
|
||||||
|
assert_eq!(epoch_days_to_ymd(19_782), (2024, 2, 29));
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-17
@@ -81,27 +81,11 @@ fn now_rfc3339() -> String {
|
|||||||
let hour = seconds_in_day / 3_600;
|
let hour = seconds_in_day / 3_600;
|
||||||
let min = (seconds_in_day % 3_600) / 60;
|
let min = (seconds_in_day % 3_600) / 60;
|
||||||
let sec = seconds_in_day % 60;
|
let sec = seconds_in_day % 60;
|
||||||
let (year, month, day) = civil_from_days(days);
|
let (year, month, day) = crate::date::epoch_days_to_ymd(days);
|
||||||
|
|
||||||
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z")
|
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn civil_from_days(days_since_epoch: i64) -> (i64, u32, u32) {
|
|
||||||
let days = days_since_epoch + 719_468;
|
|
||||||
let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
|
|
||||||
let day_of_era = days - era * 146_097;
|
|
||||||
let year_of_era =
|
|
||||||
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
|
|
||||||
let mut year = year_of_era + era * 400;
|
|
||||||
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
|
|
||||||
let month_prime = (5 * day_of_year + 2) / 153;
|
|
||||||
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
|
|
||||||
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
|
|
||||||
year += if month <= 2 { 1 } else { 0 };
|
|
||||||
|
|
||||||
(year, month as u32, day as u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sanitize_state(state: &mut StateFile) {
|
pub fn sanitize_state(state: &mut StateFile) {
|
||||||
state.games.retain(|game| {
|
state.games.retain(|game| {
|
||||||
let executable = ExecutableInfo {
|
let executable = ExecutableInfo {
|
||||||
|
|||||||
+82
-26
@@ -11,60 +11,110 @@ pub fn render(
|
|||||||
command: Option<&[OsString]>,
|
command: Option<&[OsString]>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
output.push_str("gamewrap doctor\n");
|
output.push_str(&format!("{}\n", crate::color::bold("gamewrap doctor")));
|
||||||
output.push_str("Assumed launch context: Steam\n");
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"Failure notifier: {}\n",
|
"{} Steam\n",
|
||||||
|
crate::color::dim("Assumed launch context:")
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
"{} {}\n",
|
||||||
|
crate::color::dim("Failure notifier:"),
|
||||||
notify::available_notifier_name()
|
notify::available_notifier_name()
|
||||||
));
|
));
|
||||||
output.push_str("\nResolved settings:\n");
|
output.push_str(&format!("\n{}\n", crate::color::bold("Resolved settings:")));
|
||||||
output.push_str(&format!(" overlay: {}\n", color::on_off(settings.overlay)));
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" performance: {}\n",
|
" {} {}\n",
|
||||||
color::on_off(settings.performance)
|
crate::color::dim("mangohud:"),
|
||||||
|
color::on_off(settings.mangohud)
|
||||||
));
|
));
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" steam-host-libs: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamemode:"),
|
||||||
|
color::on_off(settings.gamemode)
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("steam-host-libs:"),
|
||||||
color::on_off(settings.steam_host_libs)
|
color::on_off(settings.steam_host_libs)
|
||||||
));
|
));
|
||||||
output.push_str(&format!(" game-libs: {}\n", settings.game_libs.as_str()));
|
|
||||||
output.push_str(&format!(" verbose: {}\n", color::on_off(settings.verbose)));
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" gamescope: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("game-libs:"),
|
||||||
|
crate::color::accent(settings.game_libs.as_str())
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("verbose:"),
|
||||||
|
color::on_off(settings.verbose)
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamescope:"),
|
||||||
color::on_off(settings.gamescope)
|
color::on_off(settings.gamescope)
|
||||||
));
|
));
|
||||||
if settings.gamescope {
|
if settings.gamescope {
|
||||||
if let Some(width) = settings.gamescope_width {
|
if let Some(width) = settings.gamescope_width {
|
||||||
output.push_str(&format!(" gamescope-width: {width}\n"));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamescope-width:"),
|
||||||
|
crate::color::accent(&width.as_display_str())
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(height) = settings.gamescope_height {
|
if let Some(height) = settings.gamescope_height {
|
||||||
output.push_str(&format!(" gamescope-height: {height}\n"));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamescope-height:"),
|
||||||
|
crate::color::accent(&height.as_display_str())
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(fps) = settings.gamescope_fps {
|
if let Some(fps) = settings.gamescope_fps {
|
||||||
output.push_str(&format!(" gamescope-fps: {fps}\n"));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamescope-fps:"),
|
||||||
|
crate::color::accent(&fps.to_string())
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" vkbasalt: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("vkbasalt:"),
|
||||||
color::on_off(settings.vkbasalt)
|
color::on_off(settings.vkbasalt)
|
||||||
));
|
));
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" esync: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("esync:"),
|
||||||
color::option_on_off(settings.esync)
|
color::option_on_off(settings.esync)
|
||||||
));
|
));
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" fsync: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("fsync:"),
|
||||||
color::option_on_off(settings.fsync)
|
color::option_on_off(settings.fsync)
|
||||||
));
|
));
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" large-address-aware: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("large-address-aware:"),
|
||||||
color::on_off(settings.large_address_aware)
|
color::on_off(settings.large_address_aware)
|
||||||
));
|
));
|
||||||
if let Some(pre_cmd) = &settings.pre_launch {
|
if let Some(pre_cmd) = &settings.pre_launch {
|
||||||
output.push_str(&format!(" pre-launch: {pre_cmd}\n"));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("pre-launch:"),
|
||||||
|
crate::color::accent(pre_cmd)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(post_cmd) = &settings.post_launch {
|
if let Some(post_cmd) = &settings.post_launch {
|
||||||
output.push_str(&format!(" post-launch: {post_cmd}\n"));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("post-launch:"),
|
||||||
|
crate::color::accent(post_cmd)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if settings.pre_launch.is_some() || settings.post_launch.is_some() {
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("hook-errors:"),
|
||||||
|
crate::color::accent(settings.hook_errors.as_str())
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(command) = command {
|
if let Some(command) = command {
|
||||||
@@ -73,22 +123,27 @@ pub fn render(
|
|||||||
.map(|part| part.to_string_lossy().into_owned())
|
.map(|part| part.to_string_lossy().into_owned())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" ");
|
.join(" ");
|
||||||
output.push_str(&format!("\nTarget command:\n {rendered}\n"));
|
output.push_str(&format!(
|
||||||
|
"\n{}\n {}\n",
|
||||||
|
crate::color::bold("Target command:"),
|
||||||
|
crate::color::accent(&rendered)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push_str("\nChecks:\n");
|
output.push_str(&format!("\n{}\n", crate::color::bold("Checks:")));
|
||||||
for check in &report.checks {
|
for check in &report.checks {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" [{}] {}: {}\n",
|
" [{}] {}: {}\n",
|
||||||
status_label(check.status),
|
status_label(check.status),
|
||||||
check.name,
|
crate::color::dim(&check.name),
|
||||||
check.detail
|
check.detail
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push_str("\nSummary:\n");
|
output.push_str(&format!("\n{}\n", crate::color::bold("Summary:")));
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" overall: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("overall:"),
|
||||||
if report.has_failures() {
|
if report.has_failures() {
|
||||||
color::fail("fail")
|
color::fail("fail")
|
||||||
} else if report.has_warnings() {
|
} else if report.has_warnings() {
|
||||||
@@ -98,7 +153,8 @@ pub fn render(
|
|||||||
}
|
}
|
||||||
));
|
));
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" launchable: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("launchable:"),
|
||||||
if report.launchable() {
|
if report.launchable() {
|
||||||
color::ok("yes")
|
color::ok("yes")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+111
-27
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
|||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::config::{GameLibsMode, ResolvedSettings};
|
use crate::config::ResolvedSettings;
|
||||||
|
|
||||||
pub fn is_steam_context() -> bool {
|
pub fn is_steam_context() -> bool {
|
||||||
std::env::var_os("SteamAppId").is_some()
|
std::env::var_os("SteamAppId").is_some()
|
||||||
@@ -11,6 +11,15 @@ pub fn is_steam_context() -> bool {
|
|||||||
|| std::env::var_os("SteamGameId").is_some()
|
|| std::env::var_os("SteamGameId").is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_proton_no_sync(map: &mut BTreeMap<OsString, OsString>, key: &str, enabled: Option<bool>) {
|
||||||
|
if let Some(v) = enabled {
|
||||||
|
map.insert(
|
||||||
|
OsString::from(key),
|
||||||
|
OsString::from(if v { "0" } else { "1" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_env(settings: ResolvedSettings) -> BTreeMap<OsString, OsString> {
|
pub fn build_env(settings: ResolvedSettings) -> BTreeMap<OsString, OsString> {
|
||||||
let mut env = BTreeMap::new();
|
let mut env = BTreeMap::new();
|
||||||
|
|
||||||
@@ -36,27 +45,40 @@ pub fn build_env(settings: ResolvedSettings) -> BTreeMap<OsString, OsString> {
|
|||||||
|
|
||||||
if settings.vkbasalt {
|
if settings.vkbasalt {
|
||||||
env.insert(OsString::from("ENABLE_VKBASALT"), OsString::from("1"));
|
env.insert(OsString::from("ENABLE_VKBASALT"), OsString::from("1"));
|
||||||
|
if let Some(ref cfg) = settings.vkbasalt_config {
|
||||||
|
env.insert(
|
||||||
|
OsString::from("VKBASALT_CONFIG_FILE"),
|
||||||
|
OsString::from(cfg.as_str()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(level) = settings.vkbasalt_log_level {
|
||||||
|
env.insert(
|
||||||
|
OsString::from("VKBASALT_LOG_LEVEL"),
|
||||||
|
OsString::from(level.as_str()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(fps_cap) = settings.fps_cap.filter(|_| settings.overlay) {
|
if settings.mangohud && settings.mangohud_log {
|
||||||
|
env.insert(OsString::from("MANGOHUD_LOG"), OsString::from("1"));
|
||||||
|
env.insert(OsString::from("MANGOHUD_LOG_DURATION"), OsString::from("0"));
|
||||||
|
if let Some(ref path) = settings.mangohud_log_path {
|
||||||
|
env.insert(
|
||||||
|
OsString::from("MANGOHUD_OUTPUT"),
|
||||||
|
OsString::from(path.as_str()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(fps_cap) = settings.fps_cap.filter(|_| settings.mangohud) {
|
||||||
env.insert(
|
env.insert(
|
||||||
OsString::from("MANGOHUD_PARAMS"),
|
OsString::from("MANGOHUD_PARAMS"),
|
||||||
OsString::from(format!("fps_limit={fps_cap}")),
|
OsString::from(format!("fps_limit={fps_cap}")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(esync) = settings.esync {
|
set_proton_no_sync(&mut env, "PROTON_NO_ESYNC", settings.esync);
|
||||||
env.insert(
|
set_proton_no_sync(&mut env, "PROTON_NO_FSYNC", settings.fsync);
|
||||||
OsString::from("PROTON_NO_ESYNC"),
|
|
||||||
OsString::from(if esync { "0" } else { "1" }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(fsync) = settings.fsync {
|
|
||||||
env.insert(
|
|
||||||
OsString::from("PROTON_NO_FSYNC"),
|
|
||||||
OsString::from(if fsync { "0" } else { "1" }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if settings.large_address_aware {
|
if settings.large_address_aware {
|
||||||
env.insert(
|
env.insert(
|
||||||
OsString::from("PROTON_LARGE_ADDRESS_AWARE"),
|
OsString::from("PROTON_LARGE_ADDRESS_AWARE"),
|
||||||
@@ -72,15 +94,7 @@ pub fn build_env(settings: ResolvedSettings) -> BTreeMap<OsString, OsString> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn needs_host_library_injection(settings: &ResolvedSettings) -> bool {
|
pub fn needs_host_library_injection(settings: &ResolvedSettings) -> bool {
|
||||||
if !settings.performance {
|
crate::launch::needs_host_libs_for_context(settings, is_steam_context())
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
match settings.game_libs {
|
|
||||||
GameLibsMode::Keep => false,
|
|
||||||
GameLibsMode::Gamemode => true,
|
|
||||||
GameLibsMode::Auto => is_steam_context(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn detected_host_library_dirs() -> Vec<String> {
|
pub fn detected_host_library_dirs() -> Vec<String> {
|
||||||
@@ -116,7 +130,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn gamemode_mode_always_sets_library_path() {
|
fn gamemode_mode_always_sets_library_path() {
|
||||||
let env = build_env(ResolvedSettings {
|
let env = build_env(ResolvedSettings {
|
||||||
performance: true,
|
gamemode: true,
|
||||||
game_libs: GameLibsMode::Gamemode,
|
game_libs: GameLibsMode::Gamemode,
|
||||||
..ResolvedSettings::default()
|
..ResolvedSettings::default()
|
||||||
});
|
});
|
||||||
@@ -131,7 +145,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn keep_mode_preserves_library_path() {
|
fn keep_mode_preserves_library_path() {
|
||||||
let env = build_env(ResolvedSettings {
|
let env = build_env(ResolvedSettings {
|
||||||
performance: true,
|
gamemode: true,
|
||||||
game_libs: GameLibsMode::Keep,
|
game_libs: GameLibsMode::Keep,
|
||||||
..ResolvedSettings::default()
|
..ResolvedSettings::default()
|
||||||
});
|
});
|
||||||
@@ -142,7 +156,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn fps_cap_uses_mangohud_params_when_overlay_is_enabled() {
|
fn fps_cap_uses_mangohud_params_when_overlay_is_enabled() {
|
||||||
let env = build_env(ResolvedSettings {
|
let env = build_env(ResolvedSettings {
|
||||||
overlay: true,
|
mangohud: true,
|
||||||
fps_cap: Some(60),
|
fps_cap: Some(60),
|
||||||
..ResolvedSettings::default()
|
..ResolvedSettings::default()
|
||||||
});
|
});
|
||||||
@@ -156,7 +170,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn fps_cap_is_ignored_when_overlay_is_disabled() {
|
fn fps_cap_is_ignored_when_overlay_is_disabled() {
|
||||||
let env = build_env(ResolvedSettings {
|
let env = build_env(ResolvedSettings {
|
||||||
overlay: false,
|
mangohud: false,
|
||||||
fps_cap: Some(60),
|
fps_cap: Some(60),
|
||||||
..ResolvedSettings::default()
|
..ResolvedSettings::default()
|
||||||
});
|
});
|
||||||
@@ -164,10 +178,43 @@ mod tests {
|
|||||||
assert!(!env.contains_key(&OsString::from("MANGOHUD_PARAMS")));
|
assert!(!env.contains_key(&OsString::from("MANGOHUD_PARAMS")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mangohud_logging_requires_overlay_and_sets_optional_output() {
|
||||||
|
let env = build_env(ResolvedSettings {
|
||||||
|
mangohud: true,
|
||||||
|
mangohud_log: true,
|
||||||
|
mangohud_log_path: Some("/tmp/mangohud.csv".to_string()),
|
||||||
|
..ResolvedSettings::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
env.get(&OsString::from("MANGOHUD_LOG")),
|
||||||
|
Some(&OsString::from("1"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
env.get(&OsString::from("MANGOHUD_LOG_DURATION")),
|
||||||
|
Some(&OsString::from("0"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
env.get(&OsString::from("MANGOHUD_OUTPUT")),
|
||||||
|
Some(&OsString::from("/tmp/mangohud.csv"))
|
||||||
|
);
|
||||||
|
|
||||||
|
let disabled = build_env(ResolvedSettings {
|
||||||
|
mangohud: false,
|
||||||
|
mangohud_log: true,
|
||||||
|
mangohud_log_path: Some("/tmp/mangohud.csv".to_string()),
|
||||||
|
..ResolvedSettings::default()
|
||||||
|
});
|
||||||
|
assert!(!disabled.contains_key(&OsString::from("MANGOHUD_LOG")));
|
||||||
|
assert!(!disabled.contains_key(&OsString::from("MANGOHUD_OUTPUT")));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn proton_and_vkbasalt_settings_set_expected_env_vars() {
|
fn proton_and_vkbasalt_settings_set_expected_env_vars() {
|
||||||
let env = build_env(ResolvedSettings {
|
let env = build_env(ResolvedSettings {
|
||||||
vkbasalt: true,
|
vkbasalt: true,
|
||||||
|
vkbasalt_config: Some("/tmp/vkbasalt.conf".to_string()),
|
||||||
esync: Some(true),
|
esync: Some(true),
|
||||||
fsync: Some(false),
|
fsync: Some(false),
|
||||||
..ResolvedSettings::default()
|
..ResolvedSettings::default()
|
||||||
@@ -177,6 +224,10 @@ mod tests {
|
|||||||
env.get(&OsString::from("ENABLE_VKBASALT")),
|
env.get(&OsString::from("ENABLE_VKBASALT")),
|
||||||
Some(&OsString::from("1"))
|
Some(&OsString::from("1"))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
env.get(&OsString::from("VKBASALT_CONFIG_FILE")),
|
||||||
|
Some(&OsString::from("/tmp/vkbasalt.conf"))
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
env.get(&OsString::from("PROTON_NO_ESYNC")),
|
env.get(&OsString::from("PROTON_NO_ESYNC")),
|
||||||
Some(&OsString::from("0"))
|
Some(&OsString::from("0"))
|
||||||
@@ -187,6 +238,39 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vkbasalt_config_is_ignored_when_vkbasalt_is_disabled() {
|
||||||
|
let env = build_env(ResolvedSettings {
|
||||||
|
vkbasalt: false,
|
||||||
|
vkbasalt_config: Some("/tmp/vkbasalt.conf".to_string()),
|
||||||
|
..ResolvedSettings::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(!env.contains_key(&OsString::from("VKBASALT_CONFIG_FILE")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vkbasalt_log_level_requires_vkbasalt() {
|
||||||
|
use crate::config::VkbasaltLogLevel;
|
||||||
|
|
||||||
|
let env = build_env(ResolvedSettings {
|
||||||
|
vkbasalt: true,
|
||||||
|
vkbasalt_log_level: Some(VkbasaltLogLevel::Warning),
|
||||||
|
..ResolvedSettings::default()
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
env.get(&OsString::from("VKBASALT_LOG_LEVEL")),
|
||||||
|
Some(&OsString::from("warning"))
|
||||||
|
);
|
||||||
|
|
||||||
|
let disabled = build_env(ResolvedSettings {
|
||||||
|
vkbasalt: false,
|
||||||
|
vkbasalt_log_level: Some(VkbasaltLogLevel::Debug),
|
||||||
|
..ResolvedSettings::default()
|
||||||
|
});
|
||||||
|
assert!(!disabled.contains_key(&OsString::from("VKBASALT_LOG_LEVEL")));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn large_address_aware_sets_env_var() {
|
fn large_address_aware_sets_env_var() {
|
||||||
let env = build_env(ResolvedSettings {
|
let env = build_env(ResolvedSettings {
|
||||||
|
|||||||
+7
-3
@@ -26,12 +26,16 @@ impl AppError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_with_hint(message: impl Into<String>, hint: impl Into<String>) -> String {
|
||||||
|
format!("Error: {}\nHint: {}", message.into(), hint.into())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn usage_error(message: impl Into<String>, hint: impl Into<String>) -> AppError {
|
pub fn usage_error(message: impl Into<String>, hint: impl Into<String>) -> AppError {
|
||||||
AppError::Usage(format!("Error: {}\nHint: {}", message.into(), hint.into()))
|
AppError::Usage(format_with_hint(message, hint))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config_error(message: impl Into<String>, hint: impl Into<String>) -> AppError {
|
pub fn config_error(message: impl Into<String>, hint: impl Into<String>) -> AppError {
|
||||||
AppError::Config(format!("Error: {}\nHint: {}", message.into(), hint.into()))
|
AppError::Config(format_with_hint(message, hint))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profile_not_found_error(name: &str) -> AppError {
|
pub fn profile_not_found_error(name: &str) -> AppError {
|
||||||
@@ -52,7 +56,7 @@ pub fn game_not_found_error(matcher: &str) -> AppError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn dependency_error(message: impl Into<String>, hint: impl Into<String>) -> AppError {
|
pub fn dependency_error(message: impl Into<String>, hint: impl Into<String>) -> AppError {
|
||||||
AppError::Dependency(format!("Error: {}\nHint: {}", message.into(), hint.into()))
|
AppError::Dependency(format_with_hint(message, hint))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn internal_error(message: impl Into<String>) -> AppError {
|
pub fn internal_error(message: impl Into<String>) -> AppError {
|
||||||
|
|||||||
+271
-44
@@ -18,21 +18,27 @@ pub const TOP_LEVEL_AFTER_HELP: &str = r#"Common tasks:
|
|||||||
gamewrap game rename "eldenring.exe" "Elden Ring"
|
gamewrap game rename "eldenring.exe" "Elden Ring"
|
||||||
Give a game a readable display name.
|
Give a game a readable display name.
|
||||||
|
|
||||||
gamewrap config set overlay on
|
gamewrap config settings
|
||||||
|
List all available settings with one-line descriptions.
|
||||||
|
|
||||||
|
gamewrap config set mangohud on
|
||||||
Enable MangoHud overlay by default.
|
Enable MangoHud overlay by default.
|
||||||
|
|
||||||
gamewrap config set fps-cap 60
|
gamewrap config set fps-cap 60
|
||||||
Cap frame rate through MangoHud (requires overlay to be on).
|
Cap frame rate through MangoHud (requires mangohud to be on).
|
||||||
|
|
||||||
gamewrap config set gamescope on
|
gamewrap config set gamescope on
|
||||||
Wrap all launches in the gamescope Wayland compositor.
|
Wrap all launches in the gamescope Wayland compositor.
|
||||||
|
|
||||||
gamewrap config set gamescope-width 1920
|
gamewrap config set gamescope-width native
|
||||||
Set the gamescope target resolution width.
|
Match the gamescope output width to the connected display.
|
||||||
|
|
||||||
gamewrap config set pre-launch "notify-send Starting"
|
gamewrap config set pre-launch "notify-send Starting"
|
||||||
Run a shell hook before launching the game.
|
Run a shell hook before launching the game.
|
||||||
|
|
||||||
|
gamewrap config reset --all
|
||||||
|
Reset every global setting to its built-in default.
|
||||||
|
|
||||||
gamewrap profile create benchmark
|
gamewrap profile create benchmark
|
||||||
Create a named profile with its own settings.
|
Create a named profile with its own settings.
|
||||||
|
|
||||||
@@ -42,9 +48,6 @@ pub const TOP_LEVEL_AFTER_HELP: &str = r#"Common tasks:
|
|||||||
gamewrap profile set benchmark fps-cap 120
|
gamewrap profile set benchmark fps-cap 120
|
||||||
Override a setting for one specific profile.
|
Override a setting for one specific profile.
|
||||||
|
|
||||||
gamewrap profile inherit benchmark base
|
|
||||||
Make a profile build on top of another profile.
|
|
||||||
|
|
||||||
gamewrap doctor
|
gamewrap doctor
|
||||||
Check that your default setup is ready for a Steam launch.
|
Check that your default setup is ready for a Steam launch.
|
||||||
|
|
||||||
@@ -56,15 +59,25 @@ pub const TOP_LEVEL_AFTER_HELP: &str = r#"Common tasks:
|
|||||||
|
|
||||||
Help topics (run for detailed docs):
|
Help topics (run for detailed docs):
|
||||||
gamewrap help settings all settings and their effects
|
gamewrap help settings all settings and their effects
|
||||||
gamewrap help profiles profiles, inheritance, and env overrides
|
gamewrap help profiles profiles, defaults, and env overrides
|
||||||
gamewrap help bindings binding games to profiles
|
gamewrap help bindings binding games to profiles
|
||||||
gamewrap help doctor understanding preflight checks
|
gamewrap help doctor understanding preflight checks
|
||||||
gamewrap help completion shell completion setup
|
gamewrap help completion shell completion setup
|
||||||
gamewrap help troubleshooting"#;
|
gamewrap help troubleshooting"#;
|
||||||
|
|
||||||
pub fn topic_text(topic: &str) -> Option<&'static str> {
|
pub fn topic_text(topic: &str, filter: Option<&str>) -> Option<&'static str> {
|
||||||
match topic {
|
match topic {
|
||||||
"settings" => Some(SETTINGS_HELP),
|
"settings" => match filter {
|
||||||
|
Some("core") => Some(SECTION_CORE),
|
||||||
|
Some("mangohud") => Some(SECTION_MANGOHUD),
|
||||||
|
Some("gamemode") => Some(SECTION_GAMEMODE),
|
||||||
|
Some("gamescope") => Some(SECTION_GAMESCOPE),
|
||||||
|
Some("vkbasalt") => Some(SECTION_VKBASALT),
|
||||||
|
Some("proton" | "wine" | "proton-wine") => Some(SECTION_PROTON),
|
||||||
|
Some("logging" | "hooks") => Some(SECTION_LOGGING),
|
||||||
|
Some(_) => None,
|
||||||
|
None => Some(SETTINGS_HELP),
|
||||||
|
},
|
||||||
"doctor" => Some(DOCTOR_HELP),
|
"doctor" => Some(DOCTOR_HELP),
|
||||||
"profiles" => Some(PROFILES_HELP),
|
"profiles" => Some(PROFILES_HELP),
|
||||||
"bindings" => Some(BINDINGS_HELP),
|
"bindings" => Some(BINDINGS_HELP),
|
||||||
@@ -74,17 +87,9 @@ pub fn topic_text(topic: &str) -> Option<&'static str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SETTINGS_HELP: &str = r#"Settings
|
macro_rules! section_core {
|
||||||
|
() => {
|
||||||
overlay
|
r#"── Core ────────────────────────────────────────────────────────────
|
||||||
What it does: turn MangoHud on or off.
|
|
||||||
Technical effect: prefixes the launch command with mangohud.
|
|
||||||
Values: on, off
|
|
||||||
|
|
||||||
performance
|
|
||||||
What it does: turn GameMode on or off.
|
|
||||||
Technical effect: prefixes the launch command with gamemoderun.
|
|
||||||
Values: on, off
|
|
||||||
|
|
||||||
steam-host-libs
|
steam-host-libs
|
||||||
What it does: prefer host libraries when Steam is using its runtime.
|
What it does: prefer host libraries when Steam is using its runtime.
|
||||||
@@ -95,7 +100,7 @@ game-libs
|
|||||||
What it does: control how game-related library paths are handled.
|
What it does: control how game-related library paths are handled.
|
||||||
Technical effect: controls whether auto-detected host library directories are appended to LD_LIBRARY_PATH.
|
Technical effect: controls whether auto-detected host library directories are appended to LD_LIBRARY_PATH.
|
||||||
Values:
|
Values:
|
||||||
auto append auto-detected host library paths only in Steam-like contexts when performance is on
|
auto append auto-detected host library paths only in Steam-like contexts when gamemode is on
|
||||||
keep leave LD_LIBRARY_PATH alone
|
keep leave LD_LIBRARY_PATH alone
|
||||||
gamemode always append auto-detected host library paths
|
gamemode always append auto-detected host library paths
|
||||||
|
|
||||||
@@ -103,33 +108,180 @@ verbose
|
|||||||
What it does: show more detail in diagnostic commands.
|
What it does: show more detail in diagnostic commands.
|
||||||
Technical effect: enables extra explanatory output from gamewrap itself.
|
Technical effect: enables extra explanatory output from gamewrap itself.
|
||||||
Values: on, off
|
Values: on, off
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! section_mangohud {
|
||||||
|
() => {
|
||||||
|
r#"── MangoHud ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
mangohud (was: overlay)
|
||||||
|
What it does: turn MangoHud on or off.
|
||||||
|
Technical effect: prefixes the launch command with mangohud.
|
||||||
|
Values: on, off
|
||||||
|
Default: on
|
||||||
|
|
||||||
|
fps-cap
|
||||||
|
What it does: cap frame rate.
|
||||||
|
Technical effect: sets MangoHud fps_limit through MANGOHUD_PARAMS when mangohud is on.
|
||||||
|
Values: number, or reset to use the default/clear
|
||||||
|
Example: gamewrap config set fps-cap 60
|
||||||
|
|
||||||
|
mangohud-log
|
||||||
|
What it does: log full-session FPS and performance data to a file.
|
||||||
|
Technical effect: sets MANGOHUD_LOG=1 and MANGOHUD_LOG_DURATION=0.
|
||||||
|
Values: on, off
|
||||||
|
Default: off
|
||||||
|
Requires: mangohud = on
|
||||||
|
|
||||||
|
mangohud-log-path
|
||||||
|
What it does: override where MangoHud writes the performance log.
|
||||||
|
Technical effect: sets MANGOHUD_OUTPUT when mangohud-log and mangohud are on.
|
||||||
|
Values: file path
|
||||||
|
Default: MangoHud's default location.
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! section_gamemode {
|
||||||
|
() => {
|
||||||
|
r#"── GameMode ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
gamemode (was: performance)
|
||||||
|
What it does: turn GameMode on or off.
|
||||||
|
Technical effect: prefixes the launch command with gamemoderun.
|
||||||
|
Values: on, off
|
||||||
|
Default: on
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! section_gamescope {
|
||||||
|
() => {
|
||||||
|
r#"── Gamescope ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Gamescope is a Wayland micro-compositor that provides resolution scaling,
|
||||||
|
refresh rate control, and HDR support. Enable it with 'gamescope = on',
|
||||||
|
then set width/height/fps to your preferences. gamescope-nested-* controls
|
||||||
|
the inner game resolution when scaling is active.
|
||||||
|
|
||||||
|
Recommended setup:
|
||||||
|
gamewrap config set gamescope on
|
||||||
|
gamewrap config set gamescope-mode fullscreen
|
||||||
|
gamewrap config set gamescope-width native
|
||||||
|
gamewrap config set gamescope-height native
|
||||||
|
|
||||||
|
Output vs. render resolution:
|
||||||
|
gamescope-width / gamescope-height = what the monitor displays (-W / -H flags)
|
||||||
|
gamescope-nested-width / gamescope-nested-height = what the game renders (-w / -h flags)
|
||||||
|
For native-res gaming, set gamescope-width/height to native and leave nested unset.
|
||||||
|
For upscaling: set nested to a lower render resolution and width/height to your monitor res.
|
||||||
|
Example: render at 1080p, output at 4K:
|
||||||
|
gamewrap config set gamescope-nested-width 1920
|
||||||
|
gamewrap config set gamescope-nested-height 1080
|
||||||
|
gamewrap config set gamescope-width native
|
||||||
|
gamewrap config set gamescope-height native
|
||||||
|
|
||||||
gamescope
|
gamescope
|
||||||
What it does: wrap the game in the gamescope Wayland compositor.
|
What it does: wrap the game in the gamescope Wayland compositor.
|
||||||
Technical effect: prefixes the launch command with gamescope [-- args] --.
|
Technical effect: prefixes the launch command with gamescope [args] --.
|
||||||
Values: on, off
|
Values: on, off
|
||||||
Requires: gamescope installed (https://github.com/ValveSoftware/gamescope)
|
Requires: gamescope installed (https://github.com/ValveSoftware/gamescope)
|
||||||
|
|
||||||
gamescope-width
|
gamescope-width
|
||||||
What it does: set the target game resolution width.
|
What it does: set the gamescope output width (what the monitor displays).
|
||||||
Technical effect: passes -W <value> to gamescope.
|
Technical effect: passes -W <value> to gamescope.
|
||||||
Values: number (e.g. 1920). Only effective when gamescope is on.
|
Values: pixel count (e.g. 1920) or native (auto-detects connected display)
|
||||||
|
Default: not set (gamescope uses 1280)
|
||||||
|
Example: gamewrap config set gamescope-width native
|
||||||
|
|
||||||
gamescope-height
|
gamescope-height
|
||||||
What it does: set the target game resolution height.
|
What it does: set the gamescope output height (what the monitor displays).
|
||||||
Technical effect: passes -H <value> to gamescope.
|
Technical effect: passes -H <value> to gamescope.
|
||||||
Values: number (e.g. 1080). Only effective when gamescope is on.
|
Values: pixel count (e.g. 1080) or native (auto-detects connected display)
|
||||||
|
Default: not set (gamescope uses 720)
|
||||||
|
Example: gamewrap config set gamescope-height native
|
||||||
|
|
||||||
gamescope-fps
|
gamescope-fps
|
||||||
What it does: set the target FPS for the gamescope compositor.
|
What it does: set the target FPS for the gamescope compositor.
|
||||||
Technical effect: passes -r <value> to gamescope.
|
Technical effect: passes -r <value> to gamescope.
|
||||||
Values: number (e.g. 60). Only effective when gamescope is on.
|
Values: number (e.g. 60). Only effective when gamescope is on.
|
||||||
|
|
||||||
fps-cap
|
gamescope-nested-width
|
||||||
What it does: cap frame rate.
|
What it does: set the game render width (inner resolution).
|
||||||
Technical effect: sets MangoHud fps_limit through MANGOHUD_PARAMS when overlay is on.
|
Technical effect: passes -w <value> to gamescope.
|
||||||
Values: number, or reset to inherit/clear
|
Values: number (e.g. 1280). Only effective when gamescope is on.
|
||||||
Example: gamewrap config set fps-cap 60
|
Note: combine with gamescope-width and gamescope-filter fsr/nis to upscale from a lower res.
|
||||||
|
|
||||||
|
gamescope-nested-height
|
||||||
|
What it does: set the game render height (inner resolution).
|
||||||
|
Technical effect: passes -h <value> to gamescope.
|
||||||
|
Values: number (e.g. 720). Only effective when gamescope is on.
|
||||||
|
|
||||||
|
gamescope-unfocused-fps
|
||||||
|
What it does: throttle FPS when the gamescope window is unfocused.
|
||||||
|
Technical effect: passes -o <value> to gamescope.
|
||||||
|
Values: number (e.g. 30). Only effective when gamescope is on.
|
||||||
|
|
||||||
|
gamescope-scaler
|
||||||
|
What it does: set how content is scaled to the output resolution.
|
||||||
|
Technical effect: passes -S <value> to gamescope.
|
||||||
|
Values: auto, integer, fit, fill, stretch. Only effective when gamescope is on.
|
||||||
|
|
||||||
|
gamescope-filter
|
||||||
|
What it does: set the upscaling/filtering algorithm.
|
||||||
|
Technical effect: passes -F <value> to gamescope.
|
||||||
|
Values: linear, nearest, fsr, nis, pixel. Only effective when gamescope is on.
|
||||||
|
Note: fsr (AMD FidelityFX SR) and nis (NVIDIA Image Scaling) are the quality upscalers.
|
||||||
|
|
||||||
|
gamescope-sharpness
|
||||||
|
What it does: set upscaler sharpness for fsr or nis filter.
|
||||||
|
Technical effect: passes --sharpness <value> to gamescope.
|
||||||
|
Values: 0 (maximum sharpness) to 20 (minimum sharpness). Only meaningful with fsr or nis filter.
|
||||||
|
|
||||||
|
gamescope-mode
|
||||||
|
What it does: set the gamescope window mode.
|
||||||
|
Technical effect: fullscreen passes -f; borderless passes -b; windowed passes neither.
|
||||||
|
Values: windowed, borderless, fullscreen
|
||||||
|
Default: windowed (no flag passed)
|
||||||
|
Example: gamewrap config set gamescope-mode fullscreen
|
||||||
|
|
||||||
|
gamescope-adaptive-sync
|
||||||
|
What it does: enable adaptive sync / variable refresh rate (VRR / FreeSync / G-Sync).
|
||||||
|
Technical effect: passes --adaptive-sync to gamescope.
|
||||||
|
Values: on, off. Requires a VRR-capable display. Only effective when gamescope is on.
|
||||||
|
|
||||||
|
gamescope-hdr
|
||||||
|
What it does: enable HDR output.
|
||||||
|
Technical effect: passes --hdr-enabled to gamescope.
|
||||||
|
Values: on, off. Requires gamescope WSI layer enabled and a compatible HDR display.
|
||||||
|
Only effective when gamescope is on.
|
||||||
|
|
||||||
|
gamescope-steam
|
||||||
|
What it does: enable Steam integration so the Steam overlay works inside gamescope.
|
||||||
|
Technical effect: passes -e to gamescope.
|
||||||
|
Values: on, off. Only effective when gamescope is on.
|
||||||
|
|
||||||
|
gamescope-expose-wayland
|
||||||
|
What it does: expose a Wayland socket for native Wayland game clients.
|
||||||
|
Technical effect: passes --expose-wayland to gamescope.
|
||||||
|
Values: on, off. Only effective when gamescope is on.
|
||||||
|
|
||||||
|
gamescope-mangoapp
|
||||||
|
What it does: use gamescope's native mangohud overlay (mangoapp) instead of running mangohud as a prefix.
|
||||||
|
Technical effect: passes --mangoapp to gamescope; skips the mangohud prefix in the command chain.
|
||||||
|
Values: on, off. Default: off.
|
||||||
|
Requires: mangoapp binary (included with the mangohud package on most distros).
|
||||||
|
Note: when both mangohud and gamescope are on, enabling this gives better reliability — the HUD
|
||||||
|
renders through gamescope's own compositor layer rather than via LD_PRELOAD injection.
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! section_vkbasalt {
|
||||||
|
() => {
|
||||||
|
r#"── vkBasalt ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
vkbasalt
|
vkbasalt
|
||||||
What it does: enable vkBasalt post-processing.
|
What it does: enable vkBasalt post-processing.
|
||||||
@@ -138,6 +290,25 @@ vkbasalt
|
|||||||
Default: off
|
Default: off
|
||||||
Requires: vkBasalt installed.
|
Requires: vkBasalt installed.
|
||||||
|
|
||||||
|
vkbasalt-config
|
||||||
|
What it does: point vkBasalt at a specific config file.
|
||||||
|
Technical effect: sets VKBASALT_CONFIG_FILE to the given path. Only active when vkbasalt is on.
|
||||||
|
Values: file path
|
||||||
|
Example: gamewrap profile set screenshot vkbasalt-config ~/.config/vkBasalt/hq.conf
|
||||||
|
|
||||||
|
vkbasalt-log-level
|
||||||
|
What it does: set vkBasalt log verbosity.
|
||||||
|
Technical effect: sets VKBASALT_LOG_LEVEL when vkbasalt is on.
|
||||||
|
Values: debug, info, warning, error, none
|
||||||
|
Requires: vkbasalt = on
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! section_proton {
|
||||||
|
() => {
|
||||||
|
r#"── Proton / Wine ────────────────────────────────────────────────────
|
||||||
|
|
||||||
esync
|
esync
|
||||||
What it does: force Proton esync on or off.
|
What it does: force Proton esync on or off.
|
||||||
Technical effect: sets PROTON_NO_ESYNC=0 when on, or PROTON_NO_ESYNC=1 when off.
|
Technical effect: sets PROTON_NO_ESYNC=0 when on, or PROTON_NO_ESYNC=1 when off.
|
||||||
@@ -156,6 +327,13 @@ large-address-aware / laa
|
|||||||
Values: on, off
|
Values: on, off
|
||||||
Default: off
|
Default: off
|
||||||
Context: only active in Proton context.
|
Context: only active in Proton context.
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! section_logging {
|
||||||
|
() => {
|
||||||
|
r#"── Hooks & Logging ──────────────────────────────────────────────────
|
||||||
|
|
||||||
pre-launch
|
pre-launch
|
||||||
What it does: run a shell command before launching the game.
|
What it does: run a shell command before launching the game.
|
||||||
@@ -166,40 +344,85 @@ pre-launch
|
|||||||
post-launch
|
post-launch
|
||||||
What it does: run a shell command after the game exits.
|
What it does: run a shell command after the game exits.
|
||||||
Technical effect: when set, gamewrap spawns the launch plan, waits for it to exit, then runs `sh -c <command>`.
|
Technical effect: when set, gamewrap spawns the launch plan, waits for it to exit, then runs `sh -c <command>`.
|
||||||
|
The post-hook is attempted even if the game fails to spawn, as long as the pre-launch hook succeeded.
|
||||||
Values: shell command string
|
Values: shell command string
|
||||||
Example: gamewrap profile set benchmark post-launch 'notify-send "Game exited"'
|
Example: gamewrap profile set benchmark post-launch 'notify-send "Game exited"'
|
||||||
|
|
||||||
|
hook-errors
|
||||||
|
What it does: control what happens when a pre-launch hook fails.
|
||||||
|
Technical effect: in fail mode, gamewrap aborts before launching when the pre-launch hook
|
||||||
|
exits nonzero or cannot be executed. In warn mode (the default), the failure is logged to
|
||||||
|
stderr and the launch continues.
|
||||||
|
Values: warn (default), fail
|
||||||
|
Note: the post-launch hook always runs after the pre-launch hook succeeds, even if the game
|
||||||
|
fails to start.
|
||||||
|
|
||||||
|
log-file
|
||||||
|
What it does: enable or disable gamewrap's own log file.
|
||||||
|
Technical effect: records what gamewrap decided at launch time — resolved profile, final command, env changes, and hook results.
|
||||||
|
Values: on, off
|
||||||
|
Default: off
|
||||||
|
|
||||||
|
log-path
|
||||||
|
What it does: override the default gamewrap log file location.
|
||||||
|
Values: file path
|
||||||
|
Default: ~/.local/state/gamewrap/gamewrap.log
|
||||||
|
|
||||||
env-vars
|
env-vars
|
||||||
What it does: apply arbitrary environment variable overrides at launch.
|
What it does: apply arbitrary environment variable overrides at launch.
|
||||||
Technical effect: inserts KEY=value pairs after gamewrap's standard environment variables, so they can override gamewrap defaults.
|
Technical effect: inserts KEY=value pairs after gamewrap's standard environment variables, so they can override gamewrap defaults.
|
||||||
Values: set through `gamewrap profile env set <profile> <KEY> <VALUE>`, not `gamewrap config set`.
|
Values: set through `gamewrap profile env set <profile> <KEY> <VALUE>`, not `gamewrap config set`.
|
||||||
Default: none
|
Default: none
|
||||||
"#;
|
"#
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECTION_CORE: &str = section_core!();
|
||||||
|
const SECTION_MANGOHUD: &str = section_mangohud!();
|
||||||
|
const SECTION_GAMEMODE: &str = section_gamemode!();
|
||||||
|
const SECTION_GAMESCOPE: &str = section_gamescope!();
|
||||||
|
const SECTION_VKBASALT: &str = section_vkbasalt!();
|
||||||
|
const SECTION_PROTON: &str = section_proton!();
|
||||||
|
const SECTION_LOGGING: &str = section_logging!();
|
||||||
|
|
||||||
|
const SETTINGS_HELP: &str = concat!(
|
||||||
|
section_core!(),
|
||||||
|
"\n",
|
||||||
|
section_mangohud!(),
|
||||||
|
"\n",
|
||||||
|
section_gamemode!(),
|
||||||
|
"\n",
|
||||||
|
section_gamescope!(),
|
||||||
|
"\n",
|
||||||
|
section_vkbasalt!(),
|
||||||
|
"\n",
|
||||||
|
section_proton!(),
|
||||||
|
"\n",
|
||||||
|
section_logging!(),
|
||||||
|
);
|
||||||
|
|
||||||
const PROFILES_HELP: &str = r#"Profiles
|
const PROFILES_HELP: &str = r#"Profiles
|
||||||
|
|
||||||
Profiles are reusable setting bundles.
|
Profiles are reusable setting bundles.
|
||||||
The default profile is built from your global config.
|
The default profile is built from your global config.
|
||||||
Named profiles override only the settings you change.
|
Named profiles override only the settings you change.
|
||||||
Profiles can also inherit from another named profile.
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
gamewrap profile list
|
gamewrap profile list
|
||||||
gamewrap profile tree
|
|
||||||
gamewrap profile create benchmark
|
gamewrap profile create benchmark
|
||||||
gamewrap profile duplicate benchmark benchmark-copy
|
gamewrap profile duplicate benchmark benchmark-copy
|
||||||
gamewrap profile inherit benchmark base
|
|
||||||
gamewrap profile clear-inherit benchmark
|
|
||||||
gamewrap profile export benchmark benchmark
|
gamewrap profile export benchmark benchmark
|
||||||
gamewrap profile import benchmark
|
gamewrap profile import benchmark
|
||||||
gamewrap profile set benchmark overlay on
|
gamewrap profile migrate old-benchmark
|
||||||
gamewrap profile reset benchmark overlay
|
gamewrap profile set benchmark mangohud on
|
||||||
|
gamewrap profile reset benchmark mangohud
|
||||||
gamewrap profile env set benchmark DXVK_ASYNC 1
|
gamewrap profile env set benchmark DXVK_ASYNC 1
|
||||||
gamewrap profile env list benchmark
|
gamewrap profile env list benchmark
|
||||||
gamewrap profile env unset benchmark DXVK_ASYNC
|
gamewrap profile env unset benchmark DXVK_ASYNC
|
||||||
gamewrap profile env clear benchmark
|
gamewrap profile env clear benchmark
|
||||||
gamewrap profile set benchmark performance on
|
gamewrap profile set benchmark gamemode on
|
||||||
gamewrap profile show benchmark
|
gamewrap profile show benchmark
|
||||||
|
gamewrap profile show benchmark --effective
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const DOCTOR_HELP: &str = r#"Doctor
|
const DOCTOR_HELP: &str = r#"Doctor
|
||||||
@@ -215,12 +438,13 @@ Examples:
|
|||||||
Check a specific game executable as if Steam were launching it.
|
Check a specific game executable as if Steam were launching it.
|
||||||
|
|
||||||
What it checks:
|
What it checks:
|
||||||
- MangoHud availability when overlay is on
|
- MangoHud availability when mangohud is on (or mangoapp when gamescope-mangoapp is on)
|
||||||
- gamescope availability when gamescope is on
|
- gamescope availability when gamescope is on
|
||||||
- GameMode availability when performance is on
|
- GameMode availability when gamemode is on
|
||||||
- vkBasalt layer availability when vkbasalt is on
|
- vkBasalt layer availability when vkbasalt is on
|
||||||
- auto-detected host library path injection for Steam/Proton-style launches
|
- auto-detected host library path injection for Steam/Proton-style launches
|
||||||
- whether a provided target command looks runnable
|
- whether a provided target command looks runnable
|
||||||
|
- warns when MangoHud is used as a prefix inside gamescope (suggest gamescope-mangoapp instead)
|
||||||
|
|
||||||
What the summary means:
|
What the summary means:
|
||||||
overall: ok
|
overall: ok
|
||||||
@@ -257,14 +481,17 @@ Matching is case-insensitive and checks the executable basename first.
|
|||||||
const TROUBLESHOOTING_HELP: &str = r#"Troubleshooting
|
const TROUBLESHOOTING_HELP: &str = r#"Troubleshooting
|
||||||
|
|
||||||
Missing mangohud
|
Missing mangohud
|
||||||
Install MangoHud or turn overlay off.
|
Install MangoHud or turn mangohud off.
|
||||||
|
|
||||||
Missing gamemoderun
|
Missing gamemoderun
|
||||||
Install GameMode or turn performance off.
|
Install GameMode or turn gamemode off.
|
||||||
|
|
||||||
Missing gamescope
|
Missing gamescope
|
||||||
Install gamescope or turn gamescope off.
|
Install gamescope or turn gamescope off.
|
||||||
|
|
||||||
|
Missing mangoapp
|
||||||
|
Install mangohud (which includes mangoapp) or turn gamescope-mangoapp off.
|
||||||
|
|
||||||
Steam runtime library issues
|
Steam runtime library issues
|
||||||
Try:
|
Try:
|
||||||
gamewrap config set steam-host-libs on
|
gamewrap config set steam-host-libs on
|
||||||
|
|||||||
+206
-62
@@ -52,33 +52,150 @@ impl PreflightReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect the primary display's native resolution.
|
||||||
|
/// Tries xrandr first, falls back to /sys/class/drm.
|
||||||
|
/// Returns None if detection fails; caller should warn and skip the flag.
|
||||||
|
fn detect_display_resolution() -> Option<(u32, u32)> {
|
||||||
|
if let Ok(output) = std::process::Command::new("xrandr")
|
||||||
|
.arg("--current")
|
||||||
|
.output()
|
||||||
|
&& output.status.success()
|
||||||
|
{
|
||||||
|
let text = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in text.lines() {
|
||||||
|
if line.contains('*') {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if let Some(res_part) = trimmed.split_whitespace().next()
|
||||||
|
&& let Some((w_str, h_str)) = res_part.split_once('x')
|
||||||
|
&& let (Ok(w), Ok(h)) = (w_str.parse::<u32>(), h_str.parse::<u32>())
|
||||||
|
{
|
||||||
|
return Some((w, h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
|
||||||
|
let mut paths: Vec<_> = entries.flatten().map(|entry| entry.path()).collect();
|
||||||
|
paths.sort();
|
||||||
|
for path in paths {
|
||||||
|
let modes_path = path.join("modes");
|
||||||
|
if let Ok(content) = std::fs::read_to_string(&modes_path)
|
||||||
|
&& let Some(first_line) = content.lines().next()
|
||||||
|
&& let Some((w_str, h_str)) = first_line.trim().split_once('x')
|
||||||
|
&& let (Ok(w), Ok(h)) = (w_str.parse::<u32>(), h_str.parse::<u32>())
|
||||||
|
{
|
||||||
|
return Some((w, h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_plan(target: &[OsString], settings: ResolvedSettings) -> Result<LaunchPlan, AppError> {
|
pub fn build_plan(target: &[OsString], settings: ResolvedSettings) -> Result<LaunchPlan, AppError> {
|
||||||
validate_launch(target, &settings, env::is_steam_context())?;
|
validate_launch(target, &settings, env::is_steam_context())?;
|
||||||
|
|
||||||
let mut command = Vec::with_capacity(target.len() + 10);
|
let mut command = Vec::with_capacity(target.len() + 10);
|
||||||
|
|
||||||
|
let use_mangoapp = settings.gamescope && settings.gamescope_mangoapp;
|
||||||
|
|
||||||
if settings.gamescope {
|
if settings.gamescope {
|
||||||
command.push(OsString::from("gamescope"));
|
command.push(OsString::from("gamescope"));
|
||||||
if let Some(width) = settings.gamescope_width {
|
match settings.gamescope_width {
|
||||||
|
Some(crate::config::GamescopeSize::Pixels(width)) => {
|
||||||
command.push(OsString::from("-W"));
|
command.push(OsString::from("-W"));
|
||||||
command.push(OsString::from(width.to_string()));
|
command.push(OsString::from(width.to_string()));
|
||||||
}
|
}
|
||||||
if let Some(height) = settings.gamescope_height {
|
Some(crate::config::GamescopeSize::Native) => {
|
||||||
|
if let Some((width, _)) = detect_display_resolution() {
|
||||||
|
command.push(OsString::from("-W"));
|
||||||
|
command.push(OsString::from(width.to_string()));
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"gamewrap: could not detect native display width; gamescope will use its default (1280)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
match settings.gamescope_height {
|
||||||
|
Some(crate::config::GamescopeSize::Pixels(height)) => {
|
||||||
command.push(OsString::from("-H"));
|
command.push(OsString::from("-H"));
|
||||||
command.push(OsString::from(height.to_string()));
|
command.push(OsString::from(height.to_string()));
|
||||||
}
|
}
|
||||||
|
Some(crate::config::GamescopeSize::Native) => {
|
||||||
|
if let Some((_, height)) = detect_display_resolution() {
|
||||||
|
command.push(OsString::from("-H"));
|
||||||
|
command.push(OsString::from(height.to_string()));
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"gamewrap: could not detect native display height; gamescope will use its default (720)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
if let Some(width) = settings.gamescope_nested_width {
|
||||||
|
command.push(OsString::from("-w"));
|
||||||
|
command.push(OsString::from(width.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(height) = settings.gamescope_nested_height {
|
||||||
|
command.push(OsString::from("-h"));
|
||||||
|
command.push(OsString::from(height.to_string()));
|
||||||
|
}
|
||||||
if let Some(fps) = settings.gamescope_fps {
|
if let Some(fps) = settings.gamescope_fps {
|
||||||
command.push(OsString::from("-r"));
|
command.push(OsString::from("-r"));
|
||||||
command.push(OsString::from(fps.to_string()));
|
command.push(OsString::from(fps.to_string()));
|
||||||
}
|
}
|
||||||
|
if let Some(fps) = settings.gamescope_unfocused_fps {
|
||||||
|
command.push(OsString::from("-o"));
|
||||||
|
command.push(OsString::from(fps.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(scaler) = settings.gamescope_scaler {
|
||||||
|
command.push(OsString::from("-S"));
|
||||||
|
command.push(OsString::from(scaler.as_str()));
|
||||||
|
}
|
||||||
|
if let Some(filter) = settings.gamescope_filter {
|
||||||
|
command.push(OsString::from("-F"));
|
||||||
|
command.push(OsString::from(filter.as_str()));
|
||||||
|
}
|
||||||
|
if let Some(sharpness) = settings.gamescope_sharpness {
|
||||||
|
command.push(OsString::from("--sharpness"));
|
||||||
|
command.push(OsString::from(sharpness.to_string()));
|
||||||
|
}
|
||||||
|
match settings.gamescope_window_mode {
|
||||||
|
Some(crate::config::GamescopeWindowMode::Fullscreen) => {
|
||||||
|
command.push(OsString::from("-f"));
|
||||||
|
}
|
||||||
|
Some(crate::config::GamescopeWindowMode::Borderless) => {
|
||||||
|
command.push(OsString::from("-b"));
|
||||||
|
}
|
||||||
|
Some(crate::config::GamescopeWindowMode::Windowed) | None => {}
|
||||||
|
}
|
||||||
|
if settings.gamescope_adaptive_sync {
|
||||||
|
command.push(OsString::from("--adaptive-sync"));
|
||||||
|
}
|
||||||
|
if settings.gamescope_hdr {
|
||||||
|
command.push(OsString::from("--hdr-enabled"));
|
||||||
|
}
|
||||||
|
if settings.gamescope_steam {
|
||||||
|
command.push(OsString::from("-e"));
|
||||||
|
}
|
||||||
|
if settings.gamescope_expose_wayland {
|
||||||
|
command.push(OsString::from("--expose-wayland"));
|
||||||
|
}
|
||||||
|
if use_mangoapp {
|
||||||
|
command.push(OsString::from("--mangoapp"));
|
||||||
|
}
|
||||||
command.push(OsString::from("--"));
|
command.push(OsString::from("--"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.overlay {
|
if settings.mangohud && !use_mangoapp {
|
||||||
command.push(OsString::from("mangohud"));
|
command.push(OsString::from("mangohud"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.performance {
|
if settings.gamemode {
|
||||||
command.push(OsString::from("gamemoderun"));
|
command.push(OsString::from("gamemoderun"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,13 +212,31 @@ pub fn preflight(
|
|||||||
) -> PreflightReport {
|
) -> PreflightReport {
|
||||||
let mut checks = Vec::new();
|
let mut checks = Vec::new();
|
||||||
|
|
||||||
if settings.overlay {
|
let use_mangoapp = settings.gamescope && settings.gamescope_mangoapp;
|
||||||
checks.push(check_dependency("overlay wrapper", "mangohud"));
|
|
||||||
|
if use_mangoapp {
|
||||||
|
checks.push(Check {
|
||||||
|
name: "mangohud wrapper".to_string(),
|
||||||
|
status: CheckStatus::Ok,
|
||||||
|
detail:
|
||||||
|
"gamescope-mangoapp is on — gamescope provides the MangoHud overlay via mangoapp."
|
||||||
|
.to_string(),
|
||||||
|
});
|
||||||
|
checks.push(check_dependency("gamescope mangoapp", "mangoapp"));
|
||||||
|
} else if settings.mangohud {
|
||||||
|
checks.push(check_dependency("mangohud wrapper", "mangohud"));
|
||||||
|
if settings.gamescope {
|
||||||
|
checks.push(Check {
|
||||||
|
name: "gamescope overlay".to_string(),
|
||||||
|
status: CheckStatus::Warn,
|
||||||
|
detail: "MangoHud is running as a prefix inside gamescope. For better reliability, enable `gamescope-mangoapp` to use gamescope's native overlay instead.".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
checks.push(Check {
|
checks.push(Check {
|
||||||
name: "overlay wrapper".to_string(),
|
name: "mangohud wrapper".to_string(),
|
||||||
status: CheckStatus::Ok,
|
status: CheckStatus::Ok,
|
||||||
detail: "overlay is off, so MangoHud is not required.".to_string(),
|
detail: "mangohud is off, so MangoHud is not required.".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,13 +250,13 @@ pub fn preflight(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.performance {
|
if settings.gamemode {
|
||||||
checks.push(check_dependency("performance wrapper", "gamemoderun"));
|
checks.push(check_dependency("gamemode wrapper", "gamemoderun"));
|
||||||
} else {
|
} else {
|
||||||
checks.push(Check {
|
checks.push(Check {
|
||||||
name: "performance wrapper".to_string(),
|
name: "gamemode wrapper".to_string(),
|
||||||
status: CheckStatus::Ok,
|
status: CheckStatus::Ok,
|
||||||
detail: "performance is off, so GameMode is not required.".to_string(),
|
detail: "gamemode is off, so GameMode is not required.".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,80 +300,74 @@ pub fn preflight(
|
|||||||
PreflightReport { checks }
|
PreflightReport { checks }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(plan: LaunchPlan) -> Result<(), AppError> {
|
fn build_std_command(plan: LaunchPlan) -> Result<Command, AppError> {
|
||||||
let executable = plan
|
let LaunchPlan { command, env } = plan;
|
||||||
.command
|
let executable = command
|
||||||
.first()
|
.first()
|
||||||
.ok_or_else(|| internal_error("Launch plan did not include a command."))?;
|
.ok_or_else(|| internal_error("Launch plan did not include a command."))?;
|
||||||
|
let mut cmd = Command::new(executable);
|
||||||
let mut command = Command::new(executable);
|
if command.len() > 1 {
|
||||||
if plan.command.len() > 1 {
|
cmd.args(&command[1..]);
|
||||||
command.args(&plan.command[1..]);
|
}
|
||||||
}
|
for (key, value) in env {
|
||||||
|
cmd.env(key, value);
|
||||||
for (key, value) in plan.env {
|
}
|
||||||
command.env(key, value);
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn execute(plan: LaunchPlan) -> Result<(), AppError> {
|
||||||
|
let mut command = build_std_command(plan)?;
|
||||||
let error = command.exec();
|
let error = command.exec();
|
||||||
Err(internal_error(format!(
|
Err(internal_error(format!(
|
||||||
"Failed to exec launch command: {error}"
|
"Failed to exec launch command: {error}"
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_wait(plan: LaunchPlan) -> Result<std::time::Duration, AppError> {
|
pub fn execute_wait(
|
||||||
let executable = plan
|
plan: LaunchPlan,
|
||||||
.command
|
) -> Result<(std::process::ExitStatus, std::time::Duration), AppError> {
|
||||||
.first()
|
let mut command = build_std_command(plan)?;
|
||||||
.ok_or_else(|| internal_error("Launch plan did not include a command."))?;
|
|
||||||
|
|
||||||
let mut command = Command::new(executable);
|
|
||||||
if plan.command.len() > 1 {
|
|
||||||
command.args(&plan.command[1..]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (key, value) in plan.env {
|
|
||||||
command.env(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let mut child = command
|
let mut child = command
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|error| internal_error(format!("Failed to spawn launch command: {error}")))?;
|
.map_err(|error| internal_error(format!("Failed to spawn launch command: {error}")))?;
|
||||||
child
|
let status = child
|
||||||
.wait()
|
.wait()
|
||||||
.map_err(|error| internal_error(format!("Failed to wait for game process: {error}")))?;
|
.map_err(|error| internal_error(format!("Failed to wait for game process: {error}")))?;
|
||||||
Ok(start.elapsed())
|
Ok((status, start.elapsed()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_plan(plan: &LaunchPlan, profile: &str, verbose: bool) -> String {
|
pub fn render_plan(plan: &LaunchPlan, profile: &str, verbose: bool) -> String {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
output.push_str(&format!("Resolved profile: {profile}\n"));
|
output.push_str(&format!(
|
||||||
|
"{} {}\n",
|
||||||
|
crate::color::dim("Resolved profile:"),
|
||||||
|
crate::color::accent(profile)
|
||||||
|
));
|
||||||
|
|
||||||
if verbose {
|
if verbose {
|
||||||
output.push_str("Environment changes:\n");
|
output.push_str(&format!("{}\n", crate::color::bold("Environment changes:")));
|
||||||
if plan.env.is_empty() {
|
if plan.env.is_empty() {
|
||||||
output.push_str(" (none)\n");
|
output.push_str(&format!(" {}\n", crate::color::dim("(none)")));
|
||||||
} else {
|
} else {
|
||||||
for (key, value) in &plan.env {
|
for (key, value) in &plan.env {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" {}={}\n",
|
" {}={}\n",
|
||||||
key.to_string_lossy(),
|
crate::color::accent(&key.to_string_lossy()),
|
||||||
value.to_string_lossy()
|
crate::color::dim(&value.to_string_lossy())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push_str("Final command:\n ");
|
output.push_str(&format!("{}\n ", crate::color::bold("Final command:")));
|
||||||
output.push_str(
|
let cmd_str = plan
|
||||||
&plan
|
|
||||||
.command
|
.command
|
||||||
.iter()
|
.iter()
|
||||||
.map(|part| shell_escape(part))
|
.map(shell_escape)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" "),
|
.join(" ");
|
||||||
);
|
output.push_str(&crate::color::accent(&cmd_str));
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,12 +432,23 @@ fn validate_launch(
|
|||||||
.ok_or_else(|| internal_error("Missing target command during launch planning."))?;
|
.ok_or_else(|| internal_error("Missing target command during launch planning."))?;
|
||||||
ensure_target_command(target_program)?;
|
ensure_target_command(target_program)?;
|
||||||
|
|
||||||
if settings.overlay {
|
let use_mangoapp = settings.gamescope && settings.gamescope_mangoapp;
|
||||||
ensure_dependency("mangohud", "overlay")?;
|
|
||||||
|
if use_mangoapp {
|
||||||
|
which("mangoapp").map(|_| ()).map_err(|_| {
|
||||||
|
dependency_error(
|
||||||
|
"`mangoapp` is required because `gamescope-mangoapp` is enabled.",
|
||||||
|
"Install `mangohud` (which includes `mangoapp`), or turn `gamescope-mangoapp` off with `gamewrap config set gamescope-mangoapp off`.",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.performance {
|
if settings.mangohud && !use_mangoapp {
|
||||||
ensure_dependency("gamemoderun", "performance")?;
|
ensure_dependency("mangohud", "mangohud")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.gamemode {
|
||||||
|
ensure_dependency("gamemoderun", "gamemode")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.gamescope {
|
if settings.gamescope {
|
||||||
@@ -337,8 +477,11 @@ fn ensure_library_paths_for_context(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_host_libs_for_context(settings: &ResolvedSettings, steam_context: bool) -> bool {
|
pub(crate) fn needs_host_libs_for_context(
|
||||||
if !settings.performance {
|
settings: &ResolvedSettings,
|
||||||
|
steam_context: bool,
|
||||||
|
) -> bool {
|
||||||
|
if !settings.gamemode {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +594,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let rendered = render_plan(&plan, "default", false);
|
let rendered = render_plan(&plan, "default", false);
|
||||||
assert!(rendered.contains("Resolved profile: default"));
|
assert!(rendered.contains("Resolved profile:"));
|
||||||
|
assert!(rendered.contains("default"));
|
||||||
assert!(rendered.contains("mangohud game.exe"));
|
assert!(rendered.contains("mangohud game.exe"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,8 +604,8 @@ mod tests {
|
|||||||
let error = build_plan(
|
let error = build_plan(
|
||||||
&[],
|
&[],
|
||||||
ResolvedSettings {
|
ResolvedSettings {
|
||||||
overlay: false,
|
mangohud: false,
|
||||||
performance: false,
|
gamemode: false,
|
||||||
steam_host_libs: false,
|
steam_host_libs: false,
|
||||||
game_libs: GameLibsMode::Keep,
|
game_libs: GameLibsMode::Keep,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
@@ -483,8 +627,8 @@ mod tests {
|
|||||||
let error = build_plan(
|
let error = build_plan(
|
||||||
&[OsString::from("--help")],
|
&[OsString::from("--help")],
|
||||||
ResolvedSettings {
|
ResolvedSettings {
|
||||||
overlay: false,
|
mangohud: false,
|
||||||
performance: false,
|
gamemode: false,
|
||||||
steam_host_libs: false,
|
steam_host_libs: false,
|
||||||
game_libs: GameLibsMode::Keep,
|
game_libs: GameLibsMode::Keep,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ mod cli;
|
|||||||
mod color;
|
mod color;
|
||||||
mod completion;
|
mod completion;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod date;
|
||||||
mod detect;
|
mod detect;
|
||||||
mod doctor;
|
mod doctor;
|
||||||
mod env;
|
mod env;
|
||||||
mod error;
|
mod error;
|
||||||
mod help;
|
mod help;
|
||||||
mod launch;
|
mod launch;
|
||||||
|
mod log;
|
||||||
mod notify;
|
mod notify;
|
||||||
mod profile;
|
mod profile;
|
||||||
mod share;
|
mod share;
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
use std::fs::{self, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
pub fn default_log_path(state_dir: &Path) -> PathBuf {
|
||||||
|
state_dir.join("gamewrap.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append timestamped lines to the log file. Logging must never crash gamewrap.
|
||||||
|
pub fn append(path: &Path, lines: &[String]) {
|
||||||
|
let _ = try_append(path, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_append(path: &Path, lines: &[String]) -> std::io::Result<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
|
||||||
|
let now = iso_timestamp();
|
||||||
|
for line in lines {
|
||||||
|
writeln!(file, "[{now}] {line}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iso_timestamp() -> String {
|
||||||
|
let secs = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
let s = secs % 60;
|
||||||
|
let m = (secs / 60) % 60;
|
||||||
|
let h = (secs / 3600) % 24;
|
||||||
|
let days = secs / 86400;
|
||||||
|
let (y, mo, d) = crate::date::epoch_days_to_ymd(days as i64);
|
||||||
|
format!("{y:04}-{mo:02}-{d:02} {h:02}:{m:02}:{s:02} UTC")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn appends_timestamped_lines() {
|
||||||
|
let temp = tempfile::tempdir().expect("temp dir");
|
||||||
|
let path = temp.path().join("nested/gamewrap.log");
|
||||||
|
|
||||||
|
append(&path, &["--- launch ---".to_string()]);
|
||||||
|
|
||||||
|
let content = fs::read_to_string(path).expect("log contents");
|
||||||
|
assert!(content.contains(" UTC] --- launch ---"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-207
@@ -1,7 +1,5 @@
|
|||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
use crate::bindings;
|
use crate::bindings;
|
||||||
use crate::config::{ConfigFile, ProfileConfig, ResolvedSettings};
|
use crate::config::{ConfigFile, ResolvedSettings};
|
||||||
use crate::detect::ExecutableInfo;
|
use crate::detect::ExecutableInfo;
|
||||||
use crate::error::{AppError, config_error};
|
use crate::error::{AppError, config_error};
|
||||||
|
|
||||||
@@ -46,7 +44,7 @@ pub fn resolve_named(config: &ConfigFile, profile_name: &str) -> Result<Resolved
|
|||||||
|
|
||||||
let mut settings = ResolvedSettings::default();
|
let mut settings = ResolvedSettings::default();
|
||||||
settings.apply(&config.defaults);
|
settings.apply(&config.defaults);
|
||||||
apply_profile_chain(config, profile, &mut settings)?;
|
settings.apply(&profile.settings);
|
||||||
|
|
||||||
Ok(ResolvedProfile {
|
Ok(ResolvedProfile {
|
||||||
profile_name: profile_name.to_string(),
|
profile_name: profile_name.to_string(),
|
||||||
@@ -64,208 +62,5 @@ pub fn validate_config(config: &ConfigFile) -> Result<(), AppError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut visiting = BTreeSet::new();
|
|
||||||
let mut visited = BTreeSet::new();
|
|
||||||
for name in config.profiles.keys() {
|
|
||||||
validate_profile_chain(config, name, &mut visiting, &mut visited)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_profile_chain(
|
|
||||||
config: &ConfigFile,
|
|
||||||
profile: &ProfileConfig,
|
|
||||||
settings: &mut ResolvedSettings,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
if let Some(parent) = &profile.inherits {
|
|
||||||
let parent_profile = config.profiles.get(parent).ok_or_else(|| {
|
|
||||||
config_error(
|
|
||||||
format!("Profile inherits from missing profile `{parent}`."),
|
|
||||||
"Fix the parent profile name or clear the inheritance chain.",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
apply_profile_chain(config, parent_profile, settings)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.apply(&profile.settings);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_profile_chain(
|
|
||||||
config: &ConfigFile,
|
|
||||||
name: &str,
|
|
||||||
visiting: &mut BTreeSet<String>,
|
|
||||||
visited: &mut BTreeSet<String>,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
if visited.contains(name) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visiting.insert(name.to_string()) {
|
|
||||||
return Err(config_error(
|
|
||||||
format!("Profile inheritance cycle detected at `{name}`."),
|
|
||||||
"Break the cycle with `gamewrap profile clear-inherit <name>` or choose a different parent profile.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(parent) = config
|
|
||||||
.profiles
|
|
||||||
.get(name)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
config_error(
|
|
||||||
format!("Profile `{name}` does not exist."),
|
|
||||||
"Run `gamewrap profile list` to see the available profiles.",
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.inherits
|
|
||||||
.as_ref()
|
|
||||||
{
|
|
||||||
if parent == name {
|
|
||||||
return Err(config_error(
|
|
||||||
format!("Profile `{name}` cannot inherit from itself."),
|
|
||||||
"Choose another parent profile or clear the inheritance chain.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if !config.profiles.contains_key(parent) {
|
|
||||||
return Err(config_error(
|
|
||||||
format!("Profile `{name}` inherits from missing profile `{parent}`."),
|
|
||||||
"Create the parent profile first or clear the inheritance chain.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
validate_profile_chain(config, parent, visiting, visited)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
visiting.remove(name);
|
|
||||||
visited.insert(name.to_string());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::config::{ConfigFile, GameLibsMode, Settings};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_named_applies_inheritance_chain() {
|
|
||||||
let config = ConfigFile {
|
|
||||||
defaults: Settings {
|
|
||||||
overlay: Some(false),
|
|
||||||
performance: Some(false),
|
|
||||||
steam_host_libs: None,
|
|
||||||
game_libs: None,
|
|
||||||
verbose: None,
|
|
||||||
gamescope: None,
|
|
||||||
gamescope_width: None,
|
|
||||||
gamescope_height: None,
|
|
||||||
gamescope_fps: None,
|
|
||||||
fps_cap: None,
|
|
||||||
env_vars: Some(BTreeMap::from([(
|
|
||||||
"GW_DEFAULT".to_string(),
|
|
||||||
"default".to_string(),
|
|
||||||
)])),
|
|
||||||
..Settings::default()
|
|
||||||
},
|
|
||||||
profiles: BTreeMap::from([
|
|
||||||
(
|
|
||||||
"base".to_string(),
|
|
||||||
ProfileConfig {
|
|
||||||
inherits: None,
|
|
||||||
settings: Settings {
|
|
||||||
overlay: Some(true),
|
|
||||||
performance: Some(true),
|
|
||||||
steam_host_libs: None,
|
|
||||||
game_libs: Some(GameLibsMode::Keep),
|
|
||||||
verbose: None,
|
|
||||||
gamescope: None,
|
|
||||||
gamescope_width: None,
|
|
||||||
gamescope_height: None,
|
|
||||||
gamescope_fps: None,
|
|
||||||
fps_cap: None,
|
|
||||||
env_vars: Some(BTreeMap::from([
|
|
||||||
("GW_BASE".to_string(), "base".to_string()),
|
|
||||||
("GW_OVERRIDE".to_string(), "base".to_string()),
|
|
||||||
])),
|
|
||||||
..Settings::default()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"benchmark".to_string(),
|
|
||||||
ProfileConfig {
|
|
||||||
inherits: Some("base".to_string()),
|
|
||||||
settings: Settings {
|
|
||||||
overlay: None,
|
|
||||||
performance: None,
|
|
||||||
steam_host_libs: Some(false),
|
|
||||||
game_libs: None,
|
|
||||||
verbose: Some(true),
|
|
||||||
gamescope: None,
|
|
||||||
gamescope_width: None,
|
|
||||||
gamescope_height: None,
|
|
||||||
gamescope_fps: None,
|
|
||||||
fps_cap: Some(60),
|
|
||||||
env_vars: Some(BTreeMap::from([(
|
|
||||||
"GW_OVERRIDE".to_string(),
|
|
||||||
"benchmark".to_string(),
|
|
||||||
)])),
|
|
||||||
..Settings::default()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
bindings: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let resolved = resolve_named(&config, "benchmark").expect("resolve benchmark");
|
|
||||||
assert_eq!(resolved.profile_name, "benchmark");
|
|
||||||
assert!(resolved.settings.overlay);
|
|
||||||
assert!(resolved.settings.performance);
|
|
||||||
assert!(!resolved.settings.steam_host_libs);
|
|
||||||
assert_eq!(resolved.settings.game_libs, GameLibsMode::Keep);
|
|
||||||
assert!(resolved.settings.verbose);
|
|
||||||
assert_eq!(resolved.settings.fps_cap, Some(60));
|
|
||||||
assert_eq!(
|
|
||||||
resolved.settings.env_vars.get("GW_DEFAULT"),
|
|
||||||
Some(&"default".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resolved.settings.env_vars.get("GW_BASE"),
|
|
||||||
Some(&"base".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resolved.settings.env_vars.get("GW_OVERRIDE"),
|
|
||||||
Some(&"benchmark".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_config_rejects_inheritance_cycles() {
|
|
||||||
let config = ConfigFile {
|
|
||||||
defaults: Settings::default(),
|
|
||||||
profiles: BTreeMap::from([
|
|
||||||
(
|
|
||||||
"alpha".to_string(),
|
|
||||||
ProfileConfig {
|
|
||||||
inherits: Some("beta".to_string()),
|
|
||||||
settings: Settings::default(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"beta".to_string(),
|
|
||||||
ProfileConfig {
|
|
||||||
inherits: Some("alpha".to_string()),
|
|
||||||
settings: Settings::default(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
bindings: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let error = validate_config(&config).expect_err("expected cycle validation to fail");
|
|
||||||
assert!(error.to_string().contains("inheritance cycle"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+70
-95
@@ -2,9 +2,9 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::config::{Binding, ConfigFile, ProfileConfig, ResolvedSettings, Settings};
|
use crate::config::migrate;
|
||||||
|
use crate::config::{Binding, ConfigFile, ProfileConfig, Settings};
|
||||||
use crate::error::{AppError, config_error};
|
use crate::error::{AppError, config_error};
|
||||||
use crate::profile;
|
|
||||||
|
|
||||||
const CONFIG_KIND: &str = "gamewrap-config";
|
const CONFIG_KIND: &str = "gamewrap-config";
|
||||||
const PROFILE_KIND: &str = "gamewrap-profile";
|
const PROFILE_KIND: &str = "gamewrap-profile";
|
||||||
@@ -16,9 +16,9 @@ pub const PROFILE_EXPORT_SUFFIX: &str = ".gamewrap-profile.toml";
|
|||||||
pub struct SharedConfigFile {
|
pub struct SharedConfigFile {
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub version: u32,
|
pub version: u32,
|
||||||
pub defaults: ResolvedSettings,
|
pub defaults: Settings,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub profiles: BTreeMap<String, ResolvedSettings>,
|
pub profiles: BTreeMap<String, Settings>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub bindings: Vec<Binding>,
|
pub bindings: Vec<Binding>,
|
||||||
}
|
}
|
||||||
@@ -28,22 +28,19 @@ pub struct SharedProfileFile {
|
|||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub version: u32,
|
pub version: u32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub settings: ResolvedSettings,
|
pub settings: Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn export_config(config: &ConfigFile) -> Result<SharedConfigFile, AppError> {
|
pub fn export_config(config: &ConfigFile) -> Result<SharedConfigFile, AppError> {
|
||||||
let defaults = resolved_defaults(&config.defaults);
|
|
||||||
let mut profiles = BTreeMap::new();
|
|
||||||
for name in config.profiles.keys() {
|
|
||||||
let resolved = profile::resolve_named(config, name)?;
|
|
||||||
profiles.insert(name.clone(), resolved.settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(SharedConfigFile {
|
Ok(SharedConfigFile {
|
||||||
kind: CONFIG_KIND.to_string(),
|
kind: CONFIG_KIND.to_string(),
|
||||||
version: FORMAT_VERSION,
|
version: FORMAT_VERSION,
|
||||||
defaults,
|
defaults: config.defaults.clone(),
|
||||||
profiles,
|
profiles: config
|
||||||
|
.profiles
|
||||||
|
.iter()
|
||||||
|
.map(|(name, profile)| (name.clone(), profile.settings.clone()))
|
||||||
|
.collect(),
|
||||||
bindings: config.bindings.clone(),
|
bindings: config.bindings.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -53,31 +50,28 @@ pub fn import_config(shared: SharedConfigFile) -> Result<ConfigFile, AppError> {
|
|||||||
validate_version(shared.version, "config")?;
|
validate_version(shared.version, "config")?;
|
||||||
|
|
||||||
Ok(ConfigFile {
|
Ok(ConfigFile {
|
||||||
defaults: explicit_settings(shared.defaults),
|
defaults: shared.defaults,
|
||||||
profiles: shared
|
profiles: shared
|
||||||
.profiles
|
.profiles
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, settings)| {
|
.map(|(name, settings)| (name, ProfileConfig { settings }))
|
||||||
(
|
|
||||||
name,
|
|
||||||
ProfileConfig {
|
|
||||||
inherits: None,
|
|
||||||
settings: explicit_settings(settings),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
.collect(),
|
||||||
bindings: shared.bindings,
|
bindings: shared.bindings,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn export_profile(config: &ConfigFile, name: &str) -> Result<SharedProfileFile, AppError> {
|
pub fn export_profile(config: &ConfigFile, name: &str) -> Result<SharedProfileFile, AppError> {
|
||||||
let resolved = profile::resolve_named(config, name)?;
|
let profile = config.profiles.get(name).ok_or_else(|| {
|
||||||
|
config_error(
|
||||||
|
format!("Profile `{name}` does not exist."),
|
||||||
|
"Run `gamewrap profile list` to see available profiles.",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
Ok(SharedProfileFile {
|
Ok(SharedProfileFile {
|
||||||
kind: PROFILE_KIND.to_string(),
|
kind: PROFILE_KIND.to_string(),
|
||||||
version: FORMAT_VERSION,
|
version: FORMAT_VERSION,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
settings: resolved.settings,
|
settings: profile.settings.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +82,48 @@ pub fn import_profile(shared: SharedProfileFile) -> Result<(String, ProfileConfi
|
|||||||
Ok((
|
Ok((
|
||||||
shared.name,
|
shared.name,
|
||||||
ProfileConfig {
|
ProfileConfig {
|
||||||
inherits: None,
|
settings: shared.settings,
|
||||||
settings: explicit_settings(shared.settings),
|
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a profile export TOML string, warn about renamed setting keys,
|
||||||
|
/// then return the parsed SharedProfileFile.
|
||||||
|
pub fn parse_imported_profile(content: &str) -> Result<SharedProfileFile, AppError> {
|
||||||
|
if let Ok(raw) = toml::from_str::<toml::Value>(content)
|
||||||
|
&& let Some(toml::Value::Table(settings)) = raw.get("settings")
|
||||||
|
{
|
||||||
|
let unknown = migrate::unknown_settings_in_table(settings);
|
||||||
|
if !unknown.is_empty() {
|
||||||
|
let keys = unknown.join(", ");
|
||||||
|
eprintln!("warning: This profile uses renamed settings that were skipped: {keys}.");
|
||||||
|
eprintln!(
|
||||||
|
" Run `gamewrap profile migrate <file>` to update the file before importing."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toml::from_str::<SharedProfileFile>(content).map_err(|error| {
|
||||||
|
config_error(
|
||||||
|
format!("Profile export file is invalid: {error}"),
|
||||||
|
"Use a `.gamewrap-profile.toml` export from `gamewrap profile export`.",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_imported_config(content: &str) -> Result<ConfigFile, AppError> {
|
pub fn parse_imported_config(content: &str) -> Result<ConfigFile, AppError> {
|
||||||
|
// Warn about any renamed settings in the defaults section before parsing.
|
||||||
|
if let Ok(raw) = toml::from_str::<toml::Value>(content)
|
||||||
|
&& let Some(toml::Value::Table(defaults)) = raw.get("defaults")
|
||||||
|
{
|
||||||
|
let unknown = migrate::unknown_settings_in_table(defaults);
|
||||||
|
if !unknown.is_empty() {
|
||||||
|
let keys = unknown.join(", ");
|
||||||
|
eprintln!("warning: This config uses renamed settings that were skipped: {keys}.");
|
||||||
|
eprintln!(
|
||||||
|
" Run `gamewrap config migrate <file>` to update the file before importing."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Ok(shared) = toml::from_str::<SharedConfigFile>(content) {
|
if let Ok(shared) = toml::from_str::<SharedConfigFile>(content) {
|
||||||
return import_config(shared);
|
return import_config(shared);
|
||||||
}
|
}
|
||||||
@@ -130,7 +159,7 @@ fn validate_kind(kind: &str, expected: &str, label: &str) -> Result<(), AppError
|
|||||||
} else {
|
} else {
|
||||||
Err(config_error(
|
Err(config_error(
|
||||||
format!("This file is not a {label} export."),
|
format!("This file is not a {label} export."),
|
||||||
&format!("Expected kind `{expected}`, but found `{kind}`."),
|
format!("Expected kind `{expected}`, but found `{kind}`."),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,92 +170,38 @@ fn validate_version(version: u32, label: &str) -> Result<(), AppError> {
|
|||||||
} else {
|
} else {
|
||||||
Err(config_error(
|
Err(config_error(
|
||||||
format!("This {label} export uses unsupported version `{version}`."),
|
format!("This {label} export uses unsupported version `{version}`."),
|
||||||
&format!("Expected version `{FORMAT_VERSION}`."),
|
format!("Expected version `{FORMAT_VERSION}`."),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolved_defaults(settings: &Settings) -> ResolvedSettings {
|
|
||||||
let mut resolved = ResolvedSettings::default();
|
|
||||||
resolved.apply(settings);
|
|
||||||
resolved
|
|
||||||
}
|
|
||||||
|
|
||||||
fn explicit_settings(settings: ResolvedSettings) -> Settings {
|
|
||||||
Settings {
|
|
||||||
overlay: Some(settings.overlay),
|
|
||||||
performance: Some(settings.performance),
|
|
||||||
steam_host_libs: Some(settings.steam_host_libs),
|
|
||||||
game_libs: Some(settings.game_libs),
|
|
||||||
verbose: Some(settings.verbose),
|
|
||||||
gamescope: Some(settings.gamescope),
|
|
||||||
gamescope_width: settings.gamescope_width,
|
|
||||||
gamescope_height: settings.gamescope_height,
|
|
||||||
gamescope_fps: settings.gamescope_fps,
|
|
||||||
fps_cap: settings.fps_cap,
|
|
||||||
vkbasalt: Some(settings.vkbasalt),
|
|
||||||
esync: settings.esync,
|
|
||||||
fsync: settings.fsync,
|
|
||||||
large_address_aware: Some(settings.large_address_aware),
|
|
||||||
pre_launch: settings.pre_launch,
|
|
||||||
post_launch: settings.post_launch,
|
|
||||||
env_vars: (!settings.env_vars.is_empty()).then_some(settings.env_vars),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::GameLibsMode;
|
use crate::config::GamescopeSize;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn import_profile_clears_inheritance_and_makes_values_explicit() {
|
fn import_profile_preserves_sparse_settings() {
|
||||||
let shared = SharedProfileFile {
|
let shared = SharedProfileFile {
|
||||||
kind: PROFILE_KIND.to_string(),
|
kind: PROFILE_KIND.to_string(),
|
||||||
version: FORMAT_VERSION,
|
version: FORMAT_VERSION,
|
||||||
name: "benchmark".to_string(),
|
name: "benchmark".to_string(),
|
||||||
settings: ResolvedSettings {
|
settings: Settings {
|
||||||
overlay: true,
|
mangohud: Some(true),
|
||||||
performance: false,
|
gamescope: Some(true),
|
||||||
steam_host_libs: true,
|
gamescope_width: Some(GamescopeSize::Pixels(1920)),
|
||||||
game_libs: GameLibsMode::Gamemode,
|
..Settings::default()
|
||||||
verbose: false,
|
|
||||||
gamescope: true,
|
|
||||||
gamescope_width: Some(1920),
|
|
||||||
gamescope_height: Some(1080),
|
|
||||||
gamescope_fps: Some(60),
|
|
||||||
fps_cap: Some(60),
|
|
||||||
vkbasalt: true,
|
|
||||||
esync: Some(true),
|
|
||||||
fsync: Some(false),
|
|
||||||
large_address_aware: true,
|
|
||||||
env_vars: BTreeMap::from([("GW_FLAG".to_string(), "1".to_string())]),
|
|
||||||
..ResolvedSettings::default()
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let (name, imported) = import_profile(shared).expect("import profile");
|
let (name, imported) = import_profile(shared).expect("import profile");
|
||||||
assert_eq!(name, "benchmark");
|
assert_eq!(name, "benchmark");
|
||||||
assert_eq!(imported.inherits, None);
|
assert_eq!(imported.settings.mangohud, Some(true));
|
||||||
assert_eq!(imported.settings.overlay, Some(true));
|
|
||||||
assert_eq!(imported.settings.performance, Some(false));
|
|
||||||
assert_eq!(imported.settings.game_libs, Some(GameLibsMode::Gamemode));
|
|
||||||
assert_eq!(imported.settings.gamescope, Some(true));
|
assert_eq!(imported.settings.gamescope, Some(true));
|
||||||
assert_eq!(imported.settings.gamescope_width, Some(1920));
|
|
||||||
assert_eq!(imported.settings.gamescope_height, Some(1080));
|
|
||||||
assert_eq!(imported.settings.gamescope_fps, Some(60));
|
|
||||||
assert_eq!(imported.settings.fps_cap, Some(60));
|
|
||||||
assert_eq!(imported.settings.vkbasalt, Some(true));
|
|
||||||
assert_eq!(imported.settings.esync, Some(true));
|
|
||||||
assert_eq!(imported.settings.fsync, Some(false));
|
|
||||||
assert_eq!(imported.settings.large_address_aware, Some(true));
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
imported
|
imported.settings.gamescope_width,
|
||||||
.settings
|
Some(GamescopeSize::Pixels(1920))
|
||||||
.env_vars
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|vars| vars.get("GW_FLAG")),
|
|
||||||
Some(&"1".to_string())
|
|
||||||
);
|
);
|
||||||
|
assert_eq!(imported.settings.gamemode, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+147
-44
@@ -13,104 +13,207 @@ pub fn render(paths: &AppPaths, config: &ConfigFile, state: &StateFile) -> Strin
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
output.push_str("Paths:\n");
|
output.push_str(&format!("{}\n", crate::color::bold("Paths:")));
|
||||||
output.push_str(&format!(" config: {}\n", paths.config_file.display()));
|
|
||||||
output.push_str(&format!(" state: {}\n", paths.state_file.display()));
|
|
||||||
output.push_str("\nDependencies:\n");
|
|
||||||
output.push_str(&format!(" mangohud: {}\n", dependency_state("mangohud")));
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" gamemoderun: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("config:"),
|
||||||
|
crate::color::dim(&paths.config_file.display().to_string())
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("state:"),
|
||||||
|
crate::color::dim(&paths.state_file.display().to_string())
|
||||||
|
));
|
||||||
|
output.push_str(&format!("\n{}\n", crate::color::bold("Dependencies:")));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("mangohud:"),
|
||||||
|
dependency_state("mangohud")
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamemoderun:"),
|
||||||
dependency_state("gamemoderun")
|
dependency_state("gamemoderun")
|
||||||
));
|
));
|
||||||
output.push_str(&format!(" zenity: {}\n", dependency_state("zenity")));
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" notify-send: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("zenity:"),
|
||||||
|
dependency_state("zenity")
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("notify-send:"),
|
||||||
dependency_state("notify-send")
|
dependency_state("notify-send")
|
||||||
));
|
));
|
||||||
output.push_str(&format!(" vkbasalt: {}\n", vkbasalt_state()));
|
|
||||||
output.push_str(&format!(" gamescope: {}\n", dependency_state("gamescope")));
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" failure notifier: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("vkbasalt:"),
|
||||||
|
vkbasalt_state()
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamescope:"),
|
||||||
|
dependency_state("gamescope")
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("failure notifier:"),
|
||||||
notify::available_notifier_name()
|
notify::available_notifier_name()
|
||||||
));
|
));
|
||||||
output.push_str(&format!(" host library dirs: {}\n", host_dirs_summary()));
|
|
||||||
output.push_str("\nResolved defaults:\n");
|
|
||||||
output.push_str(&format!(" overlay: {}\n", color::on_off(defaults.overlay)));
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" performance: {}\n",
|
" {} {}\n",
|
||||||
color::on_off(defaults.performance)
|
crate::color::dim("host library dirs:"),
|
||||||
|
host_dirs_summary()
|
||||||
|
));
|
||||||
|
output.push_str(&format!("\n{}\n", crate::color::bold("Resolved defaults:")));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("mangohud:"),
|
||||||
|
color::on_off(defaults.mangohud)
|
||||||
));
|
));
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" steam-host-libs: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamemode:"),
|
||||||
|
color::on_off(defaults.gamemode)
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("steam-host-libs:"),
|
||||||
color::on_off(defaults.steam_host_libs)
|
color::on_off(defaults.steam_host_libs)
|
||||||
));
|
));
|
||||||
output.push_str(&format!(" game-libs: {}\n", defaults.game_libs.as_str()));
|
|
||||||
output.push_str(&format!(" verbose: {}\n", color::on_off(defaults.verbose)));
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" gamescope: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("game-libs:"),
|
||||||
|
crate::color::accent(defaults.game_libs.as_str())
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("verbose:"),
|
||||||
|
color::on_off(defaults.verbose)
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamescope:"),
|
||||||
color::on_off(defaults.gamescope)
|
color::on_off(defaults.gamescope)
|
||||||
));
|
));
|
||||||
if let Some(width) = defaults.gamescope_width {
|
if let Some(width) = defaults.gamescope_width {
|
||||||
output.push_str(&format!(" gamescope-width: {width}\n"));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamescope-width:"),
|
||||||
|
crate::color::accent(&width.as_display_str())
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(height) = defaults.gamescope_height {
|
if let Some(height) = defaults.gamescope_height {
|
||||||
output.push_str(&format!(" gamescope-height: {height}\n"));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamescope-height:"),
|
||||||
|
crate::color::accent(&height.as_display_str())
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(fps) = defaults.gamescope_fps {
|
if let Some(fps) = defaults.gamescope_fps {
|
||||||
output.push_str(&format!(" gamescope-fps: {fps}\n"));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("gamescope-fps:"),
|
||||||
|
crate::color::accent(&fps.to_string())
|
||||||
|
));
|
||||||
}
|
}
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" vkbasalt: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("vkbasalt:"),
|
||||||
color::on_off(defaults.vkbasalt)
|
color::on_off(defaults.vkbasalt)
|
||||||
));
|
));
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" esync: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("esync:"),
|
||||||
color::option_on_off(defaults.esync)
|
color::option_on_off(defaults.esync)
|
||||||
));
|
));
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" fsync: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("fsync:"),
|
||||||
color::option_on_off(defaults.fsync)
|
color::option_on_off(defaults.fsync)
|
||||||
));
|
));
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
" large-address-aware: {}\n",
|
" {} {}\n",
|
||||||
|
crate::color::dim("large-address-aware:"),
|
||||||
color::on_off(defaults.large_address_aware)
|
color::on_off(defaults.large_address_aware)
|
||||||
));
|
));
|
||||||
output.push_str("\nProfiles:\n");
|
output.push_str(&format!("\n{}\n", crate::color::bold("Profiles:")));
|
||||||
output.push_str(&format!(" count: {}\n", config.profiles.len()));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("count:"),
|
||||||
|
config.profiles.len()
|
||||||
|
));
|
||||||
if config.profiles.is_empty() {
|
if config.profiles.is_empty() {
|
||||||
output.push_str(" names: (none)\n");
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("names:"),
|
||||||
|
crate::color::dim("(none)")
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
let names = config
|
let names = config
|
||||||
.profiles
|
.profiles
|
||||||
.keys()
|
.keys()
|
||||||
.map(String::as_str)
|
.map(|name| crate::color::accent(name))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
output.push_str(&format!(" names: {names}\n"));
|
output.push_str(&format!(" {} {names}\n", crate::color::dim("names:")));
|
||||||
}
|
}
|
||||||
output.push_str("Bindings:\n");
|
output.push_str(&format!("{}\n", crate::color::bold("Bindings:")));
|
||||||
output.push_str(&format!(" count: {}\n", config.bindings.len()));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("count:"),
|
||||||
|
config.bindings.len()
|
||||||
|
));
|
||||||
if config.bindings.is_empty() {
|
if config.bindings.is_empty() {
|
||||||
output.push_str(" items: (none)\n");
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("items:"),
|
||||||
|
crate::color::dim("(none)")
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
for binding in &config.bindings {
|
for binding in &config.bindings {
|
||||||
output.push_str(&format!(" {} -> {}\n", binding.matcher, binding.profile));
|
output.push_str(&format!(
|
||||||
|
" {} {} {}\n",
|
||||||
|
crate::color::accent(&binding.matcher),
|
||||||
|
crate::color::dim("->"),
|
||||||
|
crate::color::accent(&binding.profile)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
output.push_str("Observed games:\n");
|
output.push_str(&format!("{}\n", crate::color::bold("Observed games:")));
|
||||||
output.push_str(&format!(" count: {}\n", state.games.len()));
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("count:"),
|
||||||
|
state.games.len()
|
||||||
|
));
|
||||||
if state.games.is_empty() {
|
if state.games.is_empty() {
|
||||||
output.push_str(" items: (none)\n");
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("items:"),
|
||||||
|
crate::color::dim("(none)")
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
for game in &state.games {
|
for game in &state.games {
|
||||||
let resolved_profile =
|
let resolved_profile =
|
||||||
bindings::resolve_profile_for_observed(config, game).unwrap_or("default");
|
bindings::resolve_profile_for_observed(config, game).unwrap_or("default");
|
||||||
output.push_str(&format!(" {}\n", game.executable));
|
output.push_str(&format!(" {}\n", crate::color::bold(&game.executable)));
|
||||||
output.push_str(&format!(" resolved profile: {resolved_profile}\n"));
|
output.push_str(&format!(
|
||||||
output.push_str(&format!(" last launched: {}\n", game.last_profile));
|
" {} {}\n",
|
||||||
output.push_str(&format!(" path: {}\n", game.command_path));
|
crate::color::dim("resolved profile:"),
|
||||||
|
crate::color::accent(resolved_profile)
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("last launched:"),
|
||||||
|
game.last_profile
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
crate::color::dim("path:"),
|
||||||
|
crate::color::dim(&game.command_path)
|
||||||
|
));
|
||||||
if let Some(note) = &game.note {
|
if let Some(note) = &game.note {
|
||||||
output.push_str(&format!(" note: {note}\n"));
|
output.push_str(&format!(" {} {note}\n", crate::color::dim("note:")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2020
-163
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user