Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 12 additions & 13 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 127 additions & 0 deletions apps/app/src/api/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
Expand Down Expand Up @@ -162,6 +165,130 @@ pub async fn handle_command(command: String) -> Result<()> {
Ok(theseus::handler::parse_and_emit_command(&command).await?)
}

/// Parse a Vec<OsString> (as provided by std::env::args_os()) or Vec<String> (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<S: AsRef<std::ffi::OsStr>>(
args: Vec<S>,
) -> Option<String> {
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::<Vec<_>>()
.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<Url> {
#[cfg(any(windows, target_os = "android"))]
Expand Down
29 changes: 28 additions & 1 deletion apps/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion apps/app/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"active": true,
"category": "Game",
"copyright": "",
"targets": "all",
"targets": ["nsis"],
"externalBin": [],
"icon": [
"icons/128x128.png",
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/AGENTS.md
2 changes: 1 addition & 1 deletion packages/api-client/AGENTS.md
2 changes: 1 addition & 1 deletion packages/app-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/AGENTS.md