use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager}; use tokio::process::Command; use std::{path::{Path, PathBuf}, sync::OnceLock}; use crate::model::misc::Game; #[cfg(not(target_os = "windows"))] static NAME: &str = "startliner"; #[cfg(target_os = "windows")] static NAME: &str = "STARTLINER"; #[derive(Clone, Serialize, Deserialize)] pub struct Dirs { config_dir: PathBuf, data_dir: PathBuf, cache_dir: PathBuf, } static DIRS: OnceLock = OnceLock::new(); pub fn init_dirs(apph: &AppHandle) { DIRS.get_or_init(|| { if cfg!(windows) { Dirs { config_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME), data_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME).join("data"), cache_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME).join("cache"), } } else { Dirs { config_dir: apph.path().config_dir().expect("Unable to set project directories").join(NAME), data_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME), cache_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME), } } }); } pub fn all_dirs() -> &'static Dirs { &DIRS.get().expect("Directories uninitialized") } pub fn config_dir() -> &'static Path { &DIRS.get().expect("Directories uninitialized").config_dir } pub fn profile_config_dir(game: Game, name: &str) -> PathBuf { config_dir().join(format!("profile-{}-{}", game, name)) } pub fn data_dir() -> &'static Path { &DIRS.get().expect("Directories uninitialized").data_dir } pub fn cache_dir() -> &'static Path { &DIRS.get().expect("Directories uninitialized").cache_dir } pub fn pkg_dir() -> PathBuf { data_dir().join("pkg") } pub fn pkg_dir_of(namespace: &str, name: &str) -> PathBuf { pkg_dir().join(format!("{}-{}", namespace, name)) } pub fn copy_directory(src: impl AsRef, dst: impl AsRef, recursive: bool) -> std::io::Result<()> { std::fs::create_dir_all(dst.as_ref()).unwrap(); for entry in std::fs::read_dir(src.as_ref())? { let entry = entry?; let meta = entry.metadata()?; if meta.is_dir() { if recursive == true { copy_directory(&entry.path(), &dst.as_ref().join(entry.file_name()), true)?; } else { log::warn!("Skipping directory {:?}", meta); } } else { std::fs::copy(&entry.path(), &dst.as_ref().join(entry.file_name()))?; } } Ok(()) } #[cfg(target_os = "linux")] pub async fn symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { tokio::fs::symlink(src, dst).await } #[cfg(target_os = "windows")] pub async fn symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { //std::os::windows::fs::junction_point(src, dst) // is unstable junction::create(src, dst) } #[cfg(target_os = "windows")] pub static CREATE_NO_WINDOW: u32 = 0x08000000; #[cfg(target_os = "windows")] pub async fn pkill(process_name: &str) { _ = Command::new("taskkill.exe").arg("/f").arg("/im").arg(process_name) .creation_flags(CREATE_NO_WINDOW).output().await; } #[cfg(target_os = "linux")] pub async fn pkill(process_name: &str) { _ = Command::new("pkill").arg(process_name) .output().await; } pub fn clean_up_opts(dir: impl AsRef) -> Result<()> { log::debug!("begin clean_up_opts"); if dir.as_ref().is_dir() { for entry in std::fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); log::debug!("{:?}", path); if path.is_symlink() { #[cfg(target_os = "windows")] std::fs::remove_dir(path)?; #[cfg(not(target_os = "windows"))] std::fs::remove_file(path)?; } else { log::error!("Not a symlink: {:?}", path); } } } log::debug!("end clean_up_opts"); Ok(()) } pub trait PathStr { fn stringify(&self) -> Result; } fn path_to_str(p: impl AsRef) -> Result { Ok(p.as_ref().to_str().ok_or_else(|| anyhow!("Invalid path: {:?}", p.as_ref()))?.to_owned()) } impl PathStr for Path { fn stringify(&self) -> Result { path_to_str(self) } } impl PathStr for PathBuf { fn stringify(&self) -> Result { path_to_str(&self) } } #[allow(dead_code)] pub fn bool_to_01(val: bool) -> &'static str { return if val { "1" } else { "0" } } // rm -r with checks pub async fn remove_dir_all(path: impl AsRef) -> Result<()> { let canon = path.as_ref().canonicalize()?; if canon.to_string_lossy().len() < 10 { return Err(anyhow!("invalid remove_dir_all target: too short")); } if canon.starts_with(data_dir().canonicalize()?) || canon.starts_with(config_dir().canonicalize()?) || canon.starts_with(cache_dir().canonicalize()?) { tokio::fs::remove_dir_all(path).await .map_err(|e| anyhow!("invalid remove_dir_all target: {:?}", e))?; Ok(()) } else { Err(anyhow!("invalid remove_dir_all target: not in a data directory")) } } #[cfg(target_os = "windows")] pub fn create_shortcut( apph: AppHandle, meta: &crate::profiles::ProfileMeta ) -> Result<()> { use winsafe::{co, prelude::{ole_IPersistFile, ole_IUnknown, shell_IShellLink}, CoCreateInstance, CoInitializeEx, IPersistFile}; let _com_guard = CoInitializeEx( co::COINIT::APARTMENTTHREADED | co::COINIT::DISABLE_OLE1DDE, )?; let obj = CoCreateInstance::( &co::CLSID::ShellLink, None, co::CLSCTX::INPROC_SERVER, )?; let target_dir = apph.path().cache_dir()?.join(NAME); let target_path = target_dir.join("startliner.exe"); let lnk_path = apph.path().desktop_dir()?.join(format!("{} {}.lnk", &meta.game.print(), &meta.name)); obj.SetPath(target_path.to_str().ok_or_else(|| anyhow!("Illegal target path"))?)?; obj.SetDescription(&format!("{} – {} (STARTLINER)", &meta.game.print(), &meta.name))?; obj.SetArguments(&format!("--start --game {} --profile {}", &meta.game, &meta.name))?; obj.SetIconLocation( target_dir.join(format!("icon-{}.ico", &meta.game)).to_str().ok_or_else(|| anyhow!("Illegal icon path"))?, 0 )?; match meta.game { Game::Ongeki => std::fs::write(target_dir.join("icon-ongeki.ico"), include_bytes!("../../res/icon-ongeki.ico")), Game::Chunithm => std::fs::write(target_dir.join("icon-chunithm.ico"), include_bytes!("../../res/icon-chunithm.ico")) }?; let file = obj.QueryInterface::()?; file.Save(Some(lnk_path.to_str().ok_or_else(|| anyhow!("Illegal shortcut path"))?), true)?; Ok(()) }