f984acf0e3
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>
349 lines
14 KiB
Rust
349 lines
14 KiB
Rust
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 0–20."),
|
||
"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).",
|
||
)),
|
||
}
|
||
}
|