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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
+13
-22
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user