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>
This commit is contained in:
+216
-15
@@ -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};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SettingKey {
|
||||
Overlay,
|
||||
Performance,
|
||||
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 {
|
||||
"overlay" => Ok(Self::Overlay),
|
||||
"performance" => Ok(Self::Performance),
|
||||
"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."),
|
||||
"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,20 +91,58 @@ impl SettingKey {
|
||||
|
||||
pub fn set_value(settings: &mut Settings, key: SettingKey, value: &str) -> Result<(), AppError> {
|
||||
match key {
|
||||
SettingKey::Overlay => settings.overlay = Some(parse_toggle(value)?),
|
||||
SettingKey::Performance => settings.performance = Some(parse_toggle(value)?),
|
||||
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_pixel_count(value)?);
|
||||
settings.gamescope_width = Some(parse_gamescope_size(value)?);
|
||||
}
|
||||
SettingKey::GamescopeHeight => {
|
||||
settings.gamescope_height = Some(parse_pixel_count(value)?);
|
||||
settings.gamescope_height = Some(parse_gamescope_size(value)?);
|
||||
}
|
||||
SettingKey::GamescopeFps => {
|
||||
settings.gamescope_fps = Some(parse_gamescope_fps(value)?);
|
||||
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(|_| {
|
||||
@@ -74,34 +153,60 @@ pub fn set_value(settings: &mut Settings, key: SettingKey, value: &str) -> Resul
|
||||
})?;
|
||||
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::Overlay => settings.overlay = None,
|
||||
SettingKey::Performance => settings.performance = None,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +233,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> {
|
||||
value.parse::<u32>().map_err(|_| {
|
||||
config_error(
|
||||
@@ -137,11 +327,22 @@ 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(|_| {
|
||||
config_error(
|
||||
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).",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user