Files
STARTLINER/rust/src/lib.rs
2025-03-29 02:04:33 +00:00

284 lines
10 KiB
Rust

mod cmd;
mod model;
mod pkg;
mod pkg_store;
mod util;
mod download_handler;
mod appdata;
mod modules;
mod profiles;
use std::sync::OnceLock;
use anyhow::anyhow;
use closure::closure;
use appdata::{AppData, ToggleAction};
use model::misc::Game;
use pkg::PkgKey;
use pkg_store::Payload;
use tauri::{AppHandle, Listener, Manager, RunEvent};
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_cli::CliExt;
use tauri_plugin_updater::UpdaterExt;
use tokio::{fs, sync::Mutex, try_join};
static EXIT_REQUESTED: OnceLock<()> = OnceLock::new();
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub async fn run(_args: Vec<String>) {
simple_logger::init_with_env().expect("Unable to initialize the logger");
log::info!(
"Running from {}",
std::env::current_dir()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
);
let tauri = tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
let _ = app
.get_webview_window("main")
.expect("No main window")
.set_focus();
if args.len() == 2 {
deep_link(app.clone(), args);
}
}))
.plugin(tauri_plugin_cli::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| {
let apph = app.handle();
util::init_dirs(&apph);
let mut app_data = AppData::new(app.handle().clone());
let start_immediately;
if let Ok(matches) = app.cli().matches() {
let start_arg = matches.args.get("start").expect("Invalid argument configuration");
let game_arg = matches.args.get("game").expect("Invalid argument configuration");
let name_arg = matches.args.get("profile").expect("Invalid argument configuration");
log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg);
if start_arg.occurrences > 0 {
start_immediately = true;
app_data.state.remain_open = false;
} else {
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into()))
.title("STARTLINER")
.inner_size(720f64, 480f64)
.min_inner_size(720f64, 480f64)
.build()?;
start_immediately = false;
}
if game_arg.occurrences == 1 && name_arg.occurrences == 1 {
let game = game_arg.value.as_str().unwrap();
let name = name_arg.value.as_str().unwrap();
app_data.switch_profile(
Game::from_str(game).ok_or_else(|| anyhow!("Invalid game"))?,
name.to_owned()
)?;
}
} else {
return Err(anyhow!("Invalid command line arguments").into());
}
app.manage(Mutex::new(app_data));
app.deep_link().register_all()?;
log::debug!("\n{:?}\n{:?}\n{:?}", util::config_dir(), util::pkg_dir(), util::cache_dir());
tauri::async_runtime::spawn(async {
let e = try_join!(
fs::create_dir_all(util::config_dir()),
fs::create_dir_all(util::pkg_dir()),
fs::create_dir_all(util::cache_dir())
);
if let Err(e) = e {
log::error!("Unable to create base directories: {}", e);
std::process::exit(1);
}
});
app.listen("download-end", closure!(clone apph, |ev| {
log::debug!("download-end triggered: {}", ev.payload());
let raw = ev.payload();
let key = PkgKey(raw[1..raw.len()-1].to_owned());
let apph = apph.clone();
tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await;
log::debug!("download-end install {:?}", appd.pkgs.install_package(&key, true, false).await);
});
}));
app.listen("launch-end", closure!(clone apph, |_| {
log::debug!("launch-end triggered");
let apph = apph.clone();
tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>();
let appd = mutex.lock().await;
if !appd.state.remain_open {
apph.exit(0);
}
});
}));
app.listen("install-end-prelude", closure!(clone apph, |ev| {
log::debug!("install-end-prelude triggered: {}", ev.payload());
let payload = serde_json::from_str::<Payload>(ev.payload());
let apph = apph.clone();
if let Ok(payload) = payload {
tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await;
log::debug!(
"install-end-prelude toggle {:?}",
appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf)
);
use tauri::Emitter;
log::debug!("install-end {:?}", apph.emit("install-end", payload));
});
} else {
log::error!("install-end-prelude: invalid payload: {}", ev.payload());
}
}));
if start_immediately == true {
let apph = apph.clone();
tauri::async_runtime::spawn(async move {
let mtx = apph.state::<Mutex<AppData>>();
{
let mut appd = mtx.lock().await;
if let Err(e) = appd.pkgs.reload_all().await {
log::error!("Unable to reload packages: {}", e);
apph.exit(1);
}
}
if let Err(e) = cmd::startline(apph.clone(), false).await {
log::error!("Unable to launch: {}", e);
apph.exit(1);
}
});
} else {
let apph = apph.clone();
tauri::async_runtime::spawn(async move {
update(apph).await.unwrap();
});
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
cmd::start_check,
cmd::startline,
cmd::kill,
cmd::get_package,
cmd::get_all_packages,
cmd::get_game_packages,
cmd::reload_all_packages,
cmd::fetch_listings,
cmd::install_package,
cmd::delete_package,
cmd::toggle_package,
cmd::list_profiles,
cmd::init_profile,
cmd::load_profile,
cmd::rename_profile,
cmd::duplicate_profile,
cmd::delete_profile,
cmd::get_current_profile,
cmd::sync_current_profile,
cmd::save_current_profile,
cmd::list_displays,
cmd::list_platform_capabilities,
cmd::list_directories,
])
.build(tauri::generate_context!())
.expect("error while building tauri application");
tauri.run(move |app, event| {
match event {
RunEvent::ExitRequested { api, code, .. } => {
log::debug!("exit request {:?} {:?}", api, code);
if EXIT_REQUESTED.get().is_none() {
_= EXIT_REQUESTED.set(());
api.prevent_exit();
let app = app.clone();
tauri::async_runtime::spawn(async move {
let mutex = app.state::<Mutex<AppData>>();
let appd = mutex.lock().await;
if let Some(p) = &appd.profile {
log::debug!("save: {:?}", p.save());
app.exit(0);
}
});
}
},
RunEvent::Exit => {
log::info!("exit");
}
_ => {}
}
});
}
fn deep_link(app: AppHandle, args: Vec<String>) {
let url = &args[1];
let proto = "rainycolor://";
if &url[..proto.len()] == proto {
log::info!("Deep link: {}", url);
let regex = regex::Regex::new(
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
).expect("Invalid regex");
if let Some(caps) = regex.captures(url) {
if caps.len() == 3 {
let app = app.clone();
let key = PkgKey(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()));
tauri::async_runtime::spawn(async move {
let mutex = app.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await;
if appd.pkgs.is_offline() {
log::warn!("Deep link installation failed: offline");
} else if let Err(e) = appd.pkgs.install_package(&key, true, true).await {
log::warn!("Deep link installation failed: {}", e.to_string());
}
});
}
}
}
}
async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
if let Some(update) = app.updater()?.check().await? {
let mut downloaded = 0;
update
.download_and_install(
|chunk_length, content_length| {
downloaded += chunk_length;
log::debug!("downloaded {downloaded} from {content_length:?}");
},
|| {
log::info!("download finished");
},
)
.await?;
log::info!("update installed");
app.restart();
}
Ok(())
}