diff --git a/.idea/gradle.xml b/.idea/gradle.xml index bcd9480b81..a2615552ff 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,17 +1,18 @@ - - - - - + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index ecea560ce5..33af8a01da 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -1,15 +1,14 @@ - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/app/src/api/utils.rs b/apps/app/src/api/utils.rs index a2b013ae3c..64dbfb0872 100644 --- a/apps/app/src/api/utils.rs +++ b/apps/app/src/api/utils.rs @@ -9,7 +9,10 @@ use theseus::{ use crate::api::{Result, TheseusSerializableError}; use dashmap::DashMap; use std::path::{Path, PathBuf}; +use theseus::emit_warning; use theseus::prelude::canonicalize; +use theseus::profile::{self, QuickPlayType}; +use tracing; use url::Url; pub fn init() -> tauri::plugin::TauriPlugin { @@ -162,6 +165,130 @@ pub async fn handle_command(command: String) -> Result<()> { Ok(theseus::handler::parse_and_emit_command(&command).await?) } +/// Parse a Vec (as provided by std::env::args_os()) or Vec (as provided by tauri single-instance args) +/// and return the value for the first `--launch-profile` occurrence, if any. +pub fn parse_launch_profile_from_args>( + args: Vec, +) -> Option { + let mut iter = args.into_iter(); + // Skip executable name + let _ = iter.next(); + + while let Some(arg) = iter.next() { + let s = arg.as_ref().to_string_lossy(); + if let Some(rest) = s.strip_prefix("--launch-profile=") { + let val = rest.to_string(); + if !val.trim().is_empty() { + return Some(val); + } + } else if s == "--launch-profile" { + if let Some(next) = iter.next() { + let val = next.as_ref().to_string_lossy().to_string(); + if !val.trim().is_empty() { + return Some(val); + } + } else { + // flag present but no value + return None; + } + } + } + + None +} + +/// Handle launching a profile by display name. Waits for state/profile readiness via underlying +/// profile APIs and then re-uses the existing `profile::run` pipeline. +pub async fn handle_launch_profile(profile_name: String) -> Result<()> { + tracing::info!( + "Requested auto-launch profile from CLI: '{}'", + profile_name + ); + + let name = profile_name.trim(); + if name.is_empty() { + let _ = emit_warning("Empty profile name provided to --launch-profile") + .await; + tracing::warn!("Empty profile name provided to --launch-profile"); + return Ok(()); + } + + // Retrieve profiles (this will await state readiness internally) + let profiles = match profile::list().await { + Ok(p) => p, + Err(e) => { + let msg = format!("Failed to read profiles for auto-launch: {e}"); + let _ = emit_warning(&msg).await; + tracing::error!("{msg}"); + return Ok(()); + } + }; + + // Matching strategy: exact (case-sensitive), exact (case-insensitive), contains (case-insensitive) + let mut matches: Vec<_> = + profiles.iter().filter(|p| p.name == name).collect(); + + if matches.is_empty() { + let low = name.to_lowercase(); + matches = profiles + .iter() + .filter(|p| p.name.to_lowercase() == low) + .collect(); + } + + if matches.is_empty() { + let low = name.to_lowercase(); + matches = profiles + .iter() + .filter(|p| p.name.to_lowercase().contains(&low)) + .collect(); + } + + if matches.is_empty() { + let msg = format!("Profile '{name}' not found"); + let _ = emit_warning(&msg).await; + tracing::warn!("{msg}"); + return Ok(()); + } + + if matches.len() > 1 { + // Ambiguous + let candidates = matches + .iter() + .map(|p| format!("{} ({})", p.name, p.path)) + .collect::>() + .join(", "); + let msg = format!( + "Multiple profiles match '{name}'. Candidates: {candidates}" + ); + let _ = emit_warning(&msg).await; + tracing::warn!("{msg}"); + return Ok(()); + } + + let profile = matches.into_iter().next().unwrap(); + + tracing::info!( + "Auto-launch: launching profile '{}' (path='{}')", + profile.name, + profile.path + ); + + match profile::run(&profile.path, QuickPlayType::None).await { + Ok(_proc) => { + tracing::info!("Started launch for profile '{}'", profile.name) + } + Err(e) => { + let msg = + format!("Failed to launch profile '{}' : {e}", profile.name); + let _ = emit_warning(&msg).await; + tracing::error!("{msg}"); + } + } + + Ok(()) +} + // Remove when (and if) https://github.com/tauri-apps/tauri/issues/12022 is implemented pub(crate) fn tauri_convert_file_src(path: &Path) -> Result { #[cfg(any(windows, target_os = "android"))] diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index c77a1ac730..9185f3822a 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -40,6 +40,23 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { app.fs_scope() .allow_directory(state.directories.profiles_dir(), true)?; + // Check for CLI --launch-profile when the app finishes initializing state. + // We spawn the handler so this command does not block the initialize_state caller. + if let Some(profile_name) = + crate::api::utils::parse_launch_profile_from_args( + std::env::args_os().collect(), + ) + { + let name = profile_name; + tracing::info!("Detected --launch-profile on startup: {}", name); + tauri::async_runtime::spawn(async move { + if let Err(e) = crate::api::utils::handle_launch_profile(name).await + { + tracing::error!("Auto-launch profile failed: {:?}", e); + } + }); + } + Ok(()) } @@ -151,7 +168,17 @@ fn main() { builder = builder .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { - if let Some(payload) = args.get(1) { + // First, check if a second-instance invoked with --launch-profile + let args_vec = args.clone(); + if let Some(profile_name) = api::utils::parse_launch_profile_from_args(args_vec) { + tracing::info!("Received single-instance --launch-profile request: {}", profile_name); + let name = profile_name; + tauri::async_runtime::spawn(async move { + if let Err(e) = api::utils::handle_launch_profile(name).await { + tracing::error!("Auto-launch profile (single-instance) failed: {:?}", e); + } + }); + } else if let Some(payload) = args.get(1) { tracing::info!("Handling deep link from arg {payload}"); let payload = payload.clone(); tauri::async_runtime::spawn(api::utils::handle_command( diff --git a/apps/app/tauri.conf.json b/apps/app/tauri.conf.json index c1b9d08003..2633a33ccf 100644 --- a/apps/app/tauri.conf.json +++ b/apps/app/tauri.conf.json @@ -10,7 +10,7 @@ "active": true, "category": "Game", "copyright": "", - "targets": "all", + "targets": ["nsis"], "externalBin": [], "icon": [ "icons/128x128.png", diff --git a/apps/frontend/AGENTS.md b/apps/frontend/AGENTS.md index 681311eb9c..ceb2b988dc 120000 --- a/apps/frontend/AGENTS.md +++ b/apps/frontend/AGENTS.md @@ -1 +1 @@ -CLAUDE.md \ No newline at end of file +CLAUDE.md diff --git a/packages/api-client/AGENTS.md b/packages/api-client/AGENTS.md index 681311eb9c..ceb2b988dc 120000 --- a/packages/api-client/AGENTS.md +++ b/packages/api-client/AGENTS.md @@ -1 +1 @@ -CLAUDE.md \ No newline at end of file +CLAUDE.md diff --git a/packages/app-lib/src/lib.rs b/packages/app-lib/src/lib.rs index 1bb282bc2c..509807b024 100644 --- a/packages/app-lib/src/lib.rs +++ b/packages/app-lib/src/lib.rs @@ -21,7 +21,7 @@ pub use api::*; pub use error::*; pub use event::{ EventState, LoadingBar, LoadingBarType, emit::emit_loading, - emit::init_loading, + emit::emit_warning, emit::init_loading, }; pub use logger::start_logger; pub use state::State; diff --git a/packages/ui/AGENTS.md b/packages/ui/AGENTS.md index 681311eb9c..ceb2b988dc 120000 --- a/packages/ui/AGENTS.md +++ b/packages/ui/AGENTS.md @@ -1 +1 @@ -CLAUDE.md \ No newline at end of file +CLAUDE.md