diff --git a/src/cli.rs b/src/cli.rs index d71f05f..40cda4f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1421,16 +1421,10 @@ fn launch_command( let resolved = profile::resolve(config, &executable)?; let verbose = resolved.settings.verbose; let settings = resolved.settings.clone(); - let plan = launch::build_plan(command, settings.clone())?; - let log_path = settings.log_file.then(|| { - settings - .log_path - .as_deref() - .map(PathBuf::from) - .unwrap_or_else(|| crate::log::default_log_path(&paths.state_dir)) - }); + // Dry-run: build the plan (fail-fast) and render it without launching. if dry_run { + let plan = launch::build_plan(command, settings.clone())?; println!( "{}", launch::render_plan(&plan, &resolved.profile_name, verbose) @@ -1459,6 +1453,16 @@ fn launch_command( return Ok(()); } + // Real launch: capture plan errors so the post-hook can always run. + let plan_result = launch::build_plan(command, settings.clone()); + let log_path = settings.log_file.then(|| { + settings + .log_path + .as_deref() + .map(PathBuf::from) + .unwrap_or_else(|| crate::log::default_log_path(&paths.state_dir)) + }); + // Log the launch header. if let Some(log_path) = &log_path { crate::log::append( @@ -1467,13 +1471,15 @@ fn launch_command( "--- launch ---".to_string(), format!("executable: {}", executable.basename), format!("profile: {}", resolved.profile_name), - format!("command: {}", format_command(&plan.command)), + format!("command: {}", format_command(command)), ], ); } - // Run pre-launch hook. - if let Some(pre_cmd) = settings.pre_launch.as_deref() { + // Run pre-launch hook (only when the plan is valid; skip if binary not found). + if plan_result.is_ok() + && let Some(pre_cmd) = settings.pre_launch.as_deref() + { let hook_result = run_hook(pre_cmd); let log_label = match &hook_result { Ok(status) => exit_status_label(*status), @@ -1507,16 +1513,16 @@ fn launch_command( } } - // Record the launch in state (Steam context only). - if env::is_steam_context() { + // Record the launch in state (Steam context only, when plan is valid). + if plan_result.is_ok() && env::is_steam_context() { detect::record_launch(state, &executable, &resolved.profile_name); config::save_state(paths, state)?; } if let Some(post_cmd) = settings.post_launch.clone() { - // Spawn the game and wait. Capture both success and failure so the - // post-hook can always run once we've passed the pre-hook check. - let game_result = launch::execute_wait(plan); + // Chain plan building with game execution so post-hook runs even when + // the binary doesn't exist or otherwise fails to spawn. + let game_result = plan_result.and_then(launch::execute_wait); let (elapsed_secs, game_exit_label) = match &game_result { Ok((exit_status, elapsed)) => { @@ -1579,6 +1585,7 @@ fn launch_command( } } } else { + let plan = plan_result?; launch::execute(plan) } }