refactor: deduplicate and clean up across all source modules

- Extract canonical date algorithm to src/date.rs; remove duplicates in log.rs and detect.rs
- Extract build_std_command in launch.rs; unify needs_host_library_injection via pub(crate) delegation
- Add missing unknown-setting warning to parse_imported_config in share.rs
- Extract format_with_hint helper in error.rs; set_proton_no_sync helper in env.rs
- Remove dead match in completion.rs shell_path_literal; use parse_fps for FpsCap in keys.rs
- Replace six as_str() impls with impl_as_str! macro in schema.rs
- Collapse ResolvedSettings::apply (~110 lines) with apply_scalar!/apply_opt!/apply_clone! macros
- Replace six color functions with color_fn! macro in color.rs

89/89 tests passing, zero clippy warnings.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-06-14 11:08:43 -04:00
parent 76ab5351a9
commit ad46ad6b14
14 changed files with 220 additions and 319 deletions
+1
View File
@@ -252,3 +252,4 @@ Do not update this file for:
- [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] 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-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] 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.
+1 -4
View File
@@ -1533,10 +1533,7 @@ fn launch_command(
} }
(secs, exit_status_label(*exit_status)) (secs, exit_status_label(*exit_status))
} }
Err(err) => { Err(_) => (0, "unavailable".to_string()),
eprintln!("gamewrap: {err}");
(0, "unavailable".to_string())
}
}; };
// Run post-launch hook unconditionally (even when the game failed to spawn). // Run post-launch hook unconditionally (even when the game failed to spawn).
+12 -41
View File
@@ -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") }
+2 -5
View File
@@ -474,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> {
+8 -9
View File
@@ -144,15 +144,7 @@ pub fn set_value(settings: &mut Settings, key: SettingKey, value: &str) -> Resul
SettingKey::GamescopeMangoapp => { SettingKey::GamescopeMangoapp => {
settings.gamescope_mangoapp = Some(parse_toggle(value)?); settings.gamescope_mangoapp = Some(parse_toggle(value)?);
} }
SettingKey::FpsCap => { SettingKey::FpsCap => settings.fps_cap = Some(parse_fps(value, "fps-cap")?),
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::MangohudLog => settings.mangohud_log = Some(parse_toggle(value)?),
SettingKey::MangohudLogPath => settings.mangohud_log_path = Some(value.to_string()), 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)?),
@@ -329,10 +321,17 @@ fn parse_pixel_count(value: &str) -> Result<u32, AppError> {
fn parse_fps(value: &str, setting: &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`."),
format!("Use `gamewrap config set {setting} 60` for a 60 FPS target."), format!("Use `gamewrap config set {setting} 60` for a 60 FPS target."),
) )
}
}) })
} }
+109 -164
View File
@@ -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,15 +50,11 @@ pub enum GameLibsMode {
Gamemode, Gamemode,
} }
impl GameLibsMode { impl_as_str!(GameLibsMode {
pub fn as_str(self) -> &'static str { Auto => "auto",
match self { Keep => "keep",
Self::Auto => "auto", Gamemode => "gamemode",
Self::Keep => "keep", });
Self::Gamemode => "gamemode",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@@ -31,17 +66,13 @@ pub enum GamescopeScaler {
Stretch, Stretch,
} }
impl GamescopeScaler { impl_as_str!(GamescopeScaler {
pub fn as_str(self) -> &'static str { Auto => "auto",
match self { Integer => "integer",
Self::Auto => "auto", Fit => "fit",
Self::Integer => "integer", Fill => "fill",
Self::Fit => "fit", Stretch => "stretch",
Self::Fill => "fill", });
Self::Stretch => "stretch",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@@ -53,17 +84,13 @@ pub enum GamescopeFilter {
Pixel, Pixel,
} }
impl GamescopeFilter { impl_as_str!(GamescopeFilter {
pub fn as_str(self) -> &'static str { Linear => "linear",
match self { Nearest => "nearest",
Self::Linear => "linear", Fsr => "fsr",
Self::Nearest => "nearest", Nis => "nis",
Self::Fsr => "fsr", Pixel => "pixel",
Self::Nis => "nis", });
Self::Pixel => "pixel",
}
}
}
/// Output resolution value for gamescope -W/-H. /// Output resolution value for gamescope -W/-H.
/// Native = detect display resolution at launch; Pixels = explicit pixel count. /// Native = detect display resolution at launch; Pixels = explicit pixel count.
@@ -137,15 +164,11 @@ pub enum GamescopeWindowMode {
Fullscreen, Fullscreen,
} }
impl GamescopeWindowMode { impl_as_str!(GamescopeWindowMode {
pub fn as_str(self) -> &'static str { Windowed => "windowed",
match self { Borderless => "borderless",
Self::Windowed => "windowed", Fullscreen => "fullscreen",
Self::Borderless => "borderless", });
Self::Fullscreen => "fullscreen",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@@ -157,17 +180,13 @@ pub enum VkbasaltLogLevel {
None, None,
} }
impl VkbasaltLogLevel { impl_as_str!(VkbasaltLogLevel {
pub fn as_str(self) -> &'static str { Debug => "debug",
match self { Info => "info",
Self::Debug => "debug", Warning => "warning",
Self::Info => "info", Error => "error",
Self::Warning => "warning", None => "none",
Self::Error => "error", });
Self::None => "none",
}
}
}
#[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")]
@@ -177,14 +196,10 @@ pub enum HookErrors {
Fail, Fail,
} }
impl HookErrors { impl_as_str!(HookErrors {
pub fn as_str(self) -> &'static str { Warn => "warn",
match self { Fail => "fail",
Self::Warn => "warn", });
Self::Fail => "fail",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct Settings { pub struct Settings {
@@ -395,111 +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.mangohud { apply_scalar!(self, settings, mangohud);
self.mangohud = value; apply_scalar!(self, settings, gamemode);
} apply_scalar!(self, settings, steam_host_libs);
if let Some(value) = settings.gamemode { apply_scalar!(self, settings, game_libs);
self.gamemode = 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.log_file { apply_opt!(self, settings, gamescope_filter);
self.log_file = value; apply_opt!(self, settings, gamescope_sharpness);
} apply_opt!(self, settings, gamescope_window_mode);
if let Some(ref value) = settings.log_path { apply_scalar!(self, settings, gamescope_adaptive_sync);
self.log_path = Some(value.clone()); apply_scalar!(self, settings, gamescope_hdr);
} apply_scalar!(self, settings, gamescope_steam);
if let Some(value) = settings.gamescope { apply_scalar!(self, settings, gamescope_expose_wayland);
self.gamescope = value; apply_scalar!(self, settings, gamescope_mangoapp);
} apply_opt!(self, settings, fps_cap);
if let Some(value) = settings.gamescope_width { apply_scalar!(self, settings, mangohud_log);
self.gamescope_width = Some(value); apply_clone!(self, settings, mangohud_log_path);
} apply_scalar!(self, settings, vkbasalt);
if let Some(value) = settings.gamescope_height { apply_clone!(self, settings, vkbasalt_config);
self.gamescope_height = Some(value); apply_opt!(self, settings, vkbasalt_log_level);
} apply_opt!(self, settings, esync);
if let Some(value) = settings.gamescope_fps { apply_opt!(self, settings, fsync);
self.gamescope_fps = Some(value); apply_scalar!(self, settings, large_address_aware);
} apply_clone!(self, settings, pre_launch);
if let Some(value) = settings.gamescope_nested_width { apply_clone!(self, settings, post_launch);
self.gamescope_nested_width = Some(value); apply_scalar!(self, settings, hook_errors);
}
if let Some(value) = settings.gamescope_nested_height {
self.gamescope_nested_height = Some(value);
}
if let Some(value) = settings.gamescope_unfocused_fps {
self.gamescope_unfocused_fps = Some(value);
}
if let Some(value) = settings.gamescope_scaler {
self.gamescope_scaler = Some(value);
}
if let Some(value) = settings.gamescope_filter {
self.gamescope_filter = Some(value);
}
if let Some(value) = settings.gamescope_sharpness {
self.gamescope_sharpness = Some(value);
}
if let Some(value) = settings.gamescope_window_mode {
self.gamescope_window_mode = Some(value);
}
if let Some(value) = settings.gamescope_adaptive_sync {
self.gamescope_adaptive_sync = value;
}
if let Some(value) = settings.gamescope_hdr {
self.gamescope_hdr = value;
}
if let Some(value) = settings.gamescope_steam {
self.gamescope_steam = value;
}
if let Some(value) = settings.gamescope_expose_wayland {
self.gamescope_expose_wayland = value;
}
if let Some(value) = settings.gamescope_mangoapp {
self.gamescope_mangoapp = value;
}
if let Some(value) = settings.fps_cap {
self.fps_cap = Some(value);
}
if let Some(value) = settings.mangohud_log {
self.mangohud_log = value;
}
if let Some(ref value) = settings.mangohud_log_path {
self.mangohud_log_path = Some(value.clone());
}
if let Some(value) = settings.vkbasalt {
self.vkbasalt = value;
}
if let Some(ref value) = settings.vkbasalt_config {
self.vkbasalt_config = Some(value.clone());
}
if let Some(value) = settings.vkbasalt_log_level {
self.vkbasalt_log_level = Some(value);
}
if let Some(value) = settings.esync {
self.esync = Some(value);
}
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(value) = settings.hook_errors {
self.hook_errors = value;
}
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());
+26
View File
@@ -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
View File
@@ -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 {
+13 -22
View File
@@ -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();
@@ -68,18 +77,8 @@ pub fn build_env(settings: ResolvedSettings) -> BTreeMap<OsString, OsString> {
); );
} }
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"),
@@ -95,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.gamemode { 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> {
+7 -3
View File
@@ -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 {
+17 -25
View File
@@ -300,21 +300,23 @@ 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 {
for (key, value) in plan.env { cmd.env(key, value);
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}"
@@ -324,20 +326,7 @@ pub fn execute(plan: LaunchPlan) -> Result<(), AppError> {
pub fn execute_wait( pub fn execute_wait(
plan: LaunchPlan, plan: LaunchPlan,
) -> Result<(std::process::ExitStatus, std::time::Duration), AppError> { ) -> Result<(std::process::ExitStatus, std::time::Duration), AppError> {
let executable = plan let mut command = build_std_command(plan)?;
.command
.first()
.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()
@@ -488,7 +477,10 @@ 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(
settings: &ResolvedSettings,
steam_context: bool,
) -> bool {
if !settings.gamemode { if !settings.gamemode {
return false; return false;
} }
+1
View File
@@ -3,6 +3,7 @@ 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;
+1 -21
View File
@@ -33,34 +33,14 @@ fn iso_timestamp() -> String {
let m = (secs / 60) % 60; let m = (secs / 60) % 60;
let h = (secs / 3600) % 24; let h = (secs / 3600) % 24;
let days = secs / 86400; let days = secs / 86400;
let (y, mo, d) = days_to_ymd(days); 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") format!("{y:04}-{mo:02}-{d:02} {h:02}:{m:02}:{s:02} UTC")
} }
fn days_to_ymd(days: u64) -> (u32, u32, u32) {
let days = days as i64 + 719468;
let era = if days >= 0 { days } else { days - 146096 } / 146097;
let doe = (days - era * 146097) as u32;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as u32, m, d)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn converts_epoch_days_to_gregorian_dates() {
assert_eq!(days_to_ymd(0), (1970, 1, 1));
assert_eq!(days_to_ymd(19_782), (2024, 2, 29));
}
#[test] #[test]
fn appends_timestamped_lines() { fn appends_timestamped_lines() {
let temp = tempfile::tempdir().expect("temp dir"); let temp = tempfile::tempdir().expect("temp dir");
+13
View File
@@ -111,6 +111,19 @@ pub fn parse_imported_profile(content: &str) -> Result<SharedProfileFile, AppErr
} }
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);
} }