Folder names (#318)

This commit is contained in:
Wyatt Verchere
2023-07-21 20:16:07 -07:00
committed by GitHub
parent 4941260805
commit 3fa33dc241
42 changed files with 1129 additions and 535 deletions

View File

@@ -112,7 +112,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
) )
.await?; .await?;
let path = state.directories.java_versions_dir(); let path = state.directories.java_versions_dir().await;
if path.exists() { if path.exists() {
io::remove_dir_all(&path).await?; io::remove_dir_all(&path).await?;

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
util::io::{self, IOError}, util::io::{self, IOError},
State, {state::ProfilePathId, State},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -11,7 +11,7 @@ pub struct Logs {
} }
impl Logs { impl Logs {
async fn build( async fn build(
profile_uuid: uuid::Uuid, profile_subpath: &ProfilePathId,
datetime_string: String, datetime_string: String,
clear_contents: Option<bool>, clear_contents: Option<bool>,
) -> crate::Result<Self> { ) -> crate::Result<Self> {
@@ -20,7 +20,7 @@ impl Logs {
None None
} else { } else {
Some( Some(
get_output_by_datetime(profile_uuid, &datetime_string) get_output_by_datetime(profile_subpath, &datetime_string)
.await?, .await?,
) )
}, },
@@ -35,7 +35,18 @@ pub async fn get_logs(
clear_contents: Option<bool>, clear_contents: Option<bool>,
) -> crate::Result<Vec<Logs>> { ) -> crate::Result<Vec<Logs>> {
let state = State::get().await?; let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(profile_uuid); let profile_path = if let Some(p) =
crate::profile::get_by_uuid(profile_uuid, None).await?
{
p.profile_id()
} else {
return Err(crate::ErrorKind::UnmanagedProfileError(
profile_uuid.to_string(),
)
.into());
};
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
let mut logs = Vec::new(); let mut logs = Vec::new();
if logs_folder.exists() { if logs_folder.exists() {
for entry in std::fs::read_dir(&logs_folder) for entry in std::fs::read_dir(&logs_folder)
@@ -48,7 +59,7 @@ pub async fn get_logs(
if let Some(datetime_string) = path.file_name() { if let Some(datetime_string) = path.file_name() {
logs.push( logs.push(
Logs::build( Logs::build(
profile_uuid, &profile_path,
datetime_string.to_string_lossy().to_string(), datetime_string.to_string_lossy().to_string(),
clear_contents, clear_contents,
) )
@@ -69,9 +80,19 @@ pub async fn get_logs_by_datetime(
profile_uuid: uuid::Uuid, profile_uuid: uuid::Uuid,
datetime_string: String, datetime_string: String,
) -> crate::Result<Logs> { ) -> crate::Result<Logs> {
let profile_path = if let Some(p) =
crate::profile::get_by_uuid(profile_uuid, None).await?
{
p.profile_id()
} else {
return Err(crate::ErrorKind::UnmanagedProfileError(
profile_uuid.to_string(),
)
.into());
};
Ok(Logs { Ok(Logs {
output: Some( output: Some(
get_output_by_datetime(profile_uuid, &datetime_string).await?, get_output_by_datetime(&profile_path, &datetime_string).await?,
), ),
datetime_string, datetime_string,
}) })
@@ -79,19 +100,31 @@ pub async fn get_logs_by_datetime(
#[tracing::instrument] #[tracing::instrument]
pub async fn get_output_by_datetime( pub async fn get_output_by_datetime(
profile_uuid: uuid::Uuid, profile_subpath: &ProfilePathId,
datetime_string: &str, datetime_string: &str,
) -> crate::Result<String> { ) -> crate::Result<String> {
let state = State::get().await?; let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(profile_uuid); let logs_folder =
state.directories.profile_logs_dir(profile_subpath).await?;
let path = logs_folder.join(datetime_string).join("stdout.log"); let path = logs_folder.join(datetime_string).join("stdout.log");
Ok(io::read_to_string(&path).await?) Ok(io::read_to_string(&path).await?)
} }
#[tracing::instrument] #[tracing::instrument]
pub async fn delete_logs(profile_uuid: uuid::Uuid) -> crate::Result<()> { pub async fn delete_logs(profile_uuid: uuid::Uuid) -> crate::Result<()> {
let profile_path = if let Some(p) =
crate::profile::get_by_uuid(profile_uuid, None).await?
{
p.profile_id()
} else {
return Err(crate::ErrorKind::UnmanagedProfileError(
profile_uuid.to_string(),
)
.into());
};
let state = State::get().await?; let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(profile_uuid); let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
for entry in std::fs::read_dir(&logs_folder) for entry in std::fs::read_dir(&logs_folder)
.map_err(|e| IOError::with_path(e, &logs_folder))? .map_err(|e| IOError::with_path(e, &logs_folder))?
{ {
@@ -109,8 +142,19 @@ pub async fn delete_logs_by_datetime(
profile_uuid: uuid::Uuid, profile_uuid: uuid::Uuid,
datetime_string: &str, datetime_string: &str,
) -> crate::Result<()> { ) -> crate::Result<()> {
let profile_path = if let Some(p) =
crate::profile::get_by_uuid(profile_uuid, None).await?
{
p.profile_id()
} else {
return Err(crate::ErrorKind::UnmanagedProfileError(
profile_uuid.to_string(),
)
.into());
};
let state = State::get().await?; let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(profile_uuid); let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
let path = logs_folder.join(datetime_string); let path = logs_folder.join(datetime_string);
io::remove_dir_all(&path).await?; io::remove_dir_all(&path).await?;
Ok(()) Ok(())

View File

@@ -29,6 +29,7 @@ pub mod prelude {
profile::{self, Profile}, profile::{self, Profile},
profile_create, settings, profile_create, settings,
state::JavaGlobals, state::JavaGlobals,
state::{ProfilePathId, ProjectPathId},
util::{ util::{
io::{canonicalize, IOError}, io::{canonicalize, IOError},
jre::JavaVersion, jre::JavaVersion,

View File

@@ -4,7 +4,7 @@ use crate::event::emit::{
}; };
use crate::event::LoadingBarType; use crate::event::LoadingBarType;
use crate::pack::install_from::{EnvType, PackFile, PackFileHash}; use crate::pack::install_from::{EnvType, PackFile, PackFileHash};
use crate::state::{LinkedData, ProfileInstallStage, SideType}; use crate::state::{LinkedData, ProfileInstallStage, ProfilePathId, SideType};
use crate::util::fetch::{fetch_mirrors, write}; use crate::util::fetch::{fetch_mirrors, write};
use crate::State; use crate::State;
use async_zip::tokio::read::seek::ZipFileReader; use async_zip::tokio::read::seek::ZipFileReader;
@@ -20,8 +20,8 @@ use super::install_from::{
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn install_pack( pub async fn install_pack(
location: CreatePackLocation, location: CreatePackLocation,
profile: PathBuf, profile_path: ProfilePathId,
) -> crate::Result<PathBuf> { ) -> crate::Result<ProfilePathId> {
// Get file from description // Get file from description
let description: CreatePackDescription = match location { let description: CreatePackDescription = match location {
CreatePackLocation::FromVersionId { CreatePackLocation::FromVersionId {
@@ -31,12 +31,16 @@ pub async fn install_pack(
icon_url, icon_url,
} => { } => {
generate_pack_from_version_id( generate_pack_from_version_id(
project_id, version_id, title, icon_url, profile, project_id,
version_id,
title,
icon_url,
profile_path,
) )
.await? .await?
} }
CreatePackLocation::FromFile { path } => { CreatePackLocation::FromFile { path } => {
generate_pack_from_file(path, profile).await? generate_pack_from_file(path, profile_path).await?
} }
}; };
@@ -46,7 +50,7 @@ pub async fn install_pack(
let project_id = description.project_id; let project_id = description.project_id;
let version_id = description.version_id; let version_id = description.version_id;
let existing_loading_bar = description.existing_loading_bar; let existing_loading_bar = description.existing_loading_bar;
let profile = description.profile; let profile_path = description.profile_path;
let state = &State::get().await?; let state = &State::get().await?;
@@ -125,7 +129,7 @@ pub async fn install_pack(
loader_version.cloned(), loader_version.cloned(),
) )
.await?; .await?;
crate::api::profile::edit(&profile, |prof| { crate::api::profile::edit(&profile_path, |prof| {
prof.metadata.name = prof.metadata.name =
override_title.clone().unwrap_or_else(|| pack.name.clone()); override_title.clone().unwrap_or_else(|| pack.name.clone());
prof.install_stage = ProfileInstallStage::PackInstalling; prof.install_stage = ProfileInstallStage::PackInstalling;
@@ -142,12 +146,15 @@ pub async fn install_pack(
}) })
.await?; .await?;
let profile = profile.clone(); let profile_path = profile_path.clone();
let result = async { let result = async {
let loading_bar = init_or_edit_loading( let loading_bar = init_or_edit_loading(
existing_loading_bar, existing_loading_bar,
LoadingBarType::PackDownload { LoadingBarType::PackDownload {
profile_path: profile.clone(), profile_path: profile_path
.get_full_path()
.await?
.clone(),
pack_name: pack.name.clone(), pack_name: pack.name.clone(),
icon, icon,
pack_id: project_id, pack_id: project_id,
@@ -169,7 +176,7 @@ pub async fn install_pack(
num_files, num_files,
None, None,
|project| { |project| {
let profile = profile.clone(); let profile_path = profile_path.clone();
async move { async move {
//TODO: Future update: prompt user for optional files in a modpack //TODO: Future update: prompt user for optional files in a modpack
if let Some(env) = project.env { if let Some(env) = project.env {
@@ -203,7 +210,10 @@ pub async fn install_pack(
match path { match path {
Component::CurDir Component::CurDir
| Component::Normal(_) => { | Component::Normal(_) => {
let path = profile.join(project.path); let path = profile_path
.get_full_path()
.await?
.join(project.path);
write( write(
&path, &path,
&file, &file,
@@ -265,7 +275,10 @@ pub async fn install_pack(
if new_path.file_name().is_some() { if new_path.file_name().is_some() {
write( write(
&profile.join(new_path), &profile_path
.get_full_path()
.await?
.join(new_path),
&content, &content,
&state.io_semaphore, &state.io_semaphore,
) )
@@ -285,7 +298,7 @@ pub async fn install_pack(
} }
if let Some(profile_val) = if let Some(profile_val) =
crate::api::profile::get(&profile, None).await? crate::api::profile::get(&profile_path, None).await?
{ {
crate::launcher::install_minecraft( crate::launcher::install_minecraft(
&profile_val, &profile_val,
@@ -296,14 +309,14 @@ pub async fn install_pack(
State::sync().await?; State::sync().await?;
} }
Ok::<PathBuf, crate::Error>(profile.clone()) Ok::<ProfilePathId, crate::Error>(profile_path.clone())
} }
.await; .await;
match result { match result {
Ok(profile) => Ok(profile), Ok(profile) => Ok(profile),
Err(err) => { Err(err) => {
let _ = crate::api::profile::remove(&profile).await; let _ = crate::api::profile::remove(&profile_path).await;
Err(err) Err(err)
} }
@@ -319,7 +332,7 @@ pub async fn install_pack(
match result { match result {
Ok(profile) => Ok(profile), Ok(profile) => Ok(profile),
Err(err) => { Err(err) => {
let _ = crate::api::profile::remove(&profile).await; let _ = crate::api::profile::remove(&profile_path).await;
Err(err) Err(err)
} }

View File

@@ -2,7 +2,9 @@ use crate::config::MODRINTH_API_URL;
use crate::data::ModLoader; use crate::data::ModLoader;
use crate::event::emit::{emit_loading, init_loading}; use crate::event::emit::{emit_loading, init_loading};
use crate::event::{LoadingBarId, LoadingBarType}; use crate::event::{LoadingBarId, LoadingBarType};
use crate::state::{LinkedData, ModrinthProject, ModrinthVersion, SideType}; use crate::state::{
LinkedData, ModrinthProject, ModrinthVersion, ProfilePathId, SideType,
};
use crate::util::fetch::{ use crate::util::fetch::{
fetch, fetch_advanced, fetch_json, write_cached_icon, fetch, fetch_advanced, fetch_json, write_cached_icon,
}; };
@@ -71,7 +73,7 @@ pub enum PackDependency {
Minecraft, Minecraft,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", tag = "type")] #[serde(rename_all = "camelCase", tag = "type")]
pub enum CreatePackLocation { pub enum CreatePackLocation {
FromVersionId { FromVersionId {
@@ -98,6 +100,7 @@ pub struct CreatePackProfile {
pub skip_install_profile: Option<bool>, pub skip_install_profile: Option<bool>,
} }
#[derive(Debug)]
pub struct CreatePackDescription { pub struct CreatePackDescription {
pub file: bytes::Bytes, pub file: bytes::Bytes,
pub icon: Option<PathBuf>, pub icon: Option<PathBuf>,
@@ -105,7 +108,7 @@ pub struct CreatePackDescription {
pub project_id: Option<String>, pub project_id: Option<String>,
pub version_id: Option<String>, pub version_id: Option<String>,
pub existing_loading_bar: Option<LoadingBarId>, pub existing_loading_bar: Option<LoadingBarId>,
pub profile: PathBuf, pub profile_path: ProfilePathId,
} }
pub fn get_profile_from_pack( pub fn get_profile_from_pack(
@@ -158,13 +161,13 @@ pub async fn generate_pack_from_version_id(
version_id: String, version_id: String,
title: String, title: String,
icon_url: Option<String>, icon_url: Option<String>,
profile: PathBuf, profile_path: ProfilePathId,
) -> crate::Result<CreatePackDescription> { ) -> crate::Result<CreatePackDescription> {
let state = State::get().await?; let state = State::get().await?;
let loading_bar = init_loading( let loading_bar = init_loading(
LoadingBarType::PackFileDownload { LoadingBarType::PackFileDownload {
profile_path: profile.clone(), profile_path: profile_path.get_full_path().await?,
pack_name: title, pack_name: title,
icon: icon_url, icon: icon_url,
pack_version: version_id.clone(), pack_version: version_id.clone(),
@@ -253,7 +256,7 @@ pub async fn generate_pack_from_version_id(
project_id: Some(project_id), project_id: Some(project_id),
version_id: Some(version_id), version_id: Some(version_id),
existing_loading_bar: Some(loading_bar), existing_loading_bar: Some(loading_bar),
profile, profile_path,
}) })
} }
@@ -261,7 +264,7 @@ pub async fn generate_pack_from_version_id(
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn generate_pack_from_file( pub async fn generate_pack_from_file(
path: PathBuf, path: PathBuf,
profile: PathBuf, profile_path: ProfilePathId,
) -> crate::Result<CreatePackDescription> { ) -> crate::Result<CreatePackDescription> {
let file = io::read(&path).await?; let file = io::read(&path).await?;
Ok(CreatePackDescription { Ok(CreatePackDescription {
@@ -271,6 +274,6 @@ pub async fn generate_pack_from_file(
project_id: None, project_id: None,
version_id: None, version_id: None,
existing_loading_bar: None, existing_loading_bar: None,
profile, profile_path,
}) })
} }

View File

@@ -1,15 +1,17 @@
//! Theseus process management interface //! Theseus process management interface
use std::path::{Path, PathBuf};
use uuid::Uuid; use uuid::Uuid;
use crate::{state::MinecraftChild, util::io::IOError};
pub use crate::{ pub use crate::{
state::{ state::{
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize, Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
}, },
State, State,
}; };
use crate::{
state::{MinecraftChild, ProfilePathId},
util::io::IOError,
};
// Gets whether a child process stored in the state by UUID has finished // Gets whether a child process stored in the state by UUID has finished
#[tracing::instrument] #[tracing::instrument]
@@ -45,7 +47,8 @@ pub async fn get_all_running_uuids() -> crate::Result<Vec<Uuid>> {
// Gets the Profile paths of each *running* stored process in the state // Gets the Profile paths of each *running* stored process in the state
#[tracing::instrument] #[tracing::instrument]
pub async fn get_all_running_profile_paths() -> crate::Result<Vec<PathBuf>> { pub async fn get_all_running_profile_paths() -> crate::Result<Vec<ProfilePathId>>
{
let state = State::get().await?; let state = State::get().await?;
let children = state.children.read().await; let children = state.children.read().await;
children.running_profile_paths().await children.running_profile_paths().await
@@ -62,7 +65,7 @@ pub async fn get_all_running_profiles() -> crate::Result<Vec<Profile>> {
// Gets the UUID of each stored process in the state by profile path // Gets the UUID of each stored process in the state by profile path
#[tracing::instrument] #[tracing::instrument]
pub async fn get_uuids_by_profile_path( pub async fn get_uuids_by_profile_path(
profile_path: &Path, profile_path: ProfilePathId,
) -> crate::Result<Vec<Uuid>> { ) -> crate::Result<Vec<Uuid>> {
let state = State::get().await?; let state = State::get().await?;
let children = state.children.read().await; let children = state.children.read().await;

View File

@@ -7,7 +7,7 @@ use crate::pack::install_from::{
EnvType, PackDependency, PackFile, PackFileHash, PackFormat, EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
}; };
use crate::prelude::JavaVersion; use crate::prelude::JavaVersion;
use crate::state::ProjectMetadata; use crate::state::{ProfilePathId, ProjectMetadata, ProjectPathId};
use crate::util::io::{self, IOError}; use crate::util::io::{self, IOError};
use crate::{ use crate::{
@@ -32,14 +32,14 @@ use tokio::{fs::File, process::Command, sync::RwLock};
/// Remove a profile /// Remove a profile
#[tracing::instrument] #[tracing::instrument]
pub async fn remove(path: &Path) -> crate::Result<()> { pub async fn remove(path: &ProfilePathId) -> crate::Result<()> {
let state = State::get().await?; let state = State::get().await?;
let mut profiles = state.profiles.write().await; let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.remove(path).await? { if let Some(profile) = profiles.remove(path).await? {
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path.clone(), profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Removed, ProfilePayloadType::Removed,
) )
@@ -49,13 +49,14 @@ pub async fn remove(path: &Path) -> crate::Result<()> {
Ok(()) Ok(())
} }
/// Get a profile by path, /// Get a profile by relative path (or, name)
#[tracing::instrument] #[tracing::instrument]
pub async fn get( pub async fn get(
path: &Path, path: &ProfilePathId,
clear_projects: Option<bool>, clear_projects: Option<bool>,
) -> crate::Result<Option<Profile>> { ) -> crate::Result<Option<Profile>> {
let state = State::get().await?; let state = State::get().await?;
let profiles = state.profiles.read().await; let profiles = state.profiles.read().await;
let mut profile = profiles.0.get(path).cloned(); let mut profile = profiles.0.get(path).cloned();
@@ -68,9 +69,29 @@ pub async fn get(
Ok(profile) Ok(profile)
} }
/// Get a profile by uuid
#[tracing::instrument]
pub async fn get_by_uuid(
uuid: uuid::Uuid,
clear_projects: Option<bool>,
) -> crate::Result<Option<Profile>> {
let state = State::get().await?;
let profiles = state.profiles.read().await;
let mut profile = profiles.0.values().find(|x| x.uuid == uuid).cloned();
if clear_projects.unwrap_or(false) {
if let Some(profile) = &mut profile {
profile.projects = HashMap::new();
}
}
Ok(profile)
}
/// Edit a profile using a given asynchronous closure /// Edit a profile using a given asynchronous closure
pub async fn edit<Fut>( pub async fn edit<Fut>(
path: &Path, path: &ProfilePathId,
action: impl Fn(&mut Profile) -> Fut, action: impl Fn(&mut Profile) -> Fut,
) -> crate::Result<()> ) -> crate::Result<()>
where where
@@ -85,7 +106,7 @@ where
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path.clone(), profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Edited, ProfilePayloadType::Edited,
) )
@@ -93,16 +114,14 @@ where
Ok(()) Ok(())
} }
None => Err(crate::ErrorKind::UnmanagedProfileError( None => Err(crate::ErrorKind::UnmanagedProfileError(path.to_string())
path.display().to_string(), .as_error()),
)
.as_error()),
} }
} }
/// Edits a profile's icon /// Edits a profile's icon
pub async fn edit_icon( pub async fn edit_icon(
path: &Path, path: &ProfilePathId,
icon_path: Option<&Path>, icon_path: Option<&Path>,
) -> crate::Result<()> { ) -> crate::Result<()> {
let state = State::get().await?; let state = State::get().await?;
@@ -125,17 +144,17 @@ pub async fn edit_icon(
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path.clone(), profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Edited, ProfilePayloadType::Edited,
) )
.await?; .await?;
Ok(()) Ok(())
} }
None => Err(crate::ErrorKind::UnmanagedProfileError( None => {
path.display().to_string(), Err(crate::ErrorKind::UnmanagedProfileError(path.to_string())
) .as_error())
.as_error()), }
} }
} else { } else {
edit(path, |profile| { edit(path, |profile| {
@@ -155,7 +174,7 @@ pub async fn edit_icon(
// Generally this would be used for profile_create, to get the optimal JRE key // Generally this would be used for profile_create, to get the optimal JRE key
// this can be overwritten by the user a profile-by-profile basis // this can be overwritten by the user a profile-by-profile basis
pub async fn get_optimal_jre_key( pub async fn get_optimal_jre_key(
path: &Path, path: &ProfilePathId,
) -> crate::Result<Option<JavaVersion>> { ) -> crate::Result<Option<JavaVersion>> {
let state = State::get().await?; let state = State::get().await?;
@@ -193,10 +212,8 @@ pub async fn get_optimal_jre_key(
Ok(version) Ok(version)
} else { } else {
Err( Err(crate::ErrorKind::UnmanagedProfileError(path.to_string())
crate::ErrorKind::UnmanagedProfileError(path.display().to_string()) .as_error())
.as_error(),
)
} }
} }
@@ -204,7 +221,7 @@ pub async fn get_optimal_jre_key(
#[tracing::instrument] #[tracing::instrument]
pub async fn list( pub async fn list(
clear_projects: Option<bool>, clear_projects: Option<bool>,
) -> crate::Result<HashMap<PathBuf, Profile>> { ) -> crate::Result<HashMap<ProfilePathId, Profile>> {
let state = State::get().await?; let state = State::get().await?;
let profiles = state.profiles.read().await; let profiles = state.profiles.read().await;
Ok(profiles Ok(profiles
@@ -223,14 +240,12 @@ pub async fn list(
/// Installs/Repairs a profile /// Installs/Repairs a profile
#[tracing::instrument] #[tracing::instrument]
pub async fn install(path: &Path) -> crate::Result<()> { pub async fn install(path: &ProfilePathId) -> crate::Result<()> {
if let Some(profile) = get(path, None).await? { if let Some(profile) = get(path, None).await? {
crate::launcher::install_minecraft(&profile, None).await?; crate::launcher::install_minecraft(&profile, None).await?;
} else { } else {
return Err(crate::ErrorKind::UnmanagedProfileError( return Err(crate::ErrorKind::UnmanagedProfileError(path.to_string())
path.display().to_string(), .as_error());
)
.as_error());
} }
State::sync().await?; State::sync().await?;
Ok(()) Ok(())
@@ -239,12 +254,12 @@ pub async fn install(path: &Path) -> crate::Result<()> {
#[tracing::instrument] #[tracing::instrument]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn update_all( pub async fn update_all(
profile_path: &Path, profile_path: &ProfilePathId,
) -> crate::Result<HashMap<PathBuf, PathBuf>> { ) -> crate::Result<HashMap<ProjectPathId, ProjectPathId>> {
if let Some(profile) = get(profile_path, None).await? { if let Some(profile) = get(profile_path, None).await? {
let loading_bar = init_loading( let loading_bar = init_loading(
LoadingBarType::ProfileUpdate { LoadingBarType::ProfileUpdate {
profile_path: profile.path.clone(), profile_path: profile.get_profile_full_path().await?,
profile_name: profile.metadata.name.clone(), profile_name: profile.metadata.name.clone(),
}, },
100.0, 100.0,
@@ -252,6 +267,7 @@ pub async fn update_all(
) )
.await?; .await?;
let profile_base_path = profile.get_profile_full_path().await?;
let keys = profile let keys = profile
.projects .projects
.into_iter() .into_iter()
@@ -272,7 +288,7 @@ pub async fn update_all(
use futures::StreamExt; use futures::StreamExt;
loading_try_for_each_concurrent( loading_try_for_each_concurrent(
futures::stream::iter(keys).map(Ok::<PathBuf, crate::Error>), futures::stream::iter(keys).map(Ok::<ProjectPathId, crate::Error>),
None, None,
Some(&loading_bar), Some(&loading_bar),
100.0, 100.0,
@@ -297,7 +313,7 @@ pub async fn update_all(
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path, profile_base_path,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Edited, ProfilePayloadType::Edited,
) )
@@ -306,20 +322,22 @@ pub async fn update_all(
Ok(Arc::try_unwrap(map).unwrap().into_inner()) Ok(Arc::try_unwrap(map).unwrap().into_inner())
} else { } else {
Err(crate::ErrorKind::UnmanagedProfileError( Err(
profile_path.display().to_string(), crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
.as_error(),
) )
.as_error())
} }
} }
/// Updates a project to the latest version
/// Uses and returns the relative path to the project
#[tracing::instrument] #[tracing::instrument]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn update_project( pub async fn update_project(
profile_path: &Path, profile_path: &ProfilePathId,
project_path: &Path, project_path: &ProjectPathId,
skip_send_event: Option<bool>, skip_send_event: Option<bool>,
) -> crate::Result<PathBuf> { ) -> crate::Result<ProjectPathId> {
if let Some(profile) = get(profile_path, None).await? { if let Some(profile) = get(profile_path, None).await? {
if let Some(project) = profile.projects.get(project_path) { if let Some(project) = profile.projects.get(project_path) {
if let ProjectMetadata::Modrinth { if let ProjectMetadata::Modrinth {
@@ -331,13 +349,13 @@ pub async fn update_project(
.add_project_version(update_version.id.clone()) .add_project_version(update_version.id.clone())
.await?; .await?;
if path != project_path { if path != project_path.clone() {
profile.remove_project(project_path, Some(true)).await?; profile.remove_project(project_path, Some(true)).await?;
} }
let state = State::get().await?; let state = State::get().await?;
let mut profiles = state.profiles.write().await; let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(project_path) { if let Some(profile) = profiles.0.get_mut(profile_path) {
let value = profile.projects.remove(project_path); let value = profile.projects.remove(project_path);
if let Some(mut project) = value { if let Some(mut project) = value {
if let ProjectMetadata::Modrinth { if let ProjectMetadata::Modrinth {
@@ -354,7 +372,7 @@ pub async fn update_project(
if !skip_send_event.unwrap_or(false) { if !skip_send_event.unwrap_or(false) {
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path, profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Edited, ProfilePayloadType::Edited,
) )
@@ -371,25 +389,26 @@ pub async fn update_project(
) )
.as_error()) .as_error())
} else { } else {
Err(crate::ErrorKind::UnmanagedProfileError( Err(
profile_path.display().to_string(), crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
.as_error(),
) )
.as_error())
} }
} }
/// Add a project from a version /// Add a project from a version
/// Returns the relative path to the project as a ProjectPathId
#[tracing::instrument] #[tracing::instrument]
pub async fn add_project_from_version( pub async fn add_project_from_version(
profile_path: &Path, profile_path: &ProfilePathId,
version_id: String, version_id: String,
) -> crate::Result<PathBuf> { ) -> crate::Result<ProjectPathId> {
if let Some(profile) = get(profile_path, None).await? { if let Some(profile) = get(profile_path, None).await? {
let (path, _) = profile.add_project_version(version_id).await?; let (path, _) = profile.add_project_version(version_id).await?;
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path, profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Edited, ProfilePayloadType::Edited,
) )
@@ -398,20 +417,21 @@ pub async fn add_project_from_version(
Ok(path) Ok(path)
} else { } else {
Err(crate::ErrorKind::UnmanagedProfileError( Err(
profile_path.display().to_string(), crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
.as_error(),
) )
.as_error())
} }
} }
/// Add a project from an FS path /// Add a project from an FS path
/// Uses and returns the relative path to the project as a ProjectPathId
#[tracing::instrument] #[tracing::instrument]
pub async fn add_project_from_path( pub async fn add_project_from_path(
profile_path: &Path, profile_path: &ProfilePathId,
path: &Path, path: &Path,
project_type: Option<String>, project_type: Option<String>,
) -> crate::Result<PathBuf> { ) -> crate::Result<ProjectPathId> {
if let Some(profile) = get(profile_path, None).await? { if let Some(profile) = get(profile_path, None).await? {
let file = io::read(path).await?; let file = io::read(path).await?;
let file_name = path let file_name = path
@@ -430,7 +450,7 @@ pub async fn add_project_from_path(
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path, profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Edited, ProfilePayloadType::Edited,
) )
@@ -439,25 +459,27 @@ pub async fn add_project_from_path(
Ok(path) Ok(path)
} else { } else {
Err(crate::ErrorKind::UnmanagedProfileError( Err(
profile_path.display().to_string(), crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
.as_error(),
) )
.as_error())
} }
} }
/// Toggle whether a project is disabled or not /// Toggle whether a project is disabled or not
/// Project path should be relative to the profile
/// returns the new state, relative to the profile
#[tracing::instrument] #[tracing::instrument]
pub async fn toggle_disable_project( pub async fn toggle_disable_project(
profile: &Path, profile: &ProfilePathId,
project: &Path, project: &ProjectPathId,
) -> crate::Result<PathBuf> { ) -> crate::Result<ProjectPathId> {
if let Some(profile) = get(profile, None).await? { if let Some(profile) = get(profile, None).await? {
let res = profile.toggle_disable_project(project).await?; let res = profile.toggle_disable_project(project).await?;
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path, profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Edited, ProfilePayloadType::Edited,
) )
@@ -466,25 +488,24 @@ pub async fn toggle_disable_project(
Ok(res) Ok(res)
} else { } else {
Err(crate::ErrorKind::UnmanagedProfileError( Err(crate::ErrorKind::UnmanagedProfileError(profile.to_string())
profile.display().to_string(), .as_error())
)
.as_error())
} }
} }
/// Remove a project from a profile /// Remove a project from a profile
/// Uses and returns the relative path to the project
#[tracing::instrument] #[tracing::instrument]
pub async fn remove_project( pub async fn remove_project(
profile: &Path, profile: &ProfilePathId,
project: &Path, project: &ProjectPathId,
) -> crate::Result<()> { ) -> crate::Result<()> {
if let Some(profile) = get(profile, None).await? { if let Some(profile) = get(profile, None).await? {
profile.remove_project(project, None).await?; profile.remove_project(project, None).await?;
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path, profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Edited, ProfilePayloadType::Edited,
) )
@@ -493,10 +514,8 @@ pub async fn remove_project(
Ok(()) Ok(())
} else { } else {
Err(crate::ErrorKind::UnmanagedProfileError( Err(crate::ErrorKind::UnmanagedProfileError(profile.to_string())
profile.display().to_string(), .as_error())
)
.as_error())
} }
} }
@@ -505,7 +524,7 @@ pub async fn remove_project(
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn export_mrpack( pub async fn export_mrpack(
profile_path: &Path, profile_path: &ProfilePathId,
export_path: PathBuf, export_path: PathBuf,
included_overrides: Vec<String>, // which folders to include in the overrides included_overrides: Vec<String>, // which folders to include in the overrides
version_id: Option<String>, version_id: Option<String>,
@@ -516,11 +535,24 @@ pub async fn export_mrpack(
let profile = get(profile_path, None).await?.ok_or_else(|| { let profile = get(profile_path, None).await?.ok_or_else(|| {
crate::ErrorKind::OtherError(format!( crate::ErrorKind::OtherError(format!(
"Tried to export a nonexistent or unloaded profile at path {}!", "Tried to export a nonexistent or unloaded profile at path {}!",
profile_path.display() profile_path
)) ))
})?; })?;
let profile_base_path = &profile.path; // remove .DS_Store files from included_overrides
let included_overrides = included_overrides
.into_iter()
.filter(|x| {
if let Some(f) = PathBuf::from(x).file_name() {
if f.to_string_lossy().starts_with(".DS_Store") {
return false;
}
}
true
})
.collect::<Vec<_>>();
let profile_base_path = &profile.get_profile_full_path().await?;
let mut file = File::create(&export_path) let mut file = File::create(&export_path)
.await .await
@@ -529,7 +561,7 @@ pub async fn export_mrpack(
// Create mrpack json configuration file // Create mrpack json configuration file
let version_id = version_id.unwrap_or("1.0.0".to_string()); let version_id = version_id.unwrap_or("1.0.0".to_string());
let packfile = create_mrpack_json(&profile, version_id)?; let packfile = create_mrpack_json(&profile, version_id).await?;
let modrinth_path_list = get_modrinth_pack_list(&packfile); let modrinth_path_list = get_modrinth_pack_list(&packfile);
// Build vec of all files in the folder // Build vec of all files in the folder
@@ -539,7 +571,7 @@ pub async fn export_mrpack(
// Initialize loading bar // Initialize loading bar
let loading_bar = init_loading( let loading_bar = init_loading(
LoadingBarType::ZipExtract { LoadingBarType::ZipExtract {
profile_path: profile.path.to_path_buf(), profile_path: profile.get_profile_full_path().await?,
profile_name: profile.metadata.name.clone(), profile_name: profile.metadata.name.clone(),
}, },
path_list.len() as f64, path_list.len() as f64,
@@ -628,25 +660,28 @@ pub async fn export_mrpack(
// => [folder1, folder2] // => [folder1, folder2]
#[tracing::instrument] #[tracing::instrument]
pub async fn get_potential_override_folders( pub async fn get_potential_override_folders(
profile_path: PathBuf, profile_path: ProfilePathId,
) -> crate::Result<Vec<PathBuf>> { ) -> crate::Result<Vec<PathBuf>> {
// First, get a dummy mrpack json for the files within // First, get a dummy mrpack json for the files within
let profile: Profile = let profile: Profile =
get(&profile_path, None).await?.ok_or_else(|| { get(&profile_path, None).await?.ok_or_else(|| {
crate::ErrorKind::OtherError(format!( crate::ErrorKind::OtherError(format!(
"Tried to export a nonexistent or unloaded profile at path {}!", "Tried to export a nonexistent or unloaded profile at path {}!",
profile_path.display() profile_path
)) ))
})?; })?;
let mrpack = create_mrpack_json(&profile, "0".to_string())?; // dummy mrpack to get pack list
let mrpack = create_mrpack_json(&profile, "0".to_string()).await?;
let mrpack_files = get_modrinth_pack_list(&mrpack); let mrpack_files = get_modrinth_pack_list(&mrpack);
let mut path_list: Vec<PathBuf> = Vec::new(); let mut path_list: Vec<PathBuf> = Vec::new();
let mut read_dir = io::read_dir(&profile_path).await?;
let profile_base_dir = profile.get_profile_full_path().await?;
let mut read_dir = io::read_dir(&profile_base_dir).await?;
while let Some(entry) = read_dir while let Some(entry) = read_dir
.next_entry() .next_entry()
.await .await
.map_err(|e| IOError::with_path(e, &profile_path))? .map_err(|e| IOError::with_path(e, &profile_base_dir))?
{ {
let path: PathBuf = entry.path(); let path: PathBuf = entry.path();
if path.is_dir() { if path.is_dir() {
@@ -655,17 +690,17 @@ pub async fn get_potential_override_folders(
while let Some(entry) = read_dir while let Some(entry) = read_dir
.next_entry() .next_entry()
.await .await
.map_err(|e| IOError::with_path(e, &profile_path))? .map_err(|e| IOError::with_path(e, &profile_base_dir))?
{ {
let path: PathBuf = entry.path(); let path: PathBuf = entry.path();
let name = path.strip_prefix(&profile_path)?.to_path_buf(); let name = path.strip_prefix(&profile_base_dir)?.to_path_buf();
if !mrpack_files.contains(&name.to_string_lossy().to_string()) { if !mrpack_files.contains(&name.to_string_lossy().to_string()) {
path_list.push(name); path_list.push(name);
} }
} }
} else { } else {
// One layer of files/folders if its a file // One layer of files/folders if its a file
let name = path.strip_prefix(&profile_path)?.to_path_buf(); let name = path.strip_prefix(&profile_base_dir)?.to_path_buf();
if !mrpack_files.contains(&name.to_string_lossy().to_string()) { if !mrpack_files.contains(&name.to_string_lossy().to_string()) {
path_list.push(name); path_list.push(name);
} }
@@ -677,7 +712,9 @@ pub async fn get_potential_override_folders(
/// Run Minecraft using a profile and the default credentials, logged in credentials, /// Run Minecraft using a profile and the default credentials, logged in credentials,
/// failing with an error if no credentials are available /// failing with an error if no credentials are available
#[tracing::instrument] #[tracing::instrument]
pub async fn run(path: &Path) -> crate::Result<Arc<RwLock<MinecraftChild>>> { pub async fn run(
path: &ProfilePathId,
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
let state = State::get().await?; let state = State::get().await?;
// Get default account and refresh credentials (preferred way to log in) // Get default account and refresh credentials (preferred way to log in)
@@ -702,7 +739,7 @@ pub async fn run(path: &Path) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
#[tracing::instrument(skip(credentials))] #[tracing::instrument(skip(credentials))]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn run_credentials( pub async fn run_credentials(
path: &Path, path: &ProfilePathId,
credentials: &auth::Credentials, credentials: &auth::Credentials,
) -> crate::Result<Arc<RwLock<MinecraftChild>>> { ) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
let state = State::get().await?; let state = State::get().await?;
@@ -710,7 +747,7 @@ pub async fn run_credentials(
let profile = get(path, None).await?.ok_or_else(|| { let profile = get(path, None).await?.ok_or_else(|| {
crate::ErrorKind::OtherError(format!( crate::ErrorKind::OtherError(format!(
"Tried to run a nonexistent or unloaded profile at path {}!", "Tried to run a nonexistent or unloaded profile at path {}!",
path.display() path
)) ))
})?; })?;
@@ -720,11 +757,12 @@ pub async fn run_credentials(
// TODO: hook parameters // TODO: hook parameters
let mut cmd = hook.split(' '); let mut cmd = hook.split(' ');
if let Some(command) = cmd.next() { if let Some(command) = cmd.next() {
let full_path = path.get_full_path().await?;
let result = Command::new(command) let result = Command::new(command)
.args(&cmd.collect::<Vec<&str>>()) .args(&cmd.collect::<Vec<&str>>())
.current_dir(path) .current_dir(&full_path)
.spawn() .spawn()
.map_err(|e| IOError::with_path(e, path))? .map_err(|e| IOError::with_path(e, &full_path))?
.wait() .wait()
.await .await
.map_err(IOError::from)?; .map_err(IOError::from)?;
@@ -767,7 +805,9 @@ pub async fn run_credentials(
let mut cmd = hook.split(' '); let mut cmd = hook.split(' ');
if let Some(command) = cmd.next() { if let Some(command) = cmd.next() {
let mut command = Command::new(command); let mut command = Command::new(command);
command.args(&cmd.collect::<Vec<&str>>()).current_dir(path); command
.args(&cmd.collect::<Vec<&str>>())
.current_dir(path.get_full_path().await?);
Some(command) Some(command)
} else { } else {
None None
@@ -806,7 +846,7 @@ fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec<String> {
/// Creates a json configuration for a .mrpack zipped file /// Creates a json configuration for a .mrpack zipped file
// Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44) // Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44)
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn create_mrpack_json( pub async fn create_mrpack_json(
profile: &Profile, profile: &Profile,
version_id: String, version_id: String,
) -> crate::Result<PackFormat> { ) -> crate::Result<PackFormat> {
@@ -845,17 +885,15 @@ pub fn create_mrpack_json(
.map(|(k, v)| (k, sanitize_loader_version_string(&v).to_string())) .map(|(k, v)| (k, sanitize_loader_version_string(&v).to_string()))
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let base_path = &profile.path; let profile_base_path = profile.get_profile_full_path().await?;
let files: Result<Vec<PackFile>, crate::ErrorKind> = profile let files: Result<Vec<PackFile>, crate::ErrorKind> = profile
.projects .projects
.iter() .iter()
.filter_map(|(mod_path, project)| { .filter_map(|(mod_path, project)| {
let path = match mod_path.strip_prefix(base_path) { let path: String = profile_base_path
Ok(path) => path.to_string_lossy().to_string(), .join(mod_path.0.clone())
Err(e) => { .to_string_lossy()
return Some(Err(e.into())); .to_string();
}
};
// Only Modrinth projects have a modrinth metadata field for the modrinth.json // Only Modrinth projects have a modrinth metadata field for the modrinth.json
Some(Ok(match project.metadata { Some(Ok(match project.metadata {

View File

@@ -1,5 +1,5 @@
//! Theseus profile management interface //! Theseus profile management interface
use crate::state::LinkedData; use crate::state::{LinkedData, ProfilePathId};
use crate::util::io::{self, canonicalize}; use crate::util::io::{self, canonicalize};
use crate::{ use crate::{
event::{emit::emit_profile, ProfilePayloadType}, event::{emit::emit_profile, ProfilePayloadType},
@@ -10,20 +10,18 @@ pub use crate::{
State, State,
}; };
use daedalus::modded::LoaderVersion; use daedalus::modded::LoaderVersion;
use futures::prelude::*;
use std::path::PathBuf; use std::path::PathBuf;
use tokio_stream::wrappers::ReadDirStream;
use tracing::{info, trace}; use tracing::{info, trace};
use uuid::Uuid; use uuid::Uuid;
// Creates a profile at the given filepath and adds it to the in-memory state // Creates a profile of a given name and adds it to the in-memory state
// Returns filepath at which it can be accessed in the State // Returns relative filepath as ProfilePathId which can be used to access it in the State
#[tracing::instrument] #[tracing::instrument]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn profile_create( pub async fn profile_create(
name: String, // the name of the profile, and relative path mut name: String, // the name of the profile, and relative path
game_version: String, // the game version of the profile game_version: String, // the game version of the profile
modloader: ModLoader, // the modloader to use modloader: ModLoader, // the modloader to use
loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest
@@ -31,32 +29,34 @@ pub async fn profile_create(
icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES) icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES)
linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
skip_install_profile: Option<bool>, skip_install_profile: Option<bool>,
) -> crate::Result<PathBuf> { ) -> crate::Result<ProfilePathId> {
trace!("Creating new profile. {}", name); trace!("Creating new profile. {}", name);
let state = State::get().await?; let state = State::get().await?;
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
let path = state.directories.profiles_dir().join(uuid.to_string());
let mut path = state.directories.profiles_dir().await.join(&name);
if path.exists() { if path.exists() {
if !path.is_dir() { let mut new_name;
return Err(ProfileCreationError::NotFolder.into()); let mut new_path;
} let mut which = 1;
if path.join("profile.json").exists() { loop {
return Err(ProfileCreationError::ProfileExistsError( new_name = format!("{name} ({which})");
path.join("profile.json"), new_path = state.directories.profiles_dir().await.join(&new_name);
) if !new_path.exists() {
.into()); break;
}
which += 1;
} }
if ReadDirStream::new(io::read_dir(&path).await?) tracing::debug!(
.next() "Folder collision: {}, renaming to: {}",
.await path.display(),
.is_some() new_path.display()
{ );
return Err(ProfileCreationError::NotEmptyFolder.into()); path = new_path;
} name = new_name;
} else {
io::create_dir_all(&path).await?;
} }
io::create_dir_all(&path).await?;
info!( info!(
"Creating profile at path {}", "Creating profile at path {}",
@@ -73,13 +73,11 @@ pub async fn profile_create(
None None
}; };
// Fully canonicalize now that its created for storing purposes let mut profile = Profile::new(uuid, name, game_version).await?;
let path = canonicalize(&path)?;
let mut profile =
Profile::new(uuid, name, game_version, path.clone()).await?;
let result = async { let result = async {
if let Some(ref icon) = icon { if let Some(ref icon) = icon {
let bytes = io::read(icon).await?; let bytes =
io::read(state.directories.caches_dir().join(icon)).await?;
profile profile
.set_icon( .set_icon(
&state.directories.caches_dir(), &state.directories.caches_dir(),
@@ -100,7 +98,7 @@ pub async fn profile_create(
emit_profile( emit_profile(
uuid, uuid,
path.clone(), profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Created, ProfilePayloadType::Created,
) )
@@ -116,14 +114,14 @@ pub async fn profile_create(
} }
State::sync().await?; State::sync().await?;
Ok(path) Ok(profile.profile_id())
} }
.await; .await;
match result { match result {
Ok(profile) => Ok(profile), Ok(profile) => Ok(profile),
Err(err) => { Err(err) => {
let _ = crate::api::profile::remove(&profile.path).await; let _ = crate::api::profile::remove(&profile.profile_id()).await;
Err(err) Err(err)
} }

View File

@@ -1,5 +1,16 @@
//! Theseus profile management interface //! Theseus profile management interface
use std::path::PathBuf;
use io::IOError;
use tokio::sync::RwLock;
use crate::{
event::emit::{emit_loading, init_loading},
prelude::DirectoryInfo,
state::{self, Profiles},
util::io,
};
pub use crate::{ pub use crate::{
state::{ state::{
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize, Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
@@ -19,6 +30,16 @@ pub async fn get() -> crate::Result<Settings> {
#[tracing::instrument] #[tracing::instrument]
pub async fn set(settings: Settings) -> crate::Result<()> { pub async fn set(settings: Settings) -> crate::Result<()> {
let state = State::get().await?; let state = State::get().await?;
if settings.loaded_config_dir
!= state.settings.read().await.loaded_config_dir
{
return Err(crate::ErrorKind::OtherError(
"Cannot change config directory as setting".to_string(),
)
.as_error());
}
let (reset_io, reset_fetch) = async { let (reset_io, reset_fetch) = async {
let read = state.settings.read().await; let read = state.settings.read().await;
( (
@@ -42,3 +63,119 @@ pub async fn set(settings: Settings) -> crate::Result<()> {
State::sync().await?; State::sync().await?;
Ok(()) Ok(())
} }
/// Sets the new config dir, the location of all Theseus data except for the settings.json and caches
/// Takes control of the entire state and blocks until completion
pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
if !new_config_dir.is_dir() {
return Err(crate::ErrorKind::FSError(format!(
"New config dir is not a folder: {}",
new_config_dir.display()
))
.as_error());
}
let loading_bar = init_loading(
crate::LoadingBarType::ConfigChange {
new_path: new_config_dir.clone(),
},
100.0,
"Changing configuration directory",
)
.await?;
tracing::trace!("Changing config dir, taking control of the state");
// Take control of the state
let mut state_write = State::get_write().await?;
let old_config_dir =
state_write.directories.config_dir.read().await.clone();
tracing::trace!("Setting configuration setting");
// Set load config dir setting
let settings = {
let mut settings = state_write.settings.write().await;
settings.loaded_config_dir = Some(new_config_dir.clone());
// Some java paths are hardcoded to within our config dir, so we need to update them
tracing::trace!("Updating java keys");
for key in settings.java_globals.keys() {
if let Some(java) = settings.java_globals.get_mut(&key) {
// If the path is within the old config dir path, update it to the new config dir
if let Ok(relative_path) = PathBuf::from(java.path.clone())
.strip_prefix(&old_config_dir)
{
java.path = new_config_dir
.join(relative_path)
.to_string_lossy()
.to_string();
}
}
}
tracing::trace!("Syncing settings");
settings
.sync(&state_write.directories.settings_file())
.await?;
settings.clone()
};
tracing::trace!("Reinitializing directory");
// Set new state information
state_write.directories = DirectoryInfo::init(&settings)?;
let total_entries = std::fs::read_dir(&old_config_dir)
.map_err(|e| IOError::with_path(e, &old_config_dir))?
.count() as f64;
// Move all files over from state_write.directories.config_dir to new_config_dir
tracing::trace!("Renaming folder structure");
let mut i = 0.0;
let mut entries = io::read_dir(&old_config_dir).await?;
while let Some(entry) = entries
.next_entry()
.await
.map_err(|e| IOError::with_path(e, &old_config_dir))?
{
let entry_path = entry.path();
if let Some(file_name) = entry_path.file_name() {
// Ignore settings.json
if file_name == state::SETTINGS_FILE_NAME {
continue;
}
// Ignore caches folder
if file_name == state::CACHES_FOLDER_NAME {
continue;
}
// Ignore modrinth_logs folder
if file_name == state::LAUNCHER_LOGS_FOLDER_NAME {
continue;
}
let new_path = new_config_dir.join(file_name);
io::rename(entry_path, new_path).await?;
i += 1.0;
emit_loading(&loading_bar, 90.0 * (i / total_entries), None)
.await?;
}
}
// Reset file watcher
tracing::trace!("Reset file watcher");
let mut file_watcher = state::init_watcher().await?;
// Reset profiles (for filepaths, file watcher, etc)
state_write.profiles = RwLock::new(
Profiles::init(&state_write.directories, &mut file_watcher).await?,
);
state_write.file_watcher = RwLock::new(file_watcher);
emit_loading(&loading_bar, 10.0, None).await?;
// TODO: need to be able to safely error out of this function, reverting the changes
tracing::info!(
"Successfully switched config folder to: {}",
new_config_dir.display()
);
Ok(())
}

View File

@@ -61,7 +61,7 @@ pub enum ErrorKind {
#[error("Error acquiring semaphore: {0}")] #[error("Error acquiring semaphore: {0}")]
AcquireError(#[from] tokio::sync::AcquireError), AcquireError(#[from] tokio::sync::AcquireError),
#[error("Profile {0} is not managed by Theseus!")] #[error("Profile {0} is not managed by the app!")]
UnmanagedProfileError(String), UnmanagedProfileError(String),
#[error("Could not create profile: {0}")] #[error("Could not create profile: {0}")]

View File

@@ -181,6 +181,9 @@ pub enum LoadingBarType {
profile_path: PathBuf, profile_path: PathBuf,
profile_name: String, profile_name: String,
}, },
ConfigChange {
new_path: PathBuf,
},
} }
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]

View File

@@ -68,6 +68,7 @@ pub async fn download_version_info(
let path = st let path = st
.directories .directories
.version_dir(&version_id) .version_dir(&version_id)
.await
.join(format!("{version_id}.json")); .join(format!("{version_id}.json"));
let res = if path.exists() && !force.unwrap_or(false) { let res = if path.exists() && !force.unwrap_or(false) {
@@ -118,6 +119,7 @@ pub async fn download_client(
let path = st let path = st
.directories .directories
.version_dir(version) .version_dir(version)
.await
.join(format!("{version}.jar")); .join(format!("{version}.jar"));
if !path.exists() { if !path.exists() {
@@ -149,6 +151,7 @@ pub async fn download_assets_index(
let path = st let path = st
.directories .directories
.assets_index_dir() .assets_index_dir()
.await
.join(format!("{}.json", &version.asset_index.id)); .join(format!("{}.json", &version.asset_index.id));
let res = if path.exists() { let res = if path.exists() {
@@ -192,7 +195,7 @@ pub async fn download_assets(
None, None,
|(name, asset)| async move { |(name, asset)| async move {
let hash = &asset.hash; let hash = &asset.hash;
let resource_path = st.directories.object_dir(hash); let resource_path = st.directories.object_dir(hash).await;
let url = format!( let url = format!(
"https://resources.download.minecraft.net/{sub_hash}/{hash}", "https://resources.download.minecraft.net/{sub_hash}/{hash}",
sub_hash = &hash[..2] sub_hash = &hash[..2]
@@ -215,7 +218,7 @@ pub async fn download_assets(
let resource = fetch_cell let resource = fetch_cell
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore))
.await?; .await?;
let resource_path = st.directories.legacy_assets_dir().join( let resource_path = st.directories.legacy_assets_dir().await.join(
name.replace('/', &String::from(std::path::MAIN_SEPARATOR)) name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
); );
write(&resource_path, resource, &st.io_semaphore).await?; write(&resource_path, resource, &st.io_semaphore).await?;
@@ -245,8 +248,8 @@ pub async fn download_libraries(
tracing::debug!("Loading libraries"); tracing::debug!("Loading libraries");
tokio::try_join! { tokio::try_join! {
io::create_dir_all(st.directories.libraries_dir()), io::create_dir_all(st.directories.libraries_dir().await),
io::create_dir_all(st.directories.version_natives_dir(version)) io::create_dir_all(st.directories.version_natives_dir(version).await)
}?; }?;
let num_files = libraries.len(); let num_files = libraries.len();
loading_try_for_each_concurrent( loading_try_for_each_concurrent(
@@ -262,7 +265,7 @@ pub async fn download_libraries(
tokio::try_join! { tokio::try_join! {
async { async {
let artifact_path = d::get_path_from_artifact(&library.name)?; let artifact_path = d::get_path_from_artifact(&library.name)?;
let path = st.directories.libraries_dir().join(&artifact_path); let path = st.directories.libraries_dir().await.join(&artifact_path);
match library.downloads { match library.downloads {
_ if path.exists() => Ok(()), _ if path.exists() => Ok(()),
@@ -314,7 +317,7 @@ pub async fn download_libraries(
let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?; let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?;
let reader = std::io::Cursor::new(&data); let reader = std::io::Cursor::new(&data);
if let Ok(mut archive) = zip::ZipArchive::new(reader) { if let Ok(mut archive) = zip::ZipArchive::new(reader) {
match archive.extract(&st.directories.version_natives_dir(version)) { match archive.extract(st.directories.version_natives_dir(version).await) {
Ok(_) => tracing::info!("Fetched native {}", &library.name), Ok(_) => tracing::info!("Fetched native {}", &library.name),
Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err) Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err)
} }

View File

@@ -108,14 +108,14 @@ pub async fn install_minecraft(
LoadingBarType::MinecraftDownload { LoadingBarType::MinecraftDownload {
// If we are downloading minecraft for a profile, provide its name and uuid // If we are downloading minecraft for a profile, provide its name and uuid
profile_name: profile.metadata.name.clone(), profile_name: profile.metadata.name.clone(),
profile_path: profile.path.clone(), profile_path: profile.get_profile_full_path().await?,
}, },
100.0, 100.0,
"Downloading Minecraft", "Downloading Minecraft",
) )
.await?; .await?;
crate::api::profile::edit(&profile.path, |prof| { crate::api::profile::edit(&profile.profile_id(), |prof| {
prof.install_stage = ProfileInstallStage::Installing; prof.install_stage = ProfileInstallStage::Installing;
async { Ok(()) } async { Ok(()) }
@@ -124,7 +124,8 @@ pub async fn install_minecraft(
State::sync().await?; State::sync().await?;
let state = State::get().await?; let state = State::get().await?;
let instance_path = &io::canonicalize(&profile.path)?; let instance_path =
&io::canonicalize(&profile.get_profile_full_path().await?)?;
let metadata = state.metadata.read().await; let metadata = state.metadata.read().await;
let version = metadata let version = metadata
@@ -176,8 +177,11 @@ pub async fn install_minecraft(
let client_path = state let client_path = state
.directories .directories
.version_dir(&version_jar) .version_dir(&version_jar)
.await
.join(format!("{version_jar}.jar")); .join(format!("{version_jar}.jar"));
let libraries_dir = state.directories.libraries_dir().await;
if let Some(ref mut data) = version_info.data { if let Some(ref mut data) = version_info.data {
processor_rules! { processor_rules! {
data; data;
@@ -194,7 +198,7 @@ pub async fn install_minecraft(
client => instance_path.to_string_lossy(), client => instance_path.to_string_lossy(),
server => ""; server => "";
"LIBRARY_DIR": "LIBRARY_DIR":
client => state.directories.libraries_dir().to_string_lossy(), client => libraries_dir.to_string_lossy(),
server => ""; server => "";
} }
@@ -217,13 +221,13 @@ pub async fn install_minecraft(
let child = Command::new(&java_version.path) let child = Command::new(&java_version.path)
.arg("-cp") .arg("-cp")
.arg(args::get_class_paths_jar( .arg(args::get_class_paths_jar(
&state.directories.libraries_dir(), &libraries_dir,
&cp, &cp,
&java_version.architecture, &java_version.architecture,
)?) )?)
.arg( .arg(
args::get_processor_main_class(args::get_lib_path( args::get_processor_main_class(args::get_lib_path(
&state.directories.libraries_dir(), &libraries_dir,
&processor.jar, &processor.jar,
false, false,
)?) )?)
@@ -236,7 +240,7 @@ pub async fn install_minecraft(
})?, })?,
) )
.args(args::get_processor_arguments( .args(args::get_processor_arguments(
&state.directories.libraries_dir(), &libraries_dir,
&processor.args, &processor.args,
data, data,
)?) )?)
@@ -269,7 +273,7 @@ pub async fn install_minecraft(
} }
} }
crate::api::profile::edit(&profile.path, |prof| { crate::api::profile::edit(&profile.profile_id(), |prof| {
prof.install_stage = ProfileInstallStage::Installed; prof.install_stage = ProfileInstallStage::Installed;
async { Ok(()) } async { Ok(()) }
@@ -309,7 +313,9 @@ pub async fn launch_minecraft(
let state = State::get().await?; let state = State::get().await?;
let metadata = state.metadata.read().await; let metadata = state.metadata.read().await;
let instance_path = &io::canonicalize(&profile.path)?;
let instance_path = profile.get_profile_full_path().await?;
let instance_path = &io::canonicalize(instance_path)?;
let version = metadata let version = metadata
.minecraft .minecraft
@@ -359,6 +365,7 @@ pub async fn launch_minecraft(
let client_path = state let client_path = state
.directories .directories
.version_dir(&version_jar) .version_dir(&version_jar)
.await
.join(format!("{version_jar}.jar")); .join(format!("{version_jar}.jar"));
let args = version_info.arguments.clone().unwrap_or_default(); let args = version_info.arguments.clone().unwrap_or_default();
@@ -374,11 +381,11 @@ pub async fn launch_minecraft(
// Check if profile has a running profile, and reject running the command if it does // Check if profile has a running profile, and reject running the command if it does
// Done late so a quick double call doesn't launch two instances // Done late so a quick double call doesn't launch two instances
let existing_processes = let existing_processes =
process::get_uuids_by_profile_path(instance_path).await?; process::get_uuids_by_profile_path(profile.profile_id()).await?;
if let Some(uuid) = existing_processes.first() { if let Some(uuid) = existing_processes.first() {
return Err(crate::ErrorKind::LauncherError(format!( return Err(crate::ErrorKind::LauncherError(format!(
"Profile {} is already running at UUID: {uuid}", "Profile {} is already running at UUID: {uuid}",
instance_path.display() profile.profile_id()
)) ))
.as_error()); .as_error());
} }
@@ -388,10 +395,10 @@ pub async fn launch_minecraft(
args::get_jvm_arguments( args::get_jvm_arguments(
args.get(&d::minecraft::ArgumentType::Jvm) args.get(&d::minecraft::ArgumentType::Jvm)
.map(|x| x.as_slice()), .map(|x| x.as_slice()),
&state.directories.version_natives_dir(&version_jar), &state.directories.version_natives_dir(&version_jar).await,
&state.directories.libraries_dir(), &state.directories.libraries_dir().await,
&args::get_class_paths( &args::get_class_paths(
&state.directories.libraries_dir(), &state.directories.libraries_dir().await,
version_info.libraries.as_slice(), version_info.libraries.as_slice(),
&client_path, &client_path,
&java_version.architecture, &java_version.architecture,
@@ -414,7 +421,7 @@ pub async fn launch_minecraft(
&version.id, &version.id,
&version_info.asset_index.id, &version_info.asset_index.id,
instance_path, instance_path,
&state.directories.assets_dir(), &state.directories.assets_dir().await,
&version.type_, &version.type_,
*resolution, *resolution,
&java_version.architecture, &java_version.architecture,
@@ -439,14 +446,15 @@ pub async fn launch_minecraft(
let logs_dir = { let logs_dir = {
let st = State::get().await?; let st = State::get().await?;
st.directories st.directories
.profile_logs_dir(profile.uuid) .profile_logs_dir(&profile.profile_id())
.await?
.join(&datetime_string) .join(&datetime_string)
}; };
io::create_dir_all(&logs_dir).await?; io::create_dir_all(&logs_dir).await?;
let stdout_log_path = logs_dir.join("stdout.log"); let stdout_log_path = logs_dir.join("stdout.log");
crate::api::profile::edit(&profile.path, |prof| { crate::api::profile::edit(&profile.profile_id(), |prof| {
prof.metadata.last_played = Some(Utc::now()); prof.metadata.last_played = Some(Utc::now());
async { Ok(()) } async { Ok(()) }
@@ -499,7 +507,7 @@ pub async fn launch_minecraft(
state_children state_children
.insert_process( .insert_process(
Uuid::new_v4(), Uuid::new_v4(),
instance_path.to_path_buf(), profile.profile_id(),
stdout_log_path, stdout_log_path,
command, command,
post_exit_hook, post_exit_hook,

View File

@@ -44,10 +44,10 @@ pub fn start_logger() -> Option<WorkerGuard> {
use tracing_subscriber::prelude::*; use tracing_subscriber::prelude::*;
// Initialize and get logs directory path // Initialize and get logs directory path
let path = if let Some(dir) = DirectoryInfo::init().ok() { let logs_dir = if let Some(d) = DirectoryInfo::launcher_logs_dir() {
dir.launcher_logs_dir() d
} else { } else {
eprintln!("Could not create logger."); eprintln!("Could not start logger");
return None; return None;
}; };
@@ -55,7 +55,7 @@ pub fn start_logger() -> Option<WorkerGuard> {
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info")); .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info"));
let file_appender = let file_appender =
RollingFileAppender::new(Rotation::DAILY, path, "theseus.log"); RollingFileAppender::new(Rotation::DAILY, logs_dir, "theseus.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let subscriber = tracing_subscriber::registry() let subscriber = tracing_subscriber::registry()

View File

@@ -1,4 +1,4 @@
use super::Profile; use super::{Profile, ProfilePathId};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::ExitStatus; use std::process::ExitStatus;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
@@ -25,7 +25,7 @@ pub struct Children(HashMap<Uuid, Arc<RwLock<MinecraftChild>>>);
#[derive(Debug)] #[derive(Debug)]
pub struct MinecraftChild { pub struct MinecraftChild {
pub uuid: Uuid, pub uuid: Uuid,
pub profile_path: PathBuf, //todo: make UUID when profiles are recognized by UUID pub profile_relative_path: ProfilePathId,
pub manager: Option<JoinHandle<crate::Result<ExitStatus>>>, // None when future has completed and been handled pub manager: Option<JoinHandle<crate::Result<ExitStatus>>>, // None when future has completed and been handled
pub current_child: Arc<RwLock<Child>>, pub current_child: Arc<RwLock<Child>>,
pub output: SharedOutput, pub output: SharedOutput,
@@ -53,7 +53,7 @@ impl Children {
pub async fn insert_process( pub async fn insert_process(
&mut self, &mut self,
uuid: Uuid, uuid: Uuid,
profile_path: PathBuf, profile_relative_path: ProfilePathId,
log_path: PathBuf, log_path: PathBuf,
mut mc_command: Command, mut mc_command: Command,
post_command: Option<Command>, // Command to run after minecraft. post_command: Option<Command>, // Command to run after minecraft.
@@ -107,7 +107,7 @@ impl Children {
// Create MinecraftChild // Create MinecraftChild
let mchild = MinecraftChild { let mchild = MinecraftChild {
uuid, uuid,
profile_path, profile_relative_path,
current_child, current_child,
output: shared_output, output: shared_output,
manager, manager,
@@ -266,7 +266,7 @@ impl Children {
// Gets all PID keys of running children with a given profile path // Gets all PID keys of running children with a given profile path
pub async fn running_keys_with_profile( pub async fn running_keys_with_profile(
&self, &self,
profile_path: &Path, profile_path: ProfilePathId,
) -> crate::Result<Vec<Uuid>> { ) -> crate::Result<Vec<Uuid>> {
let running_keys = self.running_keys().await?; let running_keys = self.running_keys().await?;
let mut keys = Vec::new(); let mut keys = Vec::new();
@@ -274,7 +274,7 @@ impl Children {
if let Some(child) = self.get(&key) { if let Some(child) = self.get(&key) {
let child = child.clone(); let child = child.clone();
let child = child.read().await; let child = child.read().await;
if child.profile_path == profile_path { if child.profile_relative_path == profile_path {
keys.push(key); keys.push(key);
} }
} }
@@ -283,7 +283,9 @@ impl Children {
} }
// Gets all profiles of running children // Gets all profiles of running children
pub async fn running_profile_paths(&self) -> crate::Result<Vec<PathBuf>> { pub async fn running_profile_paths(
&self,
) -> crate::Result<Vec<ProfilePathId>> {
let mut profiles = Vec::new(); let mut profiles = Vec::new();
for key in self.keys() { for key in self.keys() {
if let Some(child) = self.get(&key) { if let Some(child) = self.get(&key) {
@@ -297,7 +299,7 @@ impl Children {
.map_err(IOError::from)? .map_err(IOError::from)?
.is_none() .is_none()
{ {
profiles.push(child.profile_path.clone()); profiles.push(child.profile_relative_path.clone());
} }
} }
} }
@@ -321,7 +323,7 @@ impl Children {
.is_none() .is_none()
{ {
if let Some(prof) = crate::api::profile::get( if let Some(prof) = crate::api::profile::get(
&child.profile_path.clone(), &child.profile_relative_path.clone(),
None, None,
) )
.await? .await?

View File

@@ -2,16 +2,42 @@
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::sync::RwLock;
use super::{ProfilePathId, Settings};
pub const SETTINGS_FILE_NAME: &str = "settings.json";
pub const CACHES_FOLDER_NAME: &str = "caches";
pub const LAUNCHER_LOGS_FOLDER_NAME: &str = "launcher_logs";
#[derive(Debug)] #[derive(Debug)]
pub struct DirectoryInfo { pub struct DirectoryInfo {
pub config_dir: PathBuf, pub settings_dir: PathBuf, // Base settings directory- settings.json and icon cache.
pub config_dir: RwLock<PathBuf>, // Base config directory- instances, minecraft downloads, etc. Changeable as a setting.
pub working_dir: PathBuf, pub working_dir: PathBuf,
} }
impl DirectoryInfo { impl DirectoryInfo {
// Get the settings directory
// init() is not needed for this function
pub fn get_initial_settings_dir() -> Option<PathBuf> {
Self::env_path("THESEUS_CONFIG_DIR")
.or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus")))
}
#[inline]
pub fn get_initial_settings_file() -> crate::Result<PathBuf> {
let settings_dir = Self::get_initial_settings_dir().ok_or(
crate::ErrorKind::FSError(
"Could not find valid config dir".to_string(),
),
)?;
Ok(settings_dir.join("settings.json"))
}
/// Get all paths needed for Theseus to operate properly /// Get all paths needed for Theseus to operate properly
#[tracing::instrument] #[tracing::instrument]
pub fn init() -> crate::Result<Self> { pub fn init(settings: &Settings) -> crate::Result<Self> {
// Working directory // Working directory
let working_dir = std::env::current_dir().map_err(|err| { let working_dir = std::env::current_dir().map_err(|err| {
crate::ErrorKind::FSError(format!( crate::ErrorKind::FSError(format!(
@@ -19,143 +45,153 @@ impl DirectoryInfo {
)) ))
})?; })?;
// Config directory let settings_dir = Self::get_initial_settings_dir().ok_or(
let config_dir = Self::env_path("THESEUS_CONFIG_DIR") crate::ErrorKind::FSError(
.or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus"))) "Could not find valid settings dir".to_string(),
.ok_or(crate::ErrorKind::FSError( ),
"Could not find valid config dir".to_string(), )?;
))?;
fs::create_dir_all(&config_dir).map_err(|err| { fs::create_dir_all(&settings_dir).map_err(|err| {
crate::ErrorKind::FSError(format!( crate::ErrorKind::FSError(format!(
"Error creating Theseus config directory: {err}" "Error creating Theseus config directory: {err}"
)) ))
})?; })?;
// config directory (for instances, etc.)
// by default this is the same as the settings directory
let config_dir = settings.loaded_config_dir.clone().ok_or(
crate::ErrorKind::FSError(
"Could not find valid config dir".to_string(),
),
)?;
Ok(Self { Ok(Self {
config_dir, settings_dir,
config_dir: RwLock::new(config_dir),
working_dir, working_dir,
}) })
} }
/// Get the Minecraft instance metadata directory /// Get the Minecraft instance metadata directory
#[inline] #[inline]
pub fn metadata_dir(&self) -> PathBuf { pub async fn metadata_dir(&self) -> PathBuf {
self.config_dir.join("meta") self.config_dir.read().await.join("meta")
} }
/// Get the Minecraft java versions metadata directory /// Get the Minecraft java versions metadata directory
#[inline] #[inline]
pub fn java_versions_dir(&self) -> PathBuf { pub async fn java_versions_dir(&self) -> PathBuf {
self.metadata_dir().join("java_versions") self.metadata_dir().await.join("java_versions")
} }
/// Get the Minecraft versions metadata directory /// Get the Minecraft versions metadata directory
#[inline] #[inline]
pub fn versions_dir(&self) -> PathBuf { pub async fn versions_dir(&self) -> PathBuf {
self.metadata_dir().join("versions") self.metadata_dir().await.join("versions")
} }
/// Get the metadata directory for a given version /// Get the metadata directory for a given version
#[inline] #[inline]
pub fn version_dir(&self, version: &str) -> PathBuf { pub async fn version_dir(&self, version: &str) -> PathBuf {
self.versions_dir().join(version) self.versions_dir().await.join(version)
} }
/// Get the Minecraft libraries metadata directory /// Get the Minecraft libraries metadata directory
#[inline] #[inline]
pub fn libraries_dir(&self) -> PathBuf { pub async fn libraries_dir(&self) -> PathBuf {
self.metadata_dir().join("libraries") self.metadata_dir().await.join("libraries")
} }
/// Get the Minecraft assets metadata directory /// Get the Minecraft assets metadata directory
#[inline] #[inline]
pub fn assets_dir(&self) -> PathBuf { pub async fn assets_dir(&self) -> PathBuf {
self.metadata_dir().join("assets") self.metadata_dir().await.join("assets")
} }
/// Get the assets index directory /// Get the assets index directory
#[inline] #[inline]
pub fn assets_index_dir(&self) -> PathBuf { pub async fn assets_index_dir(&self) -> PathBuf {
self.assets_dir().join("indexes") self.assets_dir().await.join("indexes")
} }
/// Get the assets objects directory /// Get the assets objects directory
#[inline] #[inline]
pub fn objects_dir(&self) -> PathBuf { pub async fn objects_dir(&self) -> PathBuf {
self.assets_dir().join("objects") self.assets_dir().await.join("objects")
} }
/// Get the directory for a specific object /// Get the directory for a specific object
#[inline] #[inline]
pub fn object_dir(&self, hash: &str) -> PathBuf { pub async fn object_dir(&self, hash: &str) -> PathBuf {
self.objects_dir().join(&hash[..2]).join(hash) self.objects_dir().await.join(&hash[..2]).join(hash)
} }
/// Get the Minecraft legacy assets metadata directory /// Get the Minecraft legacy assets metadata directory
#[inline] #[inline]
pub fn legacy_assets_dir(&self) -> PathBuf { pub async fn legacy_assets_dir(&self) -> PathBuf {
self.metadata_dir().join("resources") self.metadata_dir().await.join("resources")
} }
/// Get the Minecraft legacy assets metadata directory /// Get the Minecraft legacy assets metadata directory
#[inline] #[inline]
pub fn natives_dir(&self) -> PathBuf { pub async fn natives_dir(&self) -> PathBuf {
self.metadata_dir().join("natives") self.metadata_dir().await.join("natives")
} }
/// Get the natives directory for a version of Minecraft /// Get the natives directory for a version of Minecraft
#[inline] #[inline]
pub fn version_natives_dir(&self, version: &str) -> PathBuf { pub async fn version_natives_dir(&self, version: &str) -> PathBuf {
self.natives_dir().join(version) self.natives_dir().await.join(version)
} }
/// Get the directory containing instance icons /// Get the directory containing instance icons
#[inline] #[inline]
pub fn icon_dir(&self) -> PathBuf { pub async fn icon_dir(&self) -> PathBuf {
self.config_dir.join("icons") self.config_dir.read().await.join("icons")
} }
/// Get the profiles directory for created profiles /// Get the profiles directory for created profiles
#[inline] #[inline]
pub fn profiles_dir(&self) -> PathBuf { pub async fn profiles_dir(&self) -> PathBuf {
self.config_dir.join("profiles") self.config_dir.read().await.join("profiles")
} }
/// Gets the logs dir for a given profile /// Gets the logs dir for a given profile
#[inline] #[inline]
pub fn profile_logs_dir(&self, profile: uuid::Uuid) -> PathBuf { pub async fn profile_logs_dir(
self.profiles_dir() &self,
.join(profile.to_string()) profile_id: &ProfilePathId,
.join("modrinth_logs") ) -> crate::Result<PathBuf> {
Ok(profile_id.get_full_path().await?.join("modrinth_logs"))
} }
#[inline] #[inline]
pub fn launcher_logs_dir(&self) -> PathBuf { pub fn launcher_logs_dir() -> Option<PathBuf> {
self.config_dir.join("launcher_logs") Self::get_initial_settings_dir()
.map(|d| d.join(LAUNCHER_LOGS_FOLDER_NAME))
} }
/// Get the file containing the global database /// Get the file containing the global database
#[inline] #[inline]
pub fn database_file(&self) -> PathBuf { pub async fn database_file(&self) -> PathBuf {
self.config_dir.join("data.bin") self.config_dir.read().await.join("data.bin")
} }
/// Get the settings file for Theseus /// Get the settings file for Theseus
#[inline] #[inline]
pub fn settings_file(&self) -> PathBuf { pub fn settings_file(&self) -> PathBuf {
self.config_dir.join("settings.json") self.settings_dir.join(SETTINGS_FILE_NAME)
} }
/// Get the cache directory for Theseus /// Get the cache directory for Theseus
#[inline] #[inline]
pub fn caches_dir(&self) -> PathBuf { pub fn caches_dir(&self) -> PathBuf {
self.config_dir.join("caches") self.settings_dir.join(CACHES_FOLDER_NAME)
} }
#[inline] #[inline]
pub fn caches_meta_dir(&self) -> PathBuf { pub async fn caches_meta_dir(&self) -> PathBuf {
self.config_dir.join("caches").join("metadata") self.caches_dir().join("metadata")
} }
/// Get path from environment variable /// Get path from environment variable

View File

@@ -35,6 +35,10 @@ impl JavaGlobals {
self.0.len() self.0.len()
} }
pub fn keys(&self) -> Vec<String> {
self.0.keys().cloned().collect()
}
// Validates that every path here is a valid Java version and that the version matches the version stored here // Validates that every path here is a valid Java version and that the version matches the version stored here
// If false, when checked, the user should be prompted to reselect the Java version // If false, when checked, the user should be prompted to reselect the Java version
pub async fn is_all_valid(&self) -> bool { pub async fn is_all_valid(&self) -> bool {

View File

@@ -61,7 +61,7 @@ impl Metadata {
io_semaphore: &IoSemaphore, io_semaphore: &IoSemaphore,
) -> crate::Result<Self> { ) -> crate::Result<Self> {
let mut metadata = None; let mut metadata = None;
let metadata_path = dirs.caches_meta_dir().join("metadata.json"); let metadata_path = dirs.caches_meta_dir().await.join("metadata.json");
if let Ok(metadata_json) = if let Ok(metadata_json) =
read_json::<Metadata>(&metadata_path, io_semaphore).await read_json::<Metadata>(&metadata_path, io_semaphore).await
@@ -106,8 +106,11 @@ impl Metadata {
let metadata_fetch = Metadata::fetch().await?; let metadata_fetch = Metadata::fetch().await?;
let state = State::get().await?; let state = State::get().await?;
let metadata_path = let metadata_path = state
state.directories.caches_meta_dir().join("metadata.json"); .directories
.caches_meta_dir()
.await
.join("metadata.json");
write( write(
&metadata_path, &metadata_path,

View File

@@ -49,7 +49,8 @@ mod safe_processes;
pub use self::safe_processes::*; pub use self::safe_processes::*;
// Global state // Global state
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new(); // RwLock on state only has concurrent reads, except for config dir change which takes control of the State
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
pub struct State { pub struct State {
/// Information on the location of files used in the launcher /// Information on the location of files used in the launcher
pub directories: DirectoryInfo, pub directories: DirectoryInfo,
@@ -86,83 +87,97 @@ pub struct State {
impl State { impl State {
/// Get the current launcher state, initializing it if needed /// Get the current launcher state, initializing it if needed
pub async fn get(
) -> crate::Result<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
Ok(Arc::new(
LAUNCHER_STATE
.get_or_try_init(Self::initialize_state)
.await?
.read()
.await,
))
}
/// Get the current launcher state, initializing it if needed
/// Takes writing control of the state, blocking all other uses of it
/// Only used for state change such as changing the config directory
pub async fn get_write(
) -> crate::Result<tokio::sync::RwLockWriteGuard<'static, Self>> {
Ok(LAUNCHER_STATE
.get_or_try_init(Self::initialize_state)
.await?
.write()
.await)
}
#[tracing::instrument] #[tracing::instrument]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn get() -> crate::Result<Arc<Self>> { async fn initialize_state() -> crate::Result<RwLock<State>> {
LAUNCHER_STATE let loading_bar = init_loading_unsafe(
.get_or_try_init(|| { LoadingBarType::StateInit,
async { 100.0,
let loading_bar = init_loading_unsafe( "Initializing launcher",
LoadingBarType::StateInit, )
100.0, .await?;
"Initializing launcher",
)
.await?;
let mut file_watcher = init_watcher().await?; // Settings
let settings =
Settings::init(&DirectoryInfo::get_initial_settings_file()?)
.await?;
let directories = DirectoryInfo::init()?; let directories = DirectoryInfo::init(&settings)?;
emit_loading(&loading_bar, 10.0, None).await?;
// Settings emit_loading(&loading_bar, 10.0, None).await?;
let settings =
Settings::init(&directories.settings_file()).await?;
let fetch_semaphore = FetchSemaphore(RwLock::new(
Semaphore::new(settings.max_concurrent_downloads),
));
let io_semaphore = IoSemaphore(RwLock::new(
Semaphore::new(settings.max_concurrent_writes),
));
emit_loading(&loading_bar, 10.0, None).await?;
let metadata_fut = let mut file_watcher = init_watcher().await?;
Metadata::init(&directories, &io_semaphore);
let profiles_fut =
Profiles::init(&directories, &mut file_watcher);
let tags_fut = Tags::init(
&directories,
&io_semaphore,
&fetch_semaphore,
);
let users_fut = Users::init(&directories, &io_semaphore);
// Launcher data
let (metadata, profiles, tags, users) = loading_join! {
Some(&loading_bar), 70.0, Some("Loading metadata");
metadata_fut,
profiles_fut,
tags_fut,
users_fut,
}?;
let children = Children::new(); let fetch_semaphore = FetchSemaphore(RwLock::new(Semaphore::new(
let auth_flow = AuthTask::new(); settings.max_concurrent_downloads,
let safety_processes = SafeProcesses::new(); )));
emit_loading(&loading_bar, 10.0, None).await?; let io_semaphore = IoSemaphore(RwLock::new(Semaphore::new(
settings.max_concurrent_writes,
)));
emit_loading(&loading_bar, 10.0, None).await?;
Ok(Arc::new(Self { let metadata_fut = Metadata::init(&directories, &io_semaphore);
directories, let profiles_fut = Profiles::init(&directories, &mut file_watcher);
fetch_semaphore, let tags_fut =
fetch_semaphore_max: RwLock::new( Tags::init(&directories, &io_semaphore, &fetch_semaphore);
settings.max_concurrent_downloads as u32, let users_fut = Users::init(&directories, &io_semaphore);
), // Launcher data
io_semaphore, let (metadata, profiles, tags, users) = loading_join! {
io_semaphore_max: RwLock::new( Some(&loading_bar), 70.0, Some("Loading metadata");
settings.max_concurrent_writes as u32, metadata_fut,
), profiles_fut,
metadata: RwLock::new(metadata), tags_fut,
settings: RwLock::new(settings), users_fut,
profiles: RwLock::new(profiles), }?;
users: RwLock::new(users),
children: RwLock::new(children), let children = Children::new();
auth_flow: RwLock::new(auth_flow), let auth_flow = AuthTask::new();
tags: RwLock::new(tags), let safety_processes = SafeProcesses::new();
safety_processes: RwLock::new(safety_processes), emit_loading(&loading_bar, 10.0, None).await?;
file_watcher: RwLock::new(file_watcher),
})) Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self {
} directories,
}) fetch_semaphore,
.await fetch_semaphore_max: RwLock::new(
.map(Arc::clone) settings.max_concurrent_downloads as u32,
),
io_semaphore,
io_semaphore_max: RwLock::new(
settings.max_concurrent_writes as u32,
),
metadata: RwLock::new(metadata),
settings: RwLock::new(settings),
profiles: RwLock::new(profiles),
users: RwLock::new(users),
children: RwLock::new(children),
auth_flow: RwLock::new(auth_flow),
tags: RwLock::new(tags),
safety_processes: RwLock::new(safety_processes),
file_watcher: RwLock::new(file_watcher),
}))
} }
/// Updates state with data from the web /// Updates state with data from the web
@@ -240,7 +255,7 @@ impl State {
} }
} }
async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> { pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
let (mut tx, mut rx) = channel(1); let (mut tx, mut rx) = channel(1);
let file_watcher = new_debouncer( let file_watcher = new_debouncer(
@@ -256,13 +271,19 @@ async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
tokio::task::spawn(async move { tokio::task::spawn(async move {
while let Some(res) = rx.next().await { while let Some(res) = rx.next().await {
match res { match res {
Ok(events) => { Ok(mut events) => {
let mut visited_paths = Vec::new(); let mut visited_paths = Vec::new();
// sort events by e.path
events.sort_by(|a, b| a.path.cmp(&b.path));
events.iter().for_each(|e| { events.iter().for_each(|e| {
tracing::debug!(
"File watcher event: {:?}",
serde_json::to_string(&e.path).unwrap()
);
let mut new_path = PathBuf::new(); let mut new_path = PathBuf::new();
let mut components_iterator = e.path.components();
let mut found = false; let mut found = false;
for component in components_iterator.by_ref() {
for component in e.path.components() {
new_path.push(component); new_path.push(component);
if found { if found {
break; break;
@@ -271,6 +292,14 @@ async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
found = true; found = true;
} }
} }
// if any remain, it's a subfile of the profile folder and not the profile folder itself
let subfile = components_iterator.next().is_some();
// At this point, new_path is the path to the profile, and subfile is whether it's a subfile of the profile or not
let profile_path_id =
ProfilePathId::new(&PathBuf::from(
new_path.file_name().unwrap_or_default(),
));
if e.path if e.path
.components() .components()
@@ -280,10 +309,16 @@ async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
.map(|x| x == "txt") .map(|x| x == "txt")
.unwrap_or(false) .unwrap_or(false)
{ {
Profile::crash_task(new_path); Profile::crash_task(profile_path_id);
} else if !visited_paths.contains(&new_path) { } else if !visited_paths.contains(&new_path) {
Profile::sync_projects_task(new_path.clone()); if subfile {
visited_paths.push(new_path); Profile::sync_projects_task(profile_path_id);
visited_paths.push(new_path);
} else {
Profiles::sync_available_profiles_task(
profile_path_id,
);
}
} }
}); });
} }

View File

@@ -28,7 +28,7 @@ use uuid::Uuid;
const PROFILE_JSON_PATH: &str = "profile.json"; const PROFILE_JSON_PATH: &str = "profile.json";
pub(crate) struct Profiles(pub HashMap<PathBuf, Profile>); pub(crate) struct Profiles(pub HashMap<ProfilePathId, Profile>);
#[derive( #[derive(
Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq,
@@ -46,13 +46,101 @@ pub enum ProfileInstallStage {
NotInstalled, NotInstalled,
} }
/// newtype wrapper over a Profile path, to be usable as a clear identifier for the kind of path used
/// eg: for "a/b/c/profiles/My Mod", the ProfilePathId would be "My Mod" (a relative path)
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
#[serde(transparent)]
pub struct ProfilePathId(PathBuf);
impl ProfilePathId {
// Create a new ProfilePathId from a full file path
pub async fn from_fs_path(path: PathBuf) -> crate::Result<Self> {
let path: PathBuf = io::canonicalize(path)?;
let profiles_dir = io::canonicalize(
State::get().await?.directories.profiles_dir().await,
)?;
path.strip_prefix(profiles_dir)
.ok()
.and_then(|p| p.file_name())
.ok_or_else(|| {
crate::ErrorKind::FSError(format!(
"Path {path:?} does not correspond to a profile",
path = path
))
})?;
Ok(Self(path))
}
// Create a new ProfilePathId from a relative path
pub fn new(path: &Path) -> Self {
ProfilePathId(PathBuf::from(path))
}
pub async fn get_full_path(&self) -> crate::Result<PathBuf> {
let state = State::get().await?;
let profiles_dir = state.directories.profiles_dir().await;
Ok(profiles_dir.join(&self.0))
}
pub fn check_valid_utf(&self) -> crate::Result<&Self> {
self.0
.to_str()
.ok_or(crate::ErrorKind::UTFError(self.0.clone()).as_error())?;
Ok(self)
}
}
impl std::fmt::Display for ProfilePathId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.display().fmt(f)
}
}
/// newtype wrapper over a Profile path, to be usable as a clear identifier for the kind of path used
/// eg: for "a/b/c/profiles/My Mod/mods/myproj", the ProjectPathId would be "mods/myproj"
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
#[serde(transparent)]
pub struct ProjectPathId(pub PathBuf);
impl ProjectPathId {
// Create a new ProjectPathId from a full file path
pub async fn from_fs_path(path: PathBuf) -> crate::Result<Self> {
let path: PathBuf = io::canonicalize(path)?;
let profiles_dir: PathBuf = io::canonicalize(
State::get().await?.directories.profiles_dir().await,
)?;
path.strip_prefix(profiles_dir)
.ok()
.map(|p| p.components().skip(1).collect::<PathBuf>())
.ok_or_else(|| {
crate::ErrorKind::FSError(format!(
"Path {path:?} does not correspond to a profile",
path = path
))
})?;
Ok(Self(path))
}
pub async fn get_full_path(
&self,
profile: ProfilePathId,
) -> crate::Result<PathBuf> {
let _state = State::get().await?;
let profile_dir = profile.get_full_path().await?;
Ok(profile_dir.join(&self.0))
}
// Create a new ProjectPathId from a relative path
pub fn new(path: &Path) -> Self {
ProjectPathId(PathBuf::from(path))
}
}
// Represent a Minecraft instance. // Represent a Minecraft instance.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Profile { pub struct Profile {
pub uuid: Uuid, // todo: will be used in restructure to refer to profiles pub uuid: Uuid, // todo: will be used in restructure to refer to profiles
#[serde(default)] #[serde(default)]
pub install_stage: ProfileInstallStage, pub install_stage: ProfileInstallStage,
pub path: PathBuf, #[serde(default)]
pub path: PathBuf, // Relative path to the profile, to be used in ProfilePathId
pub metadata: ProfileMetadata, pub metadata: ProfileMetadata,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub java: Option<JavaSettings>, pub java: Option<JavaSettings>,
@@ -62,7 +150,7 @@ pub struct Profile {
pub resolution: Option<WindowSize>, pub resolution: Option<WindowSize>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<Hooks>, pub hooks: Option<Hooks>,
pub projects: HashMap<PathBuf, Project>, pub projects: HashMap<ProjectPathId, Project>,
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
@@ -148,7 +236,6 @@ impl Profile {
uuid: Uuid, uuid: Uuid,
name: String, name: String,
version: String, version: String,
path: PathBuf,
) -> crate::Result<Self> { ) -> crate::Result<Self> {
if name.trim().is_empty() { if name.trim().is_empty() {
return Err(crate::ErrorKind::InputError(String::from( return Err(crate::ErrorKind::InputError(String::from(
@@ -160,7 +247,7 @@ impl Profile {
Ok(Self { Ok(Self {
uuid, uuid,
install_stage: ProfileInstallStage::NotInstalled, install_stage: ProfileInstallStage::NotInstalled,
path: io::canonicalize(path)?, path: PathBuf::new().join(&name),
metadata: ProfileMetadata { metadata: ProfileMetadata {
name, name,
icon: None, icon: None,
@@ -182,6 +269,12 @@ impl Profile {
}) })
} }
// Gets the ProfilePathId for this profile
#[inline]
pub fn profile_id(&self) -> ProfilePathId {
ProfilePathId::new(&self.path)
}
#[tracing::instrument(skip(self, semaphore, icon))] #[tracing::instrument(skip(self, semaphore, icon))]
pub async fn set_icon<'a>( pub async fn set_icon<'a>(
&'a mut self, &'a mut self,
@@ -197,7 +290,7 @@ impl Profile {
Ok(()) Ok(())
} }
pub fn crash_task(path: PathBuf) { pub fn crash_task(path: ProfilePathId) {
tokio::task::spawn(async move { tokio::task::spawn(async move {
let res = async { let res = async {
let profile = crate::api::profile::get(&path, None).await?; let profile = crate::api::profile::get(&path, None).await?;
@@ -221,38 +314,47 @@ impl Profile {
}); });
} }
pub fn sync_projects_task(path: PathBuf) { pub fn sync_projects_task(profile_path_id: ProfilePathId) {
tokio::task::spawn(async move { tokio::task::spawn(async move {
let span =
tracing::span!(tracing::Level::INFO, "sync_projects_task");
tracing::debug!(
parent: &span,
"Syncing projects for profile {}",
profile_path_id
);
let res = async { let res = async {
let _span = span.enter();
let state = State::get().await?; let state = State::get().await?;
let profile = crate::api::profile::get(&path, None).await?; let profile = crate::api::profile::get(&profile_path_id, None).await?;
if let Some(profile) = profile { if let Some(profile) = profile {
let paths = profile.get_profile_project_paths()?; let paths = profile.get_profile_full_project_paths().await?;
let caches_dir = state.directories.caches_dir();
let projects = crate::state::infer_data_from_files( let projects = crate::state::infer_data_from_files(
profile.clone(), profile.clone(),
paths, paths,
state.directories.caches_dir(), caches_dir,
&state.io_semaphore, &state.io_semaphore,
&state.fetch_semaphore, &state.fetch_semaphore,
) )
.await?; .await?;
let mut new_profiles = state.profiles.write().await; let mut new_profiles = state.profiles.write().await;
if let Some(profile) = new_profiles.0.get_mut(&path) { if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) {
profile.projects = projects; profile.projects = projects;
} }
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path, profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Synced, ProfilePayloadType::Synced,
) )
.await?; .await?;
} else { } else {
tracing::warn!( tracing::warn!(
"Unable to fetch single profile projects: path {path:?} invalid", "Unable to fetch single profile projects: path {profile_path_id} invalid",
); );
} }
Ok::<(), crate::Error>(()) Ok::<(), crate::Error>(())
@@ -268,10 +370,21 @@ impl Profile {
}); });
} }
pub fn get_profile_project_paths(&self) -> crate::Result<Vec<PathBuf>> { // Get full path to profile
pub async fn get_profile_full_path(&self) -> crate::Result<PathBuf> {
let state = State::get().await?;
let profiles_dir = state.directories.profiles_dir().await;
Ok(profiles_dir.join(&self.path))
}
/// Gets paths to projects as their full paths, not just their relative paths
pub async fn get_profile_full_project_paths(
&self,
) -> crate::Result<Vec<PathBuf>> {
let mut files = Vec::new(); let mut files = Vec::new();
let profile_path = self.get_profile_full_path().await?;
let mut read_paths = |path: &str| { let mut read_paths = |path: &str| {
let new_path = self.path.join(path); let new_path = profile_path.join(path);
if new_path.exists() { if new_path.exists() {
let path = self.path.join(path); let path = self.path.join(path);
for path in std::fs::read_dir(&path) for path in std::fs::read_dir(&path)
@@ -338,7 +451,7 @@ impl Profile {
pub async fn add_project_version( pub async fn add_project_version(
&self, &self,
version_id: String, version_id: String,
) -> crate::Result<(PathBuf, ModrinthVersion)> { ) -> crate::Result<(ProjectPathId, ModrinthVersion)> {
let state = State::get().await?; let state = State::get().await?;
let version = fetch_json::<ModrinthVersion>( let version = fetch_json::<ModrinthVersion>(
@@ -387,7 +500,7 @@ impl Profile {
file_name: &str, file_name: &str,
bytes: bytes::Bytes, bytes: bytes::Bytes,
project_type: Option<ProjectType>, project_type: Option<ProjectType>,
) -> crate::Result<PathBuf> { ) -> crate::Result<ProjectPathId> {
let project_type = if let Some(project_type) = project_type { let project_type = if let Some(project_type) = project_type {
project_type project_type
} else { } else {
@@ -419,16 +532,23 @@ impl Profile {
}; };
let state = State::get().await?; let state = State::get().await?;
let path = self.path.join(project_type.get_folder()).join(file_name); let relative_name = PathBuf::new()
write(&path, &bytes, &state.io_semaphore).await?; .join(project_type.get_folder())
.join(file_name);
let file_path = self
.get_profile_full_path()
.await?
.join(relative_name.clone());
let project_path_id = ProjectPathId::new(&relative_name);
write(&file_path, &bytes, &state.io_semaphore).await?;
let hash = get_hash(bytes).await?; let hash = get_hash(bytes).await?;
{ {
let mut profiles = state.profiles.write().await; let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.path) { if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
profile.projects.insert( profile.projects.insert(
path.clone(), project_path_id.clone(),
Project { Project {
sha512: hash, sha512: hash,
disabled: false, disabled: false,
@@ -440,32 +560,40 @@ impl Profile {
} }
} }
Ok(path) Ok(project_path_id)
} }
/// Toggle a project's disabled state.
/// 'path' should be relative to the profile's path.
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn toggle_disable_project( pub async fn toggle_disable_project(
&self, &self,
path: &Path, relative_path: &ProjectPathId,
) -> crate::Result<PathBuf> { ) -> crate::Result<ProjectPathId> {
let state = State::get().await?; let state = State::get().await?;
if let Some(mut project) = { if let Some(mut project) = {
let mut profiles = state.profiles.write().await; let mut profiles: tokio::sync::RwLockWriteGuard<'_, Profiles> =
state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.path) { if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
profile.projects.remove(path) profile.projects.remove(relative_path)
} else { } else {
None None
} }
} { } {
let path = path.to_path_buf(); // Get relative path from former ProjectPathId
let mut new_path = path.clone(); let relative_path = relative_path.0.to_path_buf();
let mut new_path = relative_path.clone();
if path.extension().map_or(false, |ext| ext == "disabled") { if relative_path
.extension()
.map_or(false, |ext| ext == "disabled")
{
project.disabled = false; project.disabled = false;
new_path.set_file_name( new_path.set_file_name(
path.file_name() relative_path
.file_name()
.unwrap_or_default() .unwrap_or_default()
.to_string_lossy() .to_string_lossy()
.replace(".disabled", ""), .replace(".disabled", ""),
@@ -473,24 +601,35 @@ impl Profile {
} else { } else {
new_path.set_file_name(format!( new_path.set_file_name(format!(
"{}.disabled", "{}.disabled",
path.file_name().unwrap_or_default().to_string_lossy() relative_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
)); ));
project.disabled = true; project.disabled = true;
} }
io::rename(&path, &new_path).await?; let true_path =
self.get_profile_full_path().await?.join(&relative_path);
let true_new_path =
self.get_profile_full_path().await?.join(&new_path);
io::rename(&true_path, &true_new_path).await?;
let new_project_path_id = ProjectPathId::new(&new_path);
let mut profiles = state.profiles.write().await; let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.path) { if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
profile.projects.insert(new_path.clone(), project); profile
.projects
.insert(new_project_path_id.clone(), project);
profile.metadata.date_modified = Utc::now(); profile.metadata.date_modified = Utc::now();
} }
Ok(new_path) Ok(new_project_path_id)
} else { } else {
Err(crate::ErrorKind::InputError(format!( Err(crate::ErrorKind::InputError(format!(
"Project path does not exist: {:?}", "Project path does not exist: {:?}",
path relative_path
)) ))
.into()) .into())
} }
@@ -498,24 +637,29 @@ impl Profile {
pub async fn remove_project( pub async fn remove_project(
&self, &self,
path: &Path, relative_path: &ProjectPathId,
dont_remove_arr: Option<bool>, dont_remove_arr: Option<bool>,
) -> crate::Result<()> { ) -> crate::Result<()> {
let state = State::get().await?; let state = State::get().await?;
if self.projects.contains_key(path) { if self.projects.contains_key(relative_path) {
io::remove_file(path).await?; io::remove_file(
self.get_profile_full_path()
.await?
.join(relative_path.0.clone()),
)
.await?;
if !dont_remove_arr.unwrap_or(false) { if !dont_remove_arr.unwrap_or(false) {
let mut profiles = state.profiles.write().await; let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.path) { if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
profile.projects.remove(path); profile.projects.remove(relative_path);
profile.metadata.date_modified = Utc::now(); profile.metadata.date_modified = Utc::now();
} }
} }
} else { } else {
return Err(crate::ErrorKind::InputError(format!( return Err(crate::ErrorKind::InputError(format!(
"Project path does not exist: {:?}", "Project path does not exist: {:?}",
path relative_path
)) ))
.into()); .into());
} }
@@ -532,14 +676,21 @@ impl Profiles {
file_watcher: &mut Debouncer<RecommendedWatcher>, file_watcher: &mut Debouncer<RecommendedWatcher>,
) -> crate::Result<Self> { ) -> crate::Result<Self> {
let mut profiles = HashMap::new(); let mut profiles = HashMap::new();
io::create_dir_all(&dirs.profiles_dir()).await?; let profiles_dir = dirs.profiles_dir().await;
let mut entries = io::read_dir(&dirs.profiles_dir()).await?; io::create_dir_all(&&profiles_dir).await?;
file_watcher
.watcher()
.watch(&profiles_dir, RecursiveMode::NonRecursive)?;
let mut entries = io::read_dir(&dirs.profiles_dir().await).await?;
while let Some(entry) = while let Some(entry) =
entries.next_entry().await.map_err(IOError::from)? entries.next_entry().await.map_err(IOError::from)?
{ {
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
let prof = match Self::read_profile_from_dir(&path).await { let prof = match Self::read_profile_from_dir(&path, dirs).await
{
Ok(prof) => Some(prof), Ok(prof) => Some(prof),
Err(err) => { Err(err) => {
tracing::warn!( tracing::warn!(
@@ -551,7 +702,7 @@ impl Profiles {
if let Some(profile) = prof { if let Some(profile) = prof {
let path = io::canonicalize(path)?; let path = io::canonicalize(path)?;
Profile::watch_fs(&path, file_watcher).await?; Profile::watch_fs(&path, file_watcher).await?;
profiles.insert(path, profile); profiles.insert(profile.profile_id(), profile);
} }
} }
} }
@@ -570,26 +721,28 @@ impl Profiles {
{ {
let profiles = state.profiles.read().await; let profiles = state.profiles.read().await;
for (_profile_path, profile) in profiles.0.iter() { for (_profile_path, profile) in profiles.0.iter() {
let paths = profile.get_profile_project_paths()?; let paths =
profile.get_profile_full_project_paths().await?;
files.push((profile.clone(), paths)); files.push((profile.clone(), paths));
} }
} }
let caches_dir = state.directories.caches_dir();
future::try_join_all(files.into_iter().map( future::try_join_all(files.into_iter().map(
|(profile, files)| async { |(profile, files)| async {
let profile_path = profile.path.clone(); let profile_name = profile.profile_id();
let inferred = super::projects::infer_data_from_files( let inferred = super::projects::infer_data_from_files(
profile, profile,
files, files,
state.directories.caches_dir(), caches_dir.clone(),
&state.io_semaphore, &state.io_semaphore,
&state.fetch_semaphore, &state.fetch_semaphore,
) )
.await?; .await?;
let mut new_profiles = state.profiles.write().await; let mut new_profiles = state.profiles.write().await;
if let Some(profile) = new_profiles.0.get_mut(&profile_path) if let Some(profile) = new_profiles.0.get_mut(&profile_name)
{ {
profile.projects = inferred; profile.projects = inferred;
} }
@@ -622,7 +775,7 @@ impl Profiles {
pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> { pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
emit_profile( emit_profile(
profile.uuid, profile.uuid,
profile.path.clone(), profile.get_profile_full_path().await?,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Added, ProfilePayloadType::Added,
) )
@@ -630,30 +783,26 @@ impl Profiles {
let state = State::get().await?; let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await; let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(&profile.path, &mut file_watcher).await?; Profile::watch_fs(
&profile.get_profile_full_path().await?,
&mut file_watcher,
)
.await?;
self.0.insert( let profile_name = profile.profile_id();
io::canonicalize(&profile.path)? profile_name.check_valid_utf()?;
.to_str() self.0.insert(profile_name, profile);
.ok_or(
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
)?
.into(),
profile,
);
Ok(self) Ok(self)
} }
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub async fn remove( pub async fn remove(
&mut self, &mut self,
path: &Path, profile_path: &ProfilePathId,
) -> crate::Result<Option<Profile>> { ) -> crate::Result<Option<Profile>> {
let path = PathBuf::from( let profile = self.0.remove(profile_path);
&io::canonicalize(path)?.to_string_lossy().to_string(),
);
let profile = self.0.remove(&path);
let path = profile_path.get_full_path().await?;
if path.exists() { if path.exists() {
io::remove_dir_all(&path).await?; io::remove_dir_all(&path).await?;
} }
@@ -663,12 +812,15 @@ impl Profiles {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn sync(&self) -> crate::Result<&Self> { pub async fn sync(&self) -> crate::Result<&Self> {
let _state = State::get().await?;
stream::iter(self.0.iter()) stream::iter(self.0.iter())
.map(Ok::<_, crate::Error>) .map(Ok::<_, crate::Error>)
.try_for_each_concurrent(None, |(path, profile)| async move { .try_for_each_concurrent(None, |(_, profile)| async move {
let json = serde_json::to_vec(&profile)?; let json = serde_json::to_vec(&profile)?;
let json_path = Path::new(&path.to_string_lossy().to_string()) let json_path = profile
.get_profile_full_path()
.await?
.join(PROFILE_JSON_PATH); .join(PROFILE_JSON_PATH);
io::write(&json_path, &json).await?; io::write(&json_path, &json).await?;
@@ -679,10 +831,68 @@ impl Profiles {
Ok(self) Ok(self)
} }
async fn read_profile_from_dir(path: &Path) -> crate::Result<Profile> { async fn read_profile_from_dir(
path: &Path,
dirs: &DirectoryInfo,
) -> crate::Result<Profile> {
let json = io::read(&path.join(PROFILE_JSON_PATH)).await?; let json = io::read(&path.join(PROFILE_JSON_PATH)).await?;
let mut profile = serde_json::from_slice::<Profile>(&json)?; let mut profile = serde_json::from_slice::<Profile>(&json)?;
profile.path = PathBuf::from(path);
// Get name from stripped path
profile.path =
PathBuf::from(path.strip_prefix(dirs.profiles_dir().await)?);
Ok(profile) Ok(profile)
} }
pub fn sync_available_profiles_task(profile_path_id: ProfilePathId) {
tokio::task::spawn(async move {
let span = tracing::span!(
tracing::Level::INFO,
"sync_available_profiles_task"
);
let res = async {
let _span = span.enter();
let state = State::get().await?;
let dirs = &state.directories;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&profile_path_id) {
if !profile.get_profile_full_path().await?.exists() {
// if path exists in the state but no longer in the filesystem, remove it from the state list
emit_profile(
profile.uuid,
profile.get_profile_full_path().await?,
&profile.metadata.name,
ProfilePayloadType::Removed,
)
.await?;
tracing::debug!("Removed!");
profiles.0.remove(&profile_path_id);
}
} else if profile_path_id.get_full_path().await?.exists() {
// if it exists in the filesystem but no longer in the state, add it to the state list
profiles
.insert(
Self::read_profile_from_dir(
&profile_path_id.get_full_path().await?,
dirs,
)
.await?,
)
.await?;
Profile::sync_projects_task(profile_path_id);
}
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
tracing::warn!("Unable to fetch all profiles: {err}")
}
};
});
}
} }

View File

@@ -8,6 +8,7 @@ use crate::util::fetch::{
use crate::util::io::IOError; use crate::util::io::IOError;
use async_zip::tokio::read::fs::ZipFileReader; use async_zip::tokio::read::fs::ZipFileReader;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use futures::StreamExt;
use reqwest::Method; use reqwest::Method;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
@@ -16,6 +17,8 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use super::ProjectPathId;
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum ProjectType { pub enum ProjectType {
@@ -253,6 +256,9 @@ async fn read_icon_from_file(
Ok(None) Ok(None)
} }
// Creates Project data from the existing files in the file system, for a given Profile
// Paths must be the full paths to the files in the FS, and not the relative paths
// eg: with get_profile_full_project_paths
#[tracing::instrument(skip(paths, profile, io_semaphore, fetch_semaphore))] #[tracing::instrument(skip(paths, profile, io_semaphore, fetch_semaphore))]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn infer_data_from_files( pub async fn infer_data_from_files(
@@ -261,7 +267,7 @@ pub async fn infer_data_from_files(
cache_dir: PathBuf, cache_dir: PathBuf,
io_semaphore: &IoSemaphore, io_semaphore: &IoSemaphore,
fetch_semaphore: &FetchSemaphore, fetch_semaphore: &FetchSemaphore,
) -> crate::Result<HashMap<PathBuf, Project>> { ) -> crate::Result<HashMap<ProjectPathId, Project>> {
let mut file_path_hashes = HashMap::new(); let mut file_path_hashes = HashMap::new();
// TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory // TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory
@@ -342,7 +348,7 @@ pub async fn infer_data_from_files(
.flatten() .flatten()
.collect(); .collect();
let mut return_projects = HashMap::new(); let mut return_projects: Vec<(PathBuf, Project)> = Vec::new();
let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new(); let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new();
for (hash, path) in file_path_hashes { for (hash, path) in file_path_hashes {
@@ -356,7 +362,7 @@ pub async fn infer_data_from_files(
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
return_projects.insert( return_projects.push((
path, path,
Project { Project {
disabled: file_name.ends_with(".disabled"), disabled: file_name.ends_with(".disabled"),
@@ -392,7 +398,7 @@ pub async fn infer_data_from_files(
sha512: hash, sha512: hash,
file_name, file_name,
}, },
); ));
continue; continue;
} }
} }
@@ -412,7 +418,7 @@ pub async fn infer_data_from_files(
{ {
zip_file_reader zip_file_reader
} else { } else {
return_projects.insert( return_projects.push((
path.clone(), path.clone(),
Project { Project {
sha512: hash, sha512: hash,
@@ -420,7 +426,7 @@ pub async fn infer_data_from_files(
metadata: ProjectMetadata::Unknown, metadata: ProjectMetadata::Unknown,
file_name, file_name,
}, },
); ));
continue; continue;
}; };
let zip_index_option = zip_file_reader let zip_index_option = zip_file_reader
@@ -466,7 +472,7 @@ pub async fn infer_data_from_files(
) )
.await?; .await?;
return_projects.insert( return_projects.push((
path.clone(), path.clone(),
Project { Project {
sha512: hash, sha512: hash,
@@ -491,7 +497,7 @@ pub async fn infer_data_from_files(
project_type: Some("mod".to_string()), project_type: Some("mod".to_string()),
}, },
}, },
); ));
continue; continue;
} }
} }
@@ -533,7 +539,7 @@ pub async fn infer_data_from_files(
) )
.await?; .await?;
return_projects.insert( return_projects.push((
path.clone(), path.clone(),
Project { Project {
sha512: hash, sha512: hash,
@@ -552,7 +558,7 @@ pub async fn infer_data_from_files(
project_type: Some("mod".to_string()), project_type: Some("mod".to_string()),
}, },
}, },
); ));
continue; continue;
} }
} }
@@ -599,7 +605,7 @@ pub async fn infer_data_from_files(
) )
.await?; .await?;
return_projects.insert( return_projects.push((
path.clone(), path.clone(),
Project { Project {
sha512: hash, sha512: hash,
@@ -621,7 +627,7 @@ pub async fn infer_data_from_files(
project_type: Some("mod".to_string()), project_type: Some("mod".to_string()),
}, },
}, },
); ));
continue; continue;
} }
} }
@@ -665,7 +671,7 @@ pub async fn infer_data_from_files(
) )
.await?; .await?;
return_projects.insert( return_projects.push((
path.clone(), path.clone(),
Project { Project {
sha512: hash, sha512: hash,
@@ -697,7 +703,7 @@ pub async fn infer_data_from_files(
project_type: Some("mod".to_string()), project_type: Some("mod".to_string()),
}, },
}, },
); ));
continue; continue;
} }
} }
@@ -731,7 +737,7 @@ pub async fn infer_data_from_files(
io_semaphore, io_semaphore,
) )
.await?; .await?;
return_projects.insert( return_projects.push((
path.clone(), path.clone(),
Project { Project {
sha512: hash, sha512: hash,
@@ -746,13 +752,13 @@ pub async fn infer_data_from_files(
project_type: None, project_type: None,
}, },
}, },
); ));
continue; continue;
} }
} }
} }
return_projects.insert( return_projects.push((
path.clone(), path.clone(),
Project { Project {
sha512: hash, sha512: hash,
@@ -760,8 +766,17 @@ pub async fn infer_data_from_files(
file_name, file_name,
metadata: ProjectMetadata::Unknown, metadata: ProjectMetadata::Unknown,
}, },
); ));
} }
Ok(return_projects) // Project paths should be relative
let _profile_base_path = profile.get_profile_full_path().await?;
let mut corrected_hashmap = HashMap::new();
let mut stream = tokio_stream::iter(return_projects);
while let Some((h, v)) = stream.next().await {
let h = ProjectPathId::from_fs_path(h).await?;
corrected_hashmap.insert(h, v);
}
Ok(corrected_hashmap)
} }

View File

@@ -4,10 +4,10 @@ use crate::{
State, State,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::{Path, PathBuf};
use tokio::fs; use tokio::fs;
use super::JavaGlobals; use super::{DirectoryInfo, JavaGlobals};
// TODO: convert to semver? // TODO: convert to semver?
const CURRENT_FORMAT_VERSION: u32 = 1; const CURRENT_FORMAT_VERSION: u32 = 1;
@@ -15,7 +15,6 @@ const CURRENT_FORMAT_VERSION: u32 = 1;
// Types // Types
/// Global Theseus settings /// Global Theseus settings
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(default)]
pub struct Settings { pub struct Settings {
pub theme: Theme, pub theme: Theme,
pub memory: MemorySettings, pub memory: MemorySettings,
@@ -41,31 +40,8 @@ pub struct Settings {
pub advanced_rendering: bool, pub advanced_rendering: bool,
#[serde(default)] #[serde(default)]
pub onboarded: bool, pub onboarded: bool,
} #[serde(default = "DirectoryInfo::get_initial_settings_dir")]
pub loaded_config_dir: Option<PathBuf>,
impl Default for Settings {
fn default() -> Self {
Self {
theme: Theme::Dark,
memory: MemorySettings::default(),
game_resolution: WindowSize::default(),
custom_java_args: Vec::new(),
custom_env_args: Vec::new(),
java_globals: JavaGlobals::new(),
default_user: None,
hooks: Hooks::default(),
max_concurrent_downloads: 10,
max_concurrent_writes: 10,
version: CURRENT_FORMAT_VERSION,
collapsed_navigation: false,
hide_on_process: false,
default_page: DefaultPage::Home,
developer_mode: false,
opt_out_analytics: false,
advanced_rendering: true,
onboarded: false,
}
}
} }
impl Settings { impl Settings {
@@ -85,7 +61,29 @@ impl Settings {
.map_err(crate::Error::from) .map_err(crate::Error::from)
}) })
} else { } else {
Ok(Settings::default()) Ok(Self {
theme: Theme::Dark,
memory: MemorySettings::default(),
game_resolution: WindowSize::default(),
custom_java_args: Vec::new(),
custom_env_args: Vec::new(),
java_globals: JavaGlobals::new(),
default_user: None,
hooks: Hooks::default(),
max_concurrent_downloads: 10,
max_concurrent_writes: 10,
version: CURRENT_FORMAT_VERSION,
collapsed_navigation: false,
hide_on_process: false,
default_page: DefaultPage::Home,
developer_mode: false,
opt_out_analytics: false,
advanced_rendering: true,
onboarded: false,
// By default, the config directory is the same as the settings directory
loaded_config_dir: DirectoryInfo::get_initial_settings_dir(),
})
} }
} }

View File

@@ -28,7 +28,7 @@ impl Tags {
fetch_semaphore: &FetchSemaphore, fetch_semaphore: &FetchSemaphore,
) -> crate::Result<Self> { ) -> crate::Result<Self> {
let mut tags = None; let mut tags = None;
let tags_path = dirs.caches_meta_dir().join("tags.json"); let tags_path = dirs.caches_meta_dir().await.join("tags.json");
if let Ok(tags_json) = read_json::<Self>(&tags_path, io_semaphore).await if let Ok(tags_json) = read_json::<Self>(&tags_path, io_semaphore).await
{ {
@@ -60,7 +60,7 @@ impl Tags {
let tags_fetch = Tags::fetch(&state.fetch_semaphore).await?; let tags_fetch = Tags::fetch(&state.fetch_semaphore).await?;
let tags_path = let tags_path =
state.directories.caches_meta_dir().join("tags.json"); state.directories.caches_meta_dir().await.join("tags.json");
write( write(
&tags_path, &tags_path,

View File

@@ -17,7 +17,7 @@ impl Users {
dirs: &DirectoryInfo, dirs: &DirectoryInfo,
io_semaphore: &IoSemaphore, io_semaphore: &IoSemaphore,
) -> crate::Result<Self> { ) -> crate::Result<Self> {
let users_path = dirs.caches_meta_dir().join(USERS_JSON); let users_path = dirs.caches_meta_dir().await.join(USERS_JSON);
let users = read_json(&users_path, io_semaphore).await.ok(); let users = read_json(&users_path, io_semaphore).await.ok();
if let Some(users) = users { if let Some(users) = users {
@@ -29,7 +29,8 @@ impl Users {
pub async fn save(&self) -> crate::Result<()> { pub async fn save(&self) -> crate::Result<()> {
let state = State::get().await?; let state = State::get().await?;
let users_path = state.directories.caches_meta_dir().join(USERS_JSON); let users_path =
state.directories.caches_meta_dir().await.join(USERS_JSON);
write( write(
&users_path, &users_path,
&serde_json::to_vec(&self.0)?, &serde_json::to_vec(&self.0)?,

View File

@@ -221,6 +221,7 @@ pub async fn write<'a>(
Ok(()) Ok(())
} }
// Writes a icon to the cache and returns the absolute path of the icon within the cache directory
#[tracing::instrument(skip(bytes, semaphore))] #[tracing::instrument(skip(bytes, semaphore))]
pub async fn write_cached_icon( pub async fn write_cached_icon(
icon_path: &str, icon_path: &str,

View File

@@ -201,7 +201,7 @@ async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
let state = State::get().await.map_err(|_| JREError::StateError)?; let state = State::get().await.map_err(|_| JREError::StateError)?;
let mut jre_paths = HashSet::new(); let mut jre_paths = HashSet::new();
let base_path = state.directories.java_versions_dir(); let base_path = state.directories.java_versions_dir().await;
if base_path.is_dir() { if base_path.is_dir() {
if let Ok(dir) = std::fs::read_dir(base_path) { if let Ok(dir) = std::fs::read_dir(base_path) {

View File

@@ -227,7 +227,7 @@ impl<'a> From<&'a Profile> for ProfileRow<'a> {
fn from(it: &'a Profile) -> Self { fn from(it: &'a Profile) -> Self {
Self { Self {
name: &it.metadata.name, name: &it.metadata.name,
path: &it.path, path: Path::new(&it.metadata.name),
game_version: &it.metadata.game_version, game_version: &it.metadata.game_version,
loader: &it.metadata.loader, loader: &it.metadata.loader,
loader_version: it loader_version: it
@@ -285,7 +285,8 @@ impl ProfileRemove {
_args: &crate::Args, _args: &crate::Args,
_largs: &ProfileCommand, _largs: &ProfileCommand,
) -> Result<()> { ) -> Result<()> {
let profile = canonicalize(&self.profile)?; let profile =
ProfilePathId::from_fs_path(canonicalize(&self.profile)?).await?;
info!("Removing profile {} from Theseus", self.profile.display()); info!("Removing profile {} from Theseus", self.profile.display());
if confirm_async(String::from("Do you wish to continue"), true).await? { if confirm_async(String::from("Do you wish to continue"), true).await? {
@@ -335,7 +336,9 @@ impl ProfileRun {
.await?; .await?;
let credentials = auth::refresh(id).await?; let credentials = auth::refresh(id).await?;
let proc_lock = profile::run_credentials(&path, &credentials).await?; let profile_path_id = ProfilePathId::from_fs_path(path).await?;
let proc_lock =
profile::run_credentials(&profile_path_id, &credentials).await?;
let mut proc = proc_lock.write().await; let mut proc = proc_lock.write().await;
process::wait_for(&mut proc).await?; process::wait_for(&mut proc).await?;

View File

@@ -151,7 +151,7 @@ impl UserDefault {
) -> Result<()> { ) -> Result<()> {
info!("Setting user {} as default", self.user.as_hyphenated()); info!("Setting user {} as default", self.user.as_hyphenated());
let state: std::sync::Arc<State> = State::get().await?; let state = State::get().await?;
let mut settings = state.settings.write().await; let mut settings = state.settings.write().await;
if settings.default_user == Some(self.user) { if settings.default_user == Some(self.user) {

View File

@@ -50,7 +50,18 @@ pub async fn logs_get_output_by_datetime(
profile_uuid: Uuid, profile_uuid: Uuid,
datetime_string: String, datetime_string: String,
) -> Result<String> { ) -> Result<String> {
Ok(logs::get_output_by_datetime(profile_uuid, &datetime_string).await?) let profile_path = if let Some(p) =
crate::profile::get_by_uuid(profile_uuid, None).await?
{
p.profile_id()
} else {
return Err(theseus::Error::from(
theseus::ErrorKind::UnmanagedProfileError(profile_uuid.to_string()),
)
.into());
};
Ok(logs::get_output_by_datetime(&profile_path, &datetime_string).await?)
} }
/// Delete all logs for a profile by profile id /// Delete all logs for a profile by profile id

View File

@@ -1,5 +1,5 @@
use crate::api::Result; use crate::api::Result;
use std::path::PathBuf;
use theseus::{ use theseus::{
pack::{ pack::{
install::install_pack, install::install_pack,
@@ -20,8 +20,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
#[tauri::command] #[tauri::command]
pub async fn pack_install( pub async fn pack_install(
location: CreatePackLocation, location: CreatePackLocation,
profile: PathBuf, profile: ProfilePathId,
) -> Result<PathBuf> { ) -> Result<ProfilePathId> {
Ok(install_pack(location, profile).await?) Ok(install_pack(location, profile).await?)
} }

View File

@@ -1,5 +1,3 @@
use std::path::{Path, PathBuf};
use crate::api::Result; use crate::api::Result;
use theseus::prelude::*; use theseus::prelude::*;
use uuid::Uuid; use uuid::Uuid;
@@ -50,14 +48,15 @@ pub async fn process_get_all_running_uuids() -> Result<Vec<Uuid>> {
// Gets all process UUIDs by profile path // Gets all process UUIDs by profile path
#[tauri::command] #[tauri::command]
pub async fn process_get_uuids_by_profile_path( pub async fn process_get_uuids_by_profile_path(
profile_path: &Path, profile_path: ProfilePathId,
) -> Result<Vec<Uuid>> { ) -> Result<Vec<Uuid>> {
Ok(process::get_uuids_by_profile_path(profile_path).await?) Ok(process::get_uuids_by_profile_path(profile_path).await?)
} }
// Gets the Profile paths of each *running* stored process in the state // Gets the Profile paths of each *running* stored process in the state
#[tauri::command] #[tauri::command]
pub async fn process_get_all_running_profile_paths() -> Result<Vec<PathBuf>> { pub async fn process_get_all_running_profile_paths(
) -> Result<Vec<ProfilePathId>> {
Ok(process::get_all_running_profile_paths().await?) Ok(process::get_all_running_profile_paths().await?)
} }

View File

@@ -36,8 +36,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
// Remove a profile // Remove a profile
// invoke('plugin:profile|profile_add_path',path) // invoke('plugin:profile|profile_add_path',path)
#[tauri::command] #[tauri::command]
pub async fn profile_remove(path: &Path) -> Result<()> { pub async fn profile_remove(path: ProfilePathId) -> Result<()> {
profile::remove(path).await?; profile::remove(&path).await?;
Ok(()) Ok(())
} }
@@ -45,19 +45,19 @@ pub async fn profile_remove(path: &Path) -> Result<()> {
// invoke('plugin:profile|profile_add_path',path) // invoke('plugin:profile|profile_add_path',path)
#[tauri::command] #[tauri::command]
pub async fn profile_get( pub async fn profile_get(
path: &Path, path: ProfilePathId,
clear_projects: Option<bool>, clear_projects: Option<bool>,
) -> Result<Option<Profile>> { ) -> Result<Option<Profile>> {
let res = profile::get(path, clear_projects).await?; let res = profile::get(&path, clear_projects).await?;
Ok(res) Ok(res)
} }
// Get optimal java version from profile // Get optimal java version from profile
#[tauri::command] #[tauri::command]
pub async fn profile_get_optimal_jre_key( pub async fn profile_get_optimal_jre_key(
path: &Path, path: ProfilePathId,
) -> Result<Option<JavaVersion>> { ) -> Result<Option<JavaVersion>> {
let res = profile::get_optimal_jre_key(path).await?; let res = profile::get_optimal_jre_key(&path).await?;
Ok(res) Ok(res)
} }
@@ -66,14 +66,14 @@ pub async fn profile_get_optimal_jre_key(
#[tauri::command] #[tauri::command]
pub async fn profile_list( pub async fn profile_list(
clear_projects: Option<bool>, clear_projects: Option<bool>,
) -> Result<HashMap<PathBuf, Profile>> { ) -> Result<HashMap<ProfilePathId, Profile>> {
let res = profile::list(clear_projects).await?; let res = profile::list(clear_projects).await?;
Ok(res) Ok(res)
} }
#[tauri::command] #[tauri::command]
pub async fn profile_check_installed( pub async fn profile_check_installed(
path: &Path, path: ProfilePathId,
project_id: String, project_id: String,
) -> Result<bool> { ) -> Result<bool> {
let profile = profile_get(path, None).await?; let profile = profile_get(path, None).await?;
@@ -94,8 +94,8 @@ pub async fn profile_check_installed(
/// Installs/Repairs a profile /// Installs/Repairs a profile
/// invoke('plugin:profile|profile_install') /// invoke('plugin:profile|profile_install')
#[tauri::command] #[tauri::command]
pub async fn profile_install(path: &Path) -> Result<()> { pub async fn profile_install(path: ProfilePathId) -> Result<()> {
profile::install(path).await?; profile::install(&path).await?;
Ok(()) Ok(())
} }
@@ -103,40 +103,40 @@ pub async fn profile_install(path: &Path) -> Result<()> {
/// invoke('plugin:profile|profile_update_all') /// invoke('plugin:profile|profile_update_all')
#[tauri::command] #[tauri::command]
pub async fn profile_update_all( pub async fn profile_update_all(
path: &Path, path: ProfilePathId,
) -> Result<HashMap<PathBuf, PathBuf>> { ) -> Result<HashMap<ProjectPathId, ProjectPathId>> {
Ok(profile::update_all(path).await?) Ok(profile::update_all(&path).await?)
} }
/// Updates a specified project /// Updates a specified project
/// invoke('plugin:profile|profile_update_project') /// invoke('plugin:profile|profile_update_project')
#[tauri::command] #[tauri::command]
pub async fn profile_update_project( pub async fn profile_update_project(
path: &Path, path: ProfilePathId,
project_path: &Path, project_path: ProjectPathId,
) -> Result<PathBuf> { ) -> Result<ProjectPathId> {
Ok(profile::update_project(path, project_path, None).await?) Ok(profile::update_project(&path, &project_path, None).await?)
} }
// Adds a project to a profile from a version ID // Adds a project to a profile from a version ID
// invoke('plugin:profile|profile_add_project_from_version') // invoke('plugin:profile|profile_add_project_from_version')
#[tauri::command] #[tauri::command]
pub async fn profile_add_project_from_version( pub async fn profile_add_project_from_version(
path: &Path, path: ProfilePathId,
version_id: String, version_id: String,
) -> Result<PathBuf> { ) -> Result<ProjectPathId> {
Ok(profile::add_project_from_version(path, version_id).await?) Ok(profile::add_project_from_version(&path, version_id).await?)
} }
// Adds a project to a profile from a path // Adds a project to a profile from a path
// invoke('plugin:profile|profile_add_project_from_path') // invoke('plugin:profile|profile_add_project_from_path')
#[tauri::command] #[tauri::command]
pub async fn profile_add_project_from_path( pub async fn profile_add_project_from_path(
path: &Path, path: ProfilePathId,
project_path: &Path, project_path: &Path,
project_type: Option<String>, project_type: Option<String>,
) -> Result<PathBuf> { ) -> Result<ProjectPathId> {
let res = profile::add_project_from_path(path, project_path, project_type) let res = profile::add_project_from_path(&path, project_path, project_type)
.await?; .await?;
Ok(res) Ok(res)
} }
@@ -145,20 +145,20 @@ pub async fn profile_add_project_from_path(
// invoke('plugin:profile|profile_toggle_disable_project') // invoke('plugin:profile|profile_toggle_disable_project')
#[tauri::command] #[tauri::command]
pub async fn profile_toggle_disable_project( pub async fn profile_toggle_disable_project(
path: &Path, path: ProfilePathId,
project_path: &Path, project_path: ProjectPathId,
) -> Result<PathBuf> { ) -> Result<ProjectPathId> {
Ok(profile::toggle_disable_project(path, project_path).await?) Ok(profile::toggle_disable_project(&path, &project_path).await?)
} }
// Removes a project from a profile // Removes a project from a profile
// invoke('plugin:profile|profile_remove_project') // invoke('plugin:profile|profile_remove_project')
#[tauri::command] #[tauri::command]
pub async fn profile_remove_project( pub async fn profile_remove_project(
path: &Path, path: ProfilePathId,
project_path: &Path, project_path: ProjectPathId,
) -> Result<()> { ) -> Result<()> {
profile::remove_project(path, project_path).await?; profile::remove_project(&path, &project_path).await?;
Ok(()) Ok(())
} }
@@ -166,13 +166,13 @@ pub async fn profile_remove_project(
// invoke('profile_export_mrpack') // invoke('profile_export_mrpack')
#[tauri::command] #[tauri::command]
pub async fn profile_export_mrpack( pub async fn profile_export_mrpack(
path: &Path, path: ProfilePathId,
export_location: PathBuf, export_location: PathBuf,
included_overrides: Vec<String>, included_overrides: Vec<String>,
version_id: Option<String>, version_id: Option<String>,
) -> Result<()> { ) -> Result<()> {
profile::export_mrpack( profile::export_mrpack(
path, &path,
export_location, export_location,
included_overrides, included_overrides,
version_id, version_id,
@@ -190,7 +190,7 @@ pub async fn profile_export_mrpack(
// => [folder1, folder2] // => [folder1, folder2]
#[tauri::command] #[tauri::command]
pub async fn profile_get_potential_override_folders( pub async fn profile_get_potential_override_folders(
profile_path: PathBuf, profile_path: ProfilePathId,
) -> Result<Vec<PathBuf>> { ) -> Result<Vec<PathBuf>> {
let overrides = let overrides =
profile::get_potential_override_folders(profile_path).await?; profile::get_potential_override_folders(profile_path).await?;
@@ -202,8 +202,8 @@ pub async fn profile_get_potential_override_folders(
// for the actual Child in the state. // for the actual Child in the state.
// invoke('plugin:profile|profile_run', path) // invoke('plugin:profile|profile_run', path)
#[tauri::command] #[tauri::command]
pub async fn profile_run(path: &Path) -> Result<Uuid> { pub async fn profile_run(path: ProfilePathId) -> Result<Uuid> {
let minecraft_child = profile::run(path).await?; let minecraft_child = profile::run(&path).await?;
let uuid = minecraft_child.read().await.uuid; let uuid = minecraft_child.read().await.uuid;
Ok(uuid) Ok(uuid)
} }
@@ -211,8 +211,8 @@ pub async fn profile_run(path: &Path) -> Result<Uuid> {
// Run Minecraft using a profile using the default credentials, and wait for the result // Run Minecraft using a profile using the default credentials, and wait for the result
// invoke('plugin:profile|profile_run_wait', path) // invoke('plugin:profile|profile_run_wait', path)
#[tauri::command] #[tauri::command]
pub async fn profile_run_wait(path: &Path) -> Result<()> { pub async fn profile_run_wait(path: ProfilePathId) -> Result<()> {
let proc_lock = profile::run(path).await?; let proc_lock = profile::run(&path).await?;
let mut proc = proc_lock.write().await; let mut proc = proc_lock.write().await;
Ok(process::wait_for(&mut proc).await?) Ok(process::wait_for(&mut proc).await?)
} }
@@ -223,11 +223,12 @@ pub async fn profile_run_wait(path: &Path) -> Result<()> {
// invoke('plugin:profile|profile_run_credentials', {path, credentials})') // invoke('plugin:profile|profile_run_credentials', {path, credentials})')
#[tauri::command] #[tauri::command]
pub async fn profile_run_credentials( pub async fn profile_run_credentials(
path: &Path, path: ProfilePathId,
credentials: Credentials, credentials: Credentials,
) -> Result<Uuid> { ) -> Result<Uuid> {
let minecraft_child = profile::run_credentials(path, &credentials).await?; let minecraft_child = profile::run_credentials(&path, &credentials).await?;
let uuid = minecraft_child.read().await.uuid; let uuid = minecraft_child.read().await.uuid;
Ok(uuid) Ok(uuid)
} }
@@ -235,10 +236,10 @@ pub async fn profile_run_credentials(
// invoke('plugin:profile|profile_run_wait', {path, credentials) // invoke('plugin:profile|profile_run_wait', {path, credentials)
#[tauri::command] #[tauri::command]
pub async fn profile_run_wait_credentials( pub async fn profile_run_wait_credentials(
path: &Path, path: ProfilePathId,
credentials: Credentials, credentials: Credentials,
) -> Result<()> { ) -> Result<()> {
let proc_lock = profile::run_credentials(path, &credentials).await?; let proc_lock = profile::run_credentials(&path, &credentials).await?;
let mut proc = proc_lock.write().await; let mut proc = proc_lock.write().await;
Ok(process::wait_for(&mut proc).await?) Ok(process::wait_for(&mut proc).await?)
} }
@@ -265,10 +266,10 @@ pub struct EditProfileMetadata {
// invoke('plugin:profile|profile_edit', {path, editProfile}) // invoke('plugin:profile|profile_edit', {path, editProfile})
#[tauri::command] #[tauri::command]
pub async fn profile_edit( pub async fn profile_edit(
path: &Path, path: ProfilePathId,
edit_profile: EditProfile, edit_profile: EditProfile,
) -> Result<()> { ) -> Result<()> {
profile::edit(path, |prof| { profile::edit(&path, |prof| {
if let Some(metadata) = edit_profile.metadata.clone() { if let Some(metadata) = edit_profile.metadata.clone() {
if let Some(name) = metadata.name { if let Some(name) = metadata.name {
prof.metadata.name = name; prof.metadata.name = name;
@@ -305,9 +306,9 @@ pub async fn profile_edit(
// invoke('plugin:profile|profile_edit_icon') // invoke('plugin:profile|profile_edit_icon')
#[tauri::command] #[tauri::command]
pub async fn profile_edit_icon( pub async fn profile_edit_icon(
path: &Path, path: ProfilePathId,
icon_path: Option<&Path>, icon_path: Option<&Path>,
) -> Result<()> { ) -> Result<()> {
profile::edit_icon(path, icon_path).await?; profile::edit_icon(&path, icon_path).await?;
Ok(()) Ok(())
} }

View File

@@ -17,7 +17,7 @@ pub async fn profile_create(
modloader: ModLoader, // the modloader to use modloader: ModLoader, // the modloader to use
loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
icon: Option<PathBuf>, // the icon for the profile icon: Option<PathBuf>, // the icon for the profile
) -> Result<PathBuf> { ) -> Result<ProfilePathId> {
let res = profile_create::profile_create( let res = profile_create::profile_create(
name, name,
game_version, game_version,

View File

@@ -1,3 +1,5 @@
use std::path::PathBuf;
use crate::api::Result; use crate::api::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use theseus::prelude::*; use theseus::prelude::*;
@@ -22,7 +24,11 @@ pub struct FrontendSettings {
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("settings") tauri::plugin::Builder::new("settings")
.invoke_handler(tauri::generate_handler![settings_get, settings_set,]) .invoke_handler(tauri::generate_handler![
settings_get,
settings_set,
settings_change_config_dir
])
.build() .build()
} }
@@ -41,3 +47,12 @@ pub async fn settings_set(settings: Settings) -> Result<()> {
settings::set(settings).await?; settings::set(settings).await?;
Ok(()) Ok(())
} }
// Change config directory
// Seizes the entire State to do it
// invoke('plugin:settings|settings_change_config_dir', new_dir)
#[tauri::command]
pub async fn settings_change_config_dir(new_config_dir: PathBuf) -> Result<()> {
settings::set_config_dir(new_config_dir).await?;
Ok(())
}

View File

@@ -122,6 +122,6 @@ pub async fn handle_command(command: String) -> Result<()> {
#[tauri::command] #[tauri::command]
pub async fn await_sync() -> Result<()> { pub async fn await_sync() -> Result<()> {
State::sync().await?; State::sync().await?;
tracing::info!("State synced"); tracing::debug!("State synced");
Ok(()) Ok(())
} }

View File

@@ -69,14 +69,12 @@ const exportPack = async () => {
} }
}) })
}) })
console.log(filesToExport)
const outputPath = await open({ const outputPath = await open({
directory: true, directory: true,
multiple: false, multiple: false,
}) })
if (outputPath) { if (outputPath) {
console.log(outputPath)
export_profile_mrpack( export_profile_mrpack(
props.instance.path, props.instance.path,
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`, outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,

View File

@@ -18,14 +18,13 @@ import {
get, get,
list, list,
} from '@/helpers/profile' } from '@/helpers/profile'
import { tauri } from '@tauri-apps/api'
import { open } from '@tauri-apps/api/dialog' import { open } from '@tauri-apps/api/dialog'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { create } from '@/helpers/profile' import { create } from '@/helpers/profile'
import { installVersionDependencies } from '@/helpers/utils' import { installVersionDependencies } from '@/helpers/utils'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser' import mixpanel from 'mixpanel-browser'
import { useTheming } from '@/store/theme.js' import { useTheming } from '@/store/theme.js'
import { tauri } from '@tauri-apps/api'
const themeStore = useTheming() const themeStore = useTheming()
@@ -227,7 +226,7 @@ const check_valid = computed(() => {
!profile.metadata.icon || !profile.metadata.icon ||
(profile.metadata.icon && profile.metadata.icon.startsWith('http')) (profile.metadata.icon && profile.metadata.icon.startsWith('http'))
? profile.metadata.icon ? profile.metadata.icon
: convertFileSrc(profile.metadata?.icon) : tauri.convertFileSrc(profile.metadata?.icon)
" "
class="profile-image" class="profile-image"
/> />

View File

@@ -37,3 +37,9 @@ export async function get() {
export async function set(settings) { export async function set(settings) {
return await invoke('plugin:settings|settings_set', { settings }) return await invoke('plugin:settings|settings_set', { settings })
} }
// Changes the config dir
// Seizes the entire application state until its done
export async function change_config_dir(newConfigDir) {
return await invoke('plugin:settings|settings_change_config_dir', { newConfigDir })
}

View File

@@ -147,13 +147,13 @@ import {
import { process_listener, profile_listener } from '@/helpers/events' import { process_listener, profile_listener } from '@/helpers/events'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ref, onUnmounted } from 'vue' import { ref, onUnmounted } from 'vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { handleError, useBreadcrumbs, useLoading } from '@/store/state' import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { showInFolder } from '@/helpers/utils.js' import { showInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
import mixpanel from 'mixpanel-browser' import mixpanel from 'mixpanel-browser'
import { PackageIcon } from '@/assets/icons/index.js' import { PackageIcon } from '@/assets/icons/index.js'
import ExportModal from '@/components/ui/ExportModal.vue' import ExportModal from '@/components/ui/ExportModal.vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
const route = useRoute() const route = useRoute()
@@ -278,6 +278,12 @@ const handleOptionsClick = async (args) => {
const unlistenProfiles = await profile_listener(async (event) => { const unlistenProfiles = await profile_listener(async (event) => {
if (event.path === route.params.id) { if (event.path === route.params.id) {
if (event.event === 'removed') {
await router.push({
path: '/',
})
return
}
instance.value = await get(route.params.id).catch(handleError) instance.value = await get(route.params.id).catch(handleError)
} }
}) })

View File

@@ -107,7 +107,7 @@ async function getLiveLog() {
} }
async function getLogs() { async function getLogs() {
return (await get_logs(props.instance.uuid, true).catch(handleError)).reverse().map((log) => { return (await get_logs(props.instance.path, true).catch(handleError)).reverse().map((log) => {
log.name = dayjs( log.name = dayjs(
log.datetime_string.slice(0, 8) + 'T' + log.datetime_string.slice(9) log.datetime_string.slice(0, 8) + 'T' + log.datetime_string.slice(9)
).calendar() ).calendar()
@@ -149,7 +149,7 @@ watch(selectedLogIndex, async (newIndex) => {
if (logs.value.length > 1 && newIndex !== 0) { if (logs.value.length > 1 && newIndex !== 0) {
logs.value[newIndex].stdout = 'Loading...' logs.value[newIndex].stdout = 'Loading...'
logs.value[newIndex].stdout = await get_output_by_datetime( logs.value[newIndex].stdout = await get_output_by_datetime(
props.instance.uuid, props.instance.path,
logs.value[newIndex].datetime_string logs.value[newIndex].datetime_string
).catch(handleError) ).catch(handleError)
} }
@@ -164,7 +164,7 @@ const deleteLog = async () => {
let deleteIndex = selectedLogIndex.value let deleteIndex = selectedLogIndex.value
selectedLogIndex.value = deleteIndex - 1 selectedLogIndex.value = deleteIndex - 1
await delete_logs_by_datetime( await delete_logs_by_datetime(
props.instance.uuid, props.instance.path,
logs.value[deleteIndex].datetime_string logs.value[deleteIndex].datetime_string
).catch(handleError) ).catch(handleError)
await setLogs() await setLogs()

View File

@@ -331,7 +331,6 @@ import {
CodeIcon, CodeIcon,
} from 'omorphia' } from 'omorphia'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { import {
add_project_from_path, add_project_from_path,
@@ -345,6 +344,7 @@ import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser' import mixpanel from 'mixpanel-browser'
import { open } from '@tauri-apps/api/dialog' import { open } from '@tauri-apps/api/dialog'
import { listen } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { showInFolder } from '@/helpers/utils.js' import { showInFolder } from '@/helpers/utils.js'
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage } from '@/assets/icons' import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage } from '@/assets/icons'

View File

@@ -98,7 +98,7 @@ async fn main() -> theseus::Result<()> {
println!("running"); println!("running");
// Run a profile, running minecraft and store the RwLock to the process // Run a profile, running minecraft and store the RwLock to the process
let proc_lock = profile::run(&canonicalize(&profile_path)?).await?; let proc_lock = profile::run(&profile_path).await?;
let uuid = proc_lock.read().await.uuid; let uuid = proc_lock.read().await.uuid;
let pid = proc_lock.read().await.current_child.read().await.id(); let pid = proc_lock.read().await.current_child.read().await.id();