217 lines
6.9 KiB
Rust
217 lines
6.9 KiB
Rust
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<Dirs> = 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<Path>, dst: impl AsRef<Path>, 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<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
||
tokio::fs::symlink(src, dst).await
|
||
}
|
||
|
||
#[cfg(target_os = "windows")]
|
||
pub async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> 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<Path>) -> 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<String>;
|
||
}
|
||
|
||
fn path_to_str(p: impl AsRef<Path>) -> Result<String> {
|
||
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<String> {
|
||
path_to_str(self)
|
||
}
|
||
}
|
||
|
||
impl PathStr for PathBuf {
|
||
fn stringify(&self) -> Result<String> {
|
||
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<Path>) -> 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::<winsafe::IShellLink>(
|
||
&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::<IPersistFile>()?;
|
||
|
||
file.Save(Some(lnk_path.to_str().ok_or_else(|| anyhow!("Illegal shortcut path"))?), true)?;
|
||
|
||
Ok(())
|
||
} |