Files
gamewrap/src/config/keys.rs
T
44r0n7 f984acf0e3 feat: hook reliability, hook-errors setting, sparse config, and logging
Hook reliability:
- Add hook-errors = "warn" | "fail" setting (default: warn); in fail
  mode, abort launch when pre-launch hook exits nonzero or can't execute
- Ensure post-launch hook runs unconditionally, even when execute_wait()
  fails to spawn the game
- Propagate game's real exit status via std::process::exit(); report
  post-hook failures clearly to stderr
- Centralize hook execution via run_hook() helper (sh -c)

New features in this batch:
- Sparse config and profile support: only configured fields are written;
  unset fields fall back through profile → global chain
- config show --effective flag: renders the fully-resolved view
- Config migration: upgrades legacy flat config to current schema
- Structured decision logging (src/log.rs) for session-level audit trail
- Gamescope improvements: additional flags and validation
- CHANGELOG.md tracking template releases

Schema / UX:
- HookErrors enum (Warn/Fail) added to Settings and ResolvedSettings
- hook-errors key in keys.rs, mod.rs rendering, completion candidates,
  doctor output, help text, README, and dry-run display
- 9 focused tests covering warn/fail behavior, exit propagation,
  round-trip (set/show/reset), profile round-trip, export/import

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 09:35:24 -04:00

349 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::config::{
GameLibsMode, GamescopeFilter, GamescopeScaler, GamescopeSize, GamescopeWindowMode, Settings,
VkbasaltLogLevel,
};
use crate::error::{AppError, config_error};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingKey {
Mangohud,
Gamemode,
SteamHostLibs,
GameLibs,
Verbose,
LogFile,
LogPath,
Gamescope,
GamescopeWidth,
GamescopeHeight,
GamescopeFps,
GamescopeNestedWidth,
GamescopeNestedHeight,
GamescopeUnfocusedFps,
GamescopeScaler,
GamescopeFilter,
GamescopeSharpness,
GamescopeMode,
GamescopeAdaptiveSync,
GamescopeHdr,
GamescopeSteam,
GamescopeExposeWayland,
GamescopeMangoapp,
FpsCap,
MangohudLog,
MangohudLogPath,
Vkbasalt,
VkbasaltConfig,
VkbasaltLogLevel,
Esync,
Fsync,
LargeAddressAware,
PreLaunch,
PostLaunch,
HookErrors,
}
impl SettingKey {
pub fn parse(value: &str) -> Result<Self, AppError> {
match value {
"mangohud" => Ok(Self::Mangohud),
"gamemode" => Ok(Self::Gamemode),
"steam-host-libs" | "host-libs" => Ok(Self::SteamHostLibs),
"game-libs" => Ok(Self::GameLibs),
"verbose" => Ok(Self::Verbose),
"log-file" => Ok(Self::LogFile),
"log-path" => Ok(Self::LogPath),
"gamescope" => Ok(Self::Gamescope),
"gamescope-width" => Ok(Self::GamescopeWidth),
"gamescope-height" => Ok(Self::GamescopeHeight),
"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),
"mangohud-log" => Ok(Self::MangohudLog),
"mangohud-log-path" => Ok(Self::MangohudLogPath),
"vkbasalt" => Ok(Self::Vkbasalt),
"vkbasalt-config" => Ok(Self::VkbasaltConfig),
"vkbasalt-log-level" => Ok(Self::VkbasaltLogLevel),
"esync" => Ok(Self::Esync),
"fsync" => Ok(Self::Fsync),
"large-address-aware" | "laa" => Ok(Self::LargeAddressAware),
"pre-launch" => Ok(Self::PreLaunch),
"post-launch" => Ok(Self::PostLaunch),
"hook-errors" => Ok(Self::HookErrors),
_ => Err(config_error(
format!("`{value}` is not a known setting."),
"Run `gamewrap help settings` to see all available settings.",
)),
}
}
}
pub fn set_value(settings: &mut Settings, key: SettingKey, value: &str) -> Result<(), AppError> {
match key {
SettingKey::Mangohud => settings.mangohud = Some(parse_toggle(value)?),
SettingKey::Gamemode => settings.gamemode = 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::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::GamescopeWidth => {
settings.gamescope_width = Some(parse_gamescope_size(value)?);
}
SettingKey::GamescopeHeight => {
settings.gamescope_height = Some(parse_gamescope_size(value)?);
}
SettingKey::GamescopeFps => {
settings.gamescope_fps = Some(parse_fps(value, "gamescope-fps")?);
}
SettingKey::GamescopeNestedWidth => {
settings.gamescope_nested_width = Some(parse_pixel_count(value)?);
}
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 => {
let n: u32 = value.parse().map_err(|_| {
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::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::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::Fsync => settings.fsync = Some(parse_toggle(value)?),
SettingKey::LargeAddressAware => settings.large_address_aware = Some(parse_toggle(value)?),
SettingKey::PreLaunch => settings.pre_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(())
}
pub fn reset_value(settings: &mut Settings, key: SettingKey) {
match key {
SettingKey::Mangohud => settings.mangohud = None,
SettingKey::Gamemode => settings.gamemode = None,
SettingKey::SteamHostLibs => settings.steam_host_libs = None,
SettingKey::GameLibs => settings.game_libs = None,
SettingKey::Verbose => settings.verbose = None,
SettingKey::LogFile => settings.log_file = None,
SettingKey::LogPath => settings.log_path = None,
SettingKey::Gamescope => settings.gamescope = None,
SettingKey::GamescopeWidth => settings.gamescope_width = None,
SettingKey::GamescopeHeight => settings.gamescope_height = 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::MangohudLog => settings.mangohud_log = None,
SettingKey::MangohudLogPath => settings.mangohud_log_path = 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::Fsync => settings.fsync = None,
SettingKey::LargeAddressAware => settings.large_address_aware = None,
SettingKey::PreLaunch => settings.pre_launch = None,
SettingKey::PostLaunch => settings.post_launch = None,
SettingKey::HookErrors => settings.hook_errors = None,
}
}
fn parse_toggle(value: &str) -> Result<bool, AppError> {
match value {
"on" => Ok(true),
"off" => Ok(false),
_ => Err(config_error(
format!("`{value}` is not a valid on/off value."),
"Use `on` or `off`.",
)),
}
}
fn parse_game_libs(value: &str) -> Result<GameLibsMode, AppError> {
match value {
"auto" => Ok(GameLibsMode::Auto),
"keep" => Ok(GameLibsMode::Keep),
"gamemode" => Ok(GameLibsMode::Gamemode),
_ => Err(config_error(
format!("`{value}` is not a valid value for game-libs."),
"Use one of: auto, keep, gamemode.",
)),
}
}
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 020."),
"0 is maximum sharpness, 20 is minimum sharpness.",
));
}
Ok(n)
}
fn parse_pixel_count(value: &str) -> Result<u32, AppError> {
value.parse::<u32>().map_err(|_| {
config_error(
format!("`{value}` is not a valid pixel count. Use a number like `1920`."),
"Use a whole-number pixel count for gamescope resolution settings.",
)
})
}
fn parse_fps(value: &str, setting: &str) -> Result<u32, AppError> {
value.parse::<u32>().map_err(|_| {
config_error(
format!("`{value}` is not a valid FPS. Use a number like `60`."),
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).",
)),
}
}