1
0

Misc settings (#137)

* Initial bug fixes

* fix compile error on non-mac

* Fix even more bugs

* Fix more

* fix more

* fix build

* fix build

* Search fixes

* Fix small instance ui

* working basic

* fix javaw issue

* removed zip

* working functions

* merge fixes

* fixed loadintg bar bug

* menu fix

* wait for settings to sync

* safety expanded and for loading bars

* swtiching to windows

* minimize

* default landing page

* test link registry

* url redirection

* fix formatting

* .mrpack windows

* working mrpack reader

* changed to one layer deep

* working .mrpack + command handling for both opening and existing process

* forge version numbers

* working mac opening mrpack

* reverted changes

* prettier/fmt

* missed debug statement

* improvements + refactoring

* renamed things to fit plugin

* fixed bugs

* removed println

* overrides dont include mrpack

* merge

* fixes

* fixes

* fixed deletion

* merge errors

* force sync before export

* removed testing

* missed line

* removed console log

* mac error reverted

* incoreclty named helper

* additional fixes

* added removed merges

* fixed mislabled invokes

* mac

* added to new register method

* comments, cleanup

* mac clippy change

* review changes

* minor changes

* moved create pack

* removed playground compilation bug

* fixed linux bug; other add ons

* fixed review commets

* cicd fix

* mistaken import for prod

* cicd fix

---------

Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
Wyatt Verchere
2023-06-30 08:11:32 -07:00
committed by GitHub
parent f52e777379
commit 91d3bf825d
34 changed files with 1258 additions and 157 deletions

162
Cargo.lock generated
View File

@@ -313,22 +313,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
[[package]]
name = "attohttpc"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7"
dependencies = [
"flate2",
"http",
"log",
"native-tls",
"serde",
"serde_json",
"serde_urlencoded",
"url",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -501,6 +485,9 @@ name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
dependencies = [
"serde",
]
[[package]]
name = "bzip2"
@@ -900,12 +887,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "cty"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
[[package]]
name = "cxx"
version = "1.0.94"
@@ -2144,6 +2125,19 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "interprocess"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb"
dependencies = [
"cfg-if",
"libc",
"rustc_version",
"to_method",
"winapi",
]
[[package]]
name = "io-lifetimes"
version = "1.0.9"
@@ -2839,6 +2833,28 @@ dependencies = [
"objc_id",
]
[[package]]
name = "objc-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99e1d07c6eab1ce8b6382b8e3c7246fe117ff3f8b34be065f5ebace6749fe845"
[[package]]
name = "objc2"
version = "0.3.0-beta.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef3a6024722b4230242a53e5b5759ce117548983696b8e4b7bc2fd1f8fce621e"
dependencies = [
"objc-sys",
"objc2-encode",
]
[[package]]
name = "objc2-encode"
version = "2.0.0-pre.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f8f7297b786454a87e392631e2b2754ed59a7b413effa8521225d93f46b2192"
[[package]]
name = "objc_exception"
version = "0.1.2"
@@ -3443,12 +3459,9 @@ dependencies = [
[[package]]
name = "raw-window-handle"
version = "0.5.0"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a"
dependencies = [
"cty",
]
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
[[package]]
name = "redox_syscall"
@@ -3894,11 +3907,11 @@ dependencies = [
[[package]]
name = "serde_with"
version = "2.3.3"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe"
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
dependencies = [
"base64 0.13.1",
"base64 0.21.0",
"chrono",
"hex",
"indexmap",
@@ -3910,9 +3923,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "2.3.3"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f"
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
dependencies = [
"darling 0.20.1",
"proc-macro2",
@@ -4197,6 +4210,19 @@ dependencies = [
"libc",
]
[[package]]
name = "sys-locale"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a11bd9c338fdba09f7881ab41551932ad42e405f61d01e8406baea71c07aee"
dependencies = [
"js-sys",
"libc",
"wasm-bindgen",
"web-sys",
"windows-sys 0.45.0",
]
[[package]]
name = "system-deps"
version = "5.0.0"
@@ -4315,13 +4341,13 @@ dependencies = [
[[package]]
name = "tauri"
version = "1.3.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842"
checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e"
dependencies = [
"anyhow",
"attohttpc",
"base64 0.21.0",
"bytes",
"cocoa",
"dirs-next",
"embed_plist",
@@ -4343,6 +4369,7 @@ dependencies = [
"rand 0.8.5",
"raw-window-handle",
"regex",
"reqwest",
"rfd",
"semver",
"serde",
@@ -4350,6 +4377,7 @@ dependencies = [
"serde_repr",
"serialize-to-javascript",
"state",
"sys-locale",
"tar",
"tauri-macros",
"tauri-runtime",
@@ -4387,9 +4415,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a2105f807c6f50b2fa2ce5abd62ef207bc6f14c9fcc6b8caec437f6fb13bde"
checksum = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a"
dependencies = [
"base64 0.21.0",
"brotli",
@@ -4413,9 +4441,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8784cfe6f5444097e93c69107d1ac5e8f13d02850efa8d8f2a40fe79674cef46"
checksum = "8eb12a2454e747896929338d93b0642144bb51e0dddbb36e579035731f0d76b7"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@@ -4425,6 +4453,22 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin-deep-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33a3ae55bcfe692e5361edc4708bd9f415270cc02e1cdba8ab7768566208b4e2"
dependencies = [
"dirs 5.0.1",
"interprocess",
"log",
"objc2",
"once_cell",
"tauri-utils",
"windows-sys 0.48.0",
"winreg 0.50.0",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "0.0.0"
@@ -4455,9 +4499,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b80ea3fcd5fefb60739a3b577b277e8fc30434538a2f5bba82ad7d4368c422"
checksum = "108683199cb18f96d2d4134187bb789964143c845d2d154848dda209191fd769"
dependencies = [
"gtk",
"http",
@@ -4476,9 +4520,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1c396950b1ba06aee1b4ffe6c7cd305ff433ca0e30acbc5fa1a2f92a4ce70f1"
checksum = "0b7aa256a1407a3a091b5d843eccc1a5042289baf0a43d1179d9f0fcfea37c1b"
dependencies = [
"cocoa",
"gtk",
@@ -4496,12 +4540,13 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864"
checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84"
dependencies = [
"brotli",
"ctor",
"dunce",
"glob",
"heck 0.4.1",
"html5ever",
@@ -4598,8 +4643,9 @@ dependencies = [
"tokio-stream",
"toml 0.7.3",
"tracing",
"tracing-error 0.2.0",
"tracing-subscriber 0.3.17",
"tracing-appender",
"tracing-error 0.1.2",
"tracing-subscriber 0.2.25",
"url",
"uuid",
"whoami",
@@ -4641,8 +4687,11 @@ dependencies = [
"chrono",
"cocoa",
"daedalus",
"dirs 5.0.1",
"futures",
"lazy_static",
"objc",
"once_cell",
"os_info",
"sentry",
"sentry-rust-minidump",
@@ -4650,6 +4699,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-deep-link",
"tauri-plugin-single-instance",
"tauri-plugin-window-state",
"theseus",
@@ -4658,7 +4708,6 @@ dependencies = [
"tokio-stream",
"tracing",
"tracing-error 0.1.2",
"tracing-subscriber 0.2.25",
"url",
"uuid",
"window-shadows",
@@ -4768,6 +4817,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "to_method"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
[[package]]
name = "tokio"
version = "1.27.0"
@@ -4896,6 +4951,17 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9965507e507f12c8901432a33e31131222abac31edd90cabbcf85cf544b7127a"
dependencies = [
"chrono",
"crossbeam-channel",
"tracing-subscriber 0.2.25",
]
[[package]]
name = "tracing-attributes"
version = "0.1.23"

View File

@@ -28,9 +28,11 @@ dirs = "5.0.1"
regex = "1.5"
sys-info = "0.9.0"
thiserror = "1.0"
tracing = "0.1.37"
tracing-subscriber = "0.3.17"
tracing-error = "0.2.0"
tracing-subscriber = {version = "0.2", features = ["chrono"]}
tracing-error = "0.1"
tracing-appender = "0.1"
paste = { version = "1.0"}

View File

@@ -0,0 +1,72 @@
use std::path::PathBuf;
use crate::event::{
emit::{emit_command, emit_warning},
CommandPayload,
};
/// Handles external functions (such as through URL deep linkage)
/// Link is extracted value (link) in somewhat URL format, such as
/// subdomain1/subdomain2
/// (Does not include modrinth://)
pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
Ok(match sublink.split_once('/') {
// /mod/{id} - Installs a mod of mod id
Some(("mod", id)) => CommandPayload::InstallMod { id: id.to_string() },
// /version/{id} - Installs a specific version of id
Some(("version", id)) => {
CommandPayload::InstallVersion { id: id.to_string() }
}
// /modpack/{id} - Installs a modpack of modpack id
Some(("modpack", id)) => {
CommandPayload::InstallModpack { id: id.to_string() }
}
_ => {
emit_warning(&format!(
"Invalid command, unrecognized path: {sublink}"
))
.await?;
return Err(crate::ErrorKind::InputError(format!(
"Invalid command, unrecognized path: {sublink}"
))
.into());
}
})
}
pub async fn parse_command(
command_string: &str,
) -> crate::Result<CommandPayload> {
tracing::debug!("Parsing command: {}", &command_string);
// modrinth://some-command
// This occurs when following a web redirect link
if let Some(sublink) = command_string.strip_prefix("modrinth://") {
Ok(handle_url(sublink).await?)
} else {
// We assume anything else is a filepath to an .mrpack file
let path = PathBuf::from(command_string);
let path = path.canonicalize()?;
if let Some(ext) = path.extension() {
if ext == "mrpack" {
return Ok(CommandPayload::RunMRPack { path });
}
}
emit_warning(&format!(
"Invalid command, unrecognized filetype: {}",
path.display()
))
.await?;
Err(crate::ErrorKind::InputError(format!(
"Invalid command, unrecognized filetype: {}",
path.display()
))
.into())
}
}
pub async fn parse_and_emit_command(command_string: &str) -> crate::Result<()> {
let command = parse_command(command_string).await?;
emit_command(command).await?;
Ok(())
}

View File

@@ -1,5 +1,6 @@
//! API for interacting with Theseus
pub mod auth;
pub mod handler;
pub mod jre;
pub mod logs;
pub mod metadata;
@@ -7,6 +8,7 @@ pub mod pack;
pub mod process;
pub mod profile;
pub mod profile_create;
pub mod safety;
pub mod settings;
pub mod tags;
@@ -22,6 +24,7 @@ pub mod prelude {
pub use crate::{
auth::{self, Credentials},
data::*,
event::CommandPayload,
jre, metadata, pack, process,
profile::{self, Profile},
profile_create, settings,

View File

@@ -0,0 +1,5 @@
use crate::state::{ProcessType, SafeProcesses};
pub async fn check_safe_loading_bars() -> crate::Result<bool> {
SafeProcesses::is_complete(ProcessType::LoadingBar).await
}

View File

@@ -1,4 +1,5 @@
//! Theseus profile management interface
pub use crate::{
state::{
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,

View File

@@ -90,6 +90,10 @@ pub enum ErrorKind {
#[error("Error: {0}")]
OtherError(String),
#[cfg(feature = "tauri")]
#[error("Tauri error: {0}")]
TauriError(#[from] tauri::Error),
}
#[derive(Debug)]

View File

@@ -1,11 +1,13 @@
use super::LoadingBarId;
use crate::event::{
EventError, LoadingBar, LoadingBarType, ProcessPayloadType,
ProfilePayloadType,
use crate::{
event::{
CommandPayload, EventError, LoadingBar, LoadingBarType,
ProcessPayloadType, ProfilePayloadType,
},
state::{ProcessType, SafeProcesses},
};
use futures::prelude::*;
use std::path::PathBuf;
use tracing::warn;
#[cfg(feature = "tauri")]
use crate::event::{
@@ -42,15 +44,29 @@ const CLI_PROGRESS_BAR_TOTAL: u64 = 1000;
}
*/
// Initialize a loading bar for use in emit_loading
// This will generate a LoadingBarId, which is used to refer to the loading bar uniquely.
// total is the total amount of work to be done- all emissions will be considered a fraction of this value (should be 1 or 100 for simplicity)
// title is the title of the loading bar
/// Initialize a loading bar for use in emit_loading
/// This will generate a LoadingBarId, which is used to refer to the loading bar uniquely.
/// total is the total amount of work to be done- all emissions will be considered a fraction of this value (should be 1 or 100 for simplicity)
/// title is the title of the loading bar
/// The app will wait for this loading bar to finish before exiting, as it is considered safe.
#[theseus_macros::debug_pin]
pub async fn init_loading(
bar_type: LoadingBarType,
total: f64,
title: &str,
) -> crate::Result<LoadingBarId> {
let key = init_loading_unsafe(bar_type, total, title).await?;
SafeProcesses::add_uuid(ProcessType::LoadingBar, key.0).await?;
Ok(key)
}
/// An unsafe loading bar can be created without adding it to the SafeProcesses list,
/// meaning that the app won't ask to wait for it to finish before exiting.
#[theseus_macros::debug_pin]
pub async fn init_loading_unsafe(
bar_type: LoadingBarType,
total: f64,
title: &str,
) -> crate::Result<LoadingBarId> {
let event_state = crate::EventState::get().await?;
let key = LoadingBarId(Uuid::new_v4());
@@ -76,8 +92,6 @@ pub async fn init_loading(
).unwrap()
.progress_chars("#>-"),
);
//pb.set_message(title);
pb
},
},
@@ -215,7 +229,25 @@ pub async fn emit_warning(message: &str) -> crate::Result<()> {
)
.map_err(EventError::from)?;
}
warn!("{}", message);
tracing::warn!("{}", message);
Ok(())
}
// emit_command(CommandPayload::Something { something })
// ie: installing a pack, opening an .mrpack, etc
// Generally used for url deep links and file opens that we we want to handle in the frontend
#[allow(dead_code)]
#[allow(unused_variables)]
pub async fn emit_command(command: CommandPayload) -> crate::Result<()> {
tracing::debug!("Command: {}", serde_json::to_string(&command)?);
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get().await?;
event_state
.app
.emit_all("command", command)
.map_err(EventError::from)?;
}
Ok(())
}

View File

@@ -5,6 +5,8 @@ use tokio::sync::OnceCell;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::state::SafeProcesses;
pub mod emit;
// Global event state
@@ -48,6 +50,12 @@ impl EventState {
Ok(EVENT_STATE.get().ok_or(EventError::NotInitialized)?.clone())
}
// Initialization requires no app handle in non-tauri mode, so we can just use the same function
#[cfg(not(feature = "tauri"))]
pub async fn get() -> crate::Result<Arc<Self>> {
Self::init().await
}
// Values provided should not be used directly, as they are clones and are not guaranteed to be up-to-date
pub async fn list_progress_bars() -> crate::Result<HashMap<Uuid, LoadingBar>>
{
@@ -62,10 +70,11 @@ impl EventState {
Ok(display_list)
}
// Initialization requires no app handle in non-tauri mode, so we can just use the same function
#[cfg(not(feature = "tauri"))]
pub async fn get() -> crate::Result<Arc<Self>> {
Self::init().await
#[cfg(feature = "tauri")]
pub async fn get_main_window() -> crate::Result<Option<tauri::Window>> {
use tauri::Manager;
let value = Self::get().await?;
Ok(value.app.get_window("main"))
}
}
@@ -91,8 +100,6 @@ pub struct LoadingBarId(Uuid);
impl Drop for LoadingBarId {
fn drop(&mut self) {
let loader_uuid = self.0;
let _event = LoadingBarType::StateInit;
let _message = "finished".to_string();
tokio::spawn(async move {
if let Ok(event_state) = EventState::get().await {
let mut bars = event_state.loading_bars.write().await;
@@ -132,6 +139,11 @@ impl Drop for LoadingBarId {
#[cfg(not(any(feature = "tauri", feature = "cli")))]
bars.remove(&loader_uuid);
}
let _ = SafeProcesses::complete(
crate::state::ProcessType::LoadingBar,
loader_uuid,
)
.await;
});
}
}
@@ -184,6 +196,24 @@ pub struct WarningPayload {
pub message: String,
}
#[derive(Serialize, Clone)]
#[serde(tag = "event")]
pub enum CommandPayload {
InstallMod {
id: String,
},
InstallVersion {
id: String,
},
InstallModpack {
id: String,
},
RunMRPack {
// run or install .mrpack
path: PathBuf,
},
}
#[derive(Serialize, Clone)]
pub struct ProcessPayload {
pub uuid: Uuid, // processes in state are going to be identified by UUIDs, as they might change to different processes

View File

@@ -4,6 +4,7 @@ use crate::event::{LoadingBarId, LoadingBarType};
use crate::jre::{JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
use crate::prelude::JavaVersion;
use crate::state::ProfileInstallStage;
use crate::EventState;
use crate::{
process,
state::{self as st, MinecraftChild},
@@ -471,6 +472,18 @@ pub async fn launch_minecraft(
"{MINECRAFT_UUID}".to_string(),
);
// If in tauri, and the 'minimize on launch' setting is enabled, minimize the window
#[cfg(feature = "tauri")]
{
let window = EventState::get_main_window().await?;
if let Some(window) = window {
let settings = state.settings.read().await;
if settings.hide_on_process {
window.minimize()?;
}
}
}
// Create Minecraft child by inserting it into the state
// This also spawns the process and prepares the subsequent processes
let mut state_children = state.children.write().await;

View File

@@ -15,9 +15,11 @@ mod config;
mod error;
mod event;
mod launcher;
mod logger;
mod state;
pub use api::*;
pub use error::*;
pub use event::{EventState, LoadingBar, LoadingBarType};
pub use logger::start_logger;
pub use state::State;

75
theseus/src/logger.rs Normal file
View File

@@ -0,0 +1,75 @@
/*
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
ERROR > WARN > INFO > DEBUG > TRACE
eg. RUST_LOG=info will show info, warn, and error logs
RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates)
RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates)
Error messages returned to Tauri will display as traced error logs if they return an error.
This will also include an attached span trace if the error is from a tracing error, and the level is set to info, debug, or trace
on unix:
RUST_LOG="theseus=trace" {run command}
The default is theseus=show, meaning only logs from theseus will be displayed, and at the info or higher level.
*/
use tracing_appender::non_blocking::WorkerGuard;
// Handling for the live development logging
// This will log to the console, and will not log to a file
#[cfg(debug_assertions)]
pub fn start_logger() -> Option<WorkerGuard> {
use tracing_subscriber::prelude::*;
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info"));
let subscriber = tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber)
.expect("setting default subscriber failed");
None
}
// Handling for the live production logging
// This will log to a file in the logs directory, and will not show any logs in the console
#[cfg(not(debug_assertions))]
pub fn start_logger() -> Option<WorkerGuard> {
use crate::prelude::DirectoryInfo;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::fmt::time::ChronoLocal;
use tracing_subscriber::prelude::*;
// Initialize and get logs directory path
let path = if let Some(dir) = DirectoryInfo::init().ok() {
dir.launcher_logs_dir()
} else {
eprintln!("Could not create logger.");
return None;
};
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info"));
let file_appender =
RollingFileAppender::new(Rotation::DAILY, path, "theseus.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let subscriber = tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false) // disable ANSI escape codes
.with_timer(ChronoLocal::rfc3339()),
)
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber)
.expect("Setting default subscriber failed");
Some(guard)
}

View File

@@ -12,6 +12,7 @@ use tracing::error;
use crate::event::emit::emit_process;
use crate::event::ProcessPayloadType;
use crate::EventState;
use tokio::task::JoinHandle;
use uuid::Uuid;
@@ -129,6 +130,16 @@ impl Children {
break;
}
}
// If in tauri, window should show itself again after process exists if it was hidden
#[cfg(feature = "tauri")]
{
let window = EventState::get_main_window().await?;
if let Some(window) = window {
window.unminimize()?;
}
}
if !mc_exit_status.success() {
emit_process(
uuid,

View File

@@ -1,6 +1,6 @@
//! Theseus directory information
use std::fs;
use std::path::PathBuf;
use tokio::fs;
#[derive(Debug)]
pub struct DirectoryInfo {
@@ -11,7 +11,7 @@ pub struct DirectoryInfo {
impl DirectoryInfo {
/// Get all paths needed for Theseus to operate properly
#[tracing::instrument]
pub async fn init() -> crate::Result<Self> {
pub fn init() -> crate::Result<Self> {
// Working directory
let working_dir = std::env::current_dir().map_err(|err| {
crate::ErrorKind::FSError(format!(
@@ -26,7 +26,7 @@ impl DirectoryInfo {
"Could not find valid config dir".to_string(),
))?;
fs::create_dir_all(&config_dir).await.map_err(|err| {
fs::create_dir_all(&config_dir).map_err(|err| {
crate::ErrorKind::FSError(format!(
"Error creating Theseus config directory: {err}"
))
@@ -130,6 +130,11 @@ impl DirectoryInfo {
.join("modrinth_logs")
}
#[inline]
pub fn launcher_logs_dir(&self) -> PathBuf {
self.config_dir.join("launcher_logs")
}
/// Get the file containing the global database
#[inline]
pub fn database_file(&self) -> PathBuf {

View File

@@ -1,8 +1,7 @@
//! Theseus state management system
use crate::event::emit::emit_loading;
use crate::event::emit::{emit_loading, init_loading_unsafe};
use std::path::PathBuf;
use crate::event::emit::init_loading;
use crate::event::LoadingBarType;
use crate::loading_join;
@@ -46,6 +45,9 @@ pub use self::tags::*;
mod java_globals;
pub use self::java_globals::*;
mod safe_processes;
pub use self::safe_processes::*;
// Global state
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
pub struct State {
@@ -75,6 +77,8 @@ pub struct State {
pub(crate) users: RwLock<Users>,
/// Launcher tags
pub(crate) tags: RwLock<Tags>,
/// Launcher processes that should be safely exited on shutdown
pub(crate) safety_processes: RwLock<SafeProcesses>,
/// File watcher debouncer
pub(crate) file_watcher: RwLock<Debouncer<RecommendedWatcher>>,
@@ -88,7 +92,7 @@ impl State {
LAUNCHER_STATE
.get_or_try_init(|| {
async {
let loading_bar = init_loading(
let loading_bar = init_loading_unsafe(
LoadingBarType::StateInit,
100.0,
"Initializing launcher",
@@ -97,7 +101,7 @@ impl State {
let mut file_watcher = init_watcher().await?;
let directories = DirectoryInfo::init().await?;
let directories = DirectoryInfo::init()?;
emit_loading(&loading_bar, 10.0, None).await?;
// Settings
@@ -132,6 +136,7 @@ impl State {
let children = Children::new();
let auth_flow = AuthTask::new();
let safety_processes = SafeProcesses::new();
emit_loading(&loading_bar, 10.0, None).await?;
Ok(Arc::new(Self {
@@ -151,6 +156,7 @@ impl State {
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),
}))
}

View File

@@ -0,0 +1,69 @@
use uuid::Uuid;
use crate::State;
// We implement a store for safe loading bars such that we can wait for them to complete
// We create this store separately from the loading bars themselves, because this may be extended as needed
pub struct SafeProcesses {
pub loading_bars: Vec<Uuid>,
}
#[derive(Debug, Copy, Clone)]
pub enum ProcessType {
LoadingBar,
// Potentially other types of processes (ie: IO operations?)
}
impl SafeProcesses {
// init
pub fn new() -> Self {
Self {
loading_bars: Vec::new(),
}
}
// Adds a new running safe process to the list by uuid
pub async fn add_uuid(
r#type: ProcessType,
uuid: Uuid,
) -> crate::Result<Uuid> {
let state = State::get().await?;
let mut safe_processes = state.safety_processes.write().await;
match r#type {
ProcessType::LoadingBar => {
safe_processes.loading_bars.push(uuid);
}
}
Ok(uuid)
}
// Mark a safe process as finishing
pub async fn complete(
r#type: ProcessType,
uuid: Uuid,
) -> crate::Result<()> {
let state = State::get().await?;
let mut safe_processes = state.safety_processes.write().await;
match r#type {
ProcessType::LoadingBar => {
safe_processes.loading_bars.retain(|x| *x != uuid);
}
}
Ok(())
}
// Check if there are any pending safe processes of a given type
pub async fn is_complete(r#type: ProcessType) -> crate::Result<bool> {
let state = State::get().await?;
let safe_processes = state.safety_processes.read().await;
match r#type {
ProcessType::LoadingBar => {
if safe_processes.loading_bars.is_empty() {
return Ok(true);
}
}
}
Ok(false)
}
}

View File

@@ -30,6 +30,10 @@ pub struct Settings {
pub version: u32,
pub collapsed_navigation: bool,
#[serde(default)]
pub hide_on_process: bool,
#[serde(default)]
pub default_page: DefaultPage,
#[serde(default)]
pub developer_mode: bool,
#[serde(default)]
pub opt_out_analytics: bool,
@@ -54,6 +58,8 @@ impl Default for Settings {
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,
@@ -126,7 +132,8 @@ impl Settings {
"Error saving settings to file: {err}"
))
.as_error()
})
})?;
Ok(())
}
}
@@ -172,3 +179,16 @@ pub struct Hooks {
#[serde(skip_serializing_if = "Option::is_none")]
pub post_exit: Option<String>,
}
/// Opening window to start with
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum DefaultPage {
Home,
Library,
}
impl Default for DefaultPage {
fn default() -> Self {
Self::Home
}
}

View File

@@ -18,9 +18,12 @@ theseus = { path = "../../theseus", features = ["tauri"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.3", features = ["app-all", "devtools", "dialog", "dialog-open", "macos-private-api", "os-all", "protocol-asset", "shell-open", "updater", "window-close", "window-create", "window-hide", "window-maximize", "window-minimize", "window-set-decorations", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri = { version = "1.3", features = ["app-all", "devtools", "dialog", "dialog-confirm", "dialog-open", "macos-private-api", "os-all", "protocol-asset", "shell-open", "updater", "window-close", "window-create", "window-hide", "window-maximize", "window-minimize", "window-set-decorations", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-deep-link = "0.1.1"
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
tokio-stream = { version = "0.1", features = ["fs"] }
@@ -28,17 +31,21 @@ futures = "0.3"
daedalus = {version = "0.1.15", features = ["bincode"] }
chrono = "0.4.26"
dirs = "5.0.1"
url = "2.2"
uuid = { version = "1.1", features = ["serde", "v4"] }
os_info = "3.7.0"
tracing = "0.1.37"
tracing-subscriber = "0.2"
tracing-error = "0.1"
sentry = "0.30"
sentry-rust-minidump = "0.5"
lazy_static = "1"
once_cell = "1"
[target.'cfg(not(target_os = "linux"))'.dependencies]
window-shadows = "0.2.1"

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<!-- Obviously needs to be replaced with your app's bundle identifier -->
<string>com.modrinth.theseus</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- register the myapp:// and myscheme:// schemes -->
<string>modrinth</string>
</array>
</dict>
</array>
<!-- Declare file types your app can open -->
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Modrinth type</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.modrinth.theseus-type</string>
</array>
<key>NSDocumentClass</key>
<string>NSDocument</string>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Modrinth File</string>
<key>UTTypeIcons</key>
<dict/>
<key>UTTypeIdentifier</key>
<string>com.modrinth.theseus-type</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>mrpack</string>
</array>
<key>public.mime-type</key>
<array>
<string>application/x-mrpack</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,346 @@
<?if $(sys.BUILDARCH)="x86"?>
<?define Win64 = "no" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?elseif $(sys.BUILDARCH)="x64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else?>
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
<?endif?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product
Id="*"
Name="{{product_name}}"
UpgradeCode="{{upgrade_code}}"
Language="!(loc.TauriLanguage)"
Manufacturer="{{manufacturer}}"
Version="{{version}}">
<Package Id="*"
Keywords="Installer"
InstallerVersion="450"
Languages="0"
Compressed="yes"
InstallScope="perMachine"
SummaryCodepage="!(loc.TauriCodepage)"/>
<!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->
<!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
<Property Id="REINSTALLMODE" Value="amus" />
{{#if allow_downgrades}}
<MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
{{else}}
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="!(loc.DowngradeErrorMessage)" AllowSameVersionUpgrades="yes" />
{{/if}}
<InstallExecuteSequence>
<RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>
</InstallExecuteSequence>
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
{{#if banner_path}}
<WixVariable Id="WixUIBannerBmp" Value="{{banner_path}}" />
{{/if}}
{{#if dialog_image_path}}
<WixVariable Id="WixUIDialogBmp" Value="{{dialog_image_path}}" />
{{/if}}
{{#if license}}
<WixVariable Id="WixUILicenseRtf" Value="{{license}}" />
{{/if}}
<Icon Id="ProductIcon" SourceFile="{{icon_path}}"/>
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" /> <!-- Remove repair -->
<SetProperty Id="ARPNOMODIFY" Value="1" After="InstallValidate" Sequence="execute"/>
<!-- initialize with previous InstallDir -->
<Property Id="INSTALLDIR">
<RegistrySearch Id="PrevInstallDirReg" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw"/>
</Property>
<!-- launch app checkbox -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
<Property Id="WixShellExecTarget" Value="[!Path]" />
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
<UI>
<!-- launch app checkbox -->
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
{{#unless license}}
<!-- Skip license dialog -->
<Publish Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="InstallDirDlg"
Order="2">1</Publish>
<Publish Dialog="InstallDirDlg"
Control="Back"
Event="NewDialog"
Value="WelcomeDlg"
Order="2">1</Publish>
{{/unless}}
</UI>
<UIRef Id="WixUI_InstallDir" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="DesktopFolder" Name="Desktop">
<Component Id="ApplicationShortcutDesktop" Guid="*">
<Shortcut Id="ApplicationDesktopShortcut" Name="{{product_name}}" Description="Runs {{product_name}}" Target="[!Path]" WorkingDirectory="INSTALLDIR" />
<RemoveFolder Id="DesktopFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Desktop Shortcut" Type="integer" Value="1" KeyPath="yes" />
</Component>
</Directory>
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
<Directory Id="INSTALLDIR" Name="{{product_name}}"/>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="{{product_name}}"/>
</Directory>
</Directory>
<DirectoryRef Id="INSTALLDIR">
<Component Id="RegistryEntries" Guid="*">
<RegistryKey Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}">
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLDIR]" KeyPath="yes" />
</RegistryKey>
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
<!-- THESEUS -->
<ProgId Id="theseus.mrpack.Document" Description="Modrinth File">
<Extension Id="mrpack" ContentType="application/mrpack">
<!-- no flags on argument, so we can hijack deep link library-->
<Verb Id="open" Command="Open" TargetFile="Path" Argument="&quot;%1&quot;" />
</Extension>
</ProgId>
<!-- /THESEUS -->
</Component>
{{#each binaries as |bin| ~}}
<Component Id="{{ bin.id }}" Guid="{{bin.guid}}" Win64="$(var.Win64)">
<File Id="Bin_{{ bin.id }}" Source="{{bin.path}}" KeyPath="yes"/>
</Component>
{{/each~}}
{{#if enable_elevated_update_task}}
<Component Id="UpdateTask" Guid="C492327D-9720-4CD5-8DB8-F09082AF44BE" Win64="$(var.Win64)">
<File Id="UpdateTask" Source="update.xml" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskInstaller" Guid="011F25ED-9BE3-50A7-9E9B-3519ED2B9932" Win64="$(var.Win64)">
<File Id="UpdateTaskInstaller" Source="install-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskUninstaller" Guid="D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1" Win64="$(var.Win64)">
<File Id="UpdateTaskUninstaller" Source="uninstall-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
{{/if}}
{{resources}}
<Component Id="CMP_UninstallShortcut" Guid="*">
<Shortcut Id="UninstallShortcut"
Name="Uninstall {{product_name}}"
Description="Uninstalls {{product_name}}"
Target="[System64Folder]msiexec.exe"
Arguments="/x [ProductCode]" />
<RemoveFolder Id="INSTALLDIR"
On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\\{{manufacturer}}\\{{product_name}}"
Name="Uninstaller Shortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
<!-- THESEUS -->
<Component Id="FileTypeAssociationsReg" Guid="*">
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationDescription" Value="theseus" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationIcon" Value="[INSTALLDIR]theseus,0" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationName" Value="theseus" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\DefaultIcon" Value="[INSTALLDIR]theseus,1" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\FileAssociations" Name=".mrpack" Value="theseus.mrpack.Document" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\MIMEAssociations" Name="application/mrpack" Value="theseus.mrpack.Document" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\shell\Open\command" Value="&quot;[INSTALLDIR]theseus&quot; -e &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\RegisteredApplications" Name="theseus" Value="SOFTWARE\modrinth\theseus\Capabilities" Type="string" KeyPath="yes" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\theseus.mrpack.Document" Name="MRPACK File" Value="Modrinth Modpack Installer" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack" Name="Content Type" Value="application/mrpack" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack\OpenWithList\theseus" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack\OpenWithProgids" Name="theseus.mrpack.Document" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\Applications\mrpack\SupportedTypes" Name=".mrpack" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\Applications\mrpack\shell\open" Name="FriendlyAppName" Value="theseus" Type="string" />
</Component>
<!-- /THESEUS -->
</DirectoryRef>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="{{product_name}}"
Description="Runs {{product_name}}"
Target="[!Path]"
Icon="ProductIcon"
WorkingDirectory="INSTALLDIR">
<ShortcutProperty Key="System.AppUserModel.ID" Value="{{bundle_id}}"/>
</Shortcut>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Start Menu Shortcut" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
{{#each merge_modules as |msm| ~}}
<DirectoryRef Id="TARGETDIR">
<Merge Id="{{ msm.name }}" SourceFile="{{ msm.path }}" DiskId="1" Language="!(loc.TauriLanguage)" />
</DirectoryRef>
<Feature Id="{{ msm.name }}" Title="{{ msm.name }}" AllowAdvertise="no" Display="hidden" Level="1">
<MergeRef Id="{{ msm.name }}"/>
</Feature>
{{/each~}}
<Feature
Id="MainProgram"
Title="Application"
Description="!(loc.InstallAppFeature)"
Level="1"
ConfigurableDirectory="INSTALLDIR"
AllowAdvertise="no"
Display="expand"
Absent="disallow">
<ComponentRef Id="RegistryEntries"/>
<!-- THESEUS -->
<ComponentRef Id="FileTypeAssociationsReg" />
<!-- /THESEUS -->
{{#each resource_file_ids as |resource_file_id| ~}}
<ComponentRef Id="{{ resource_file_id }}"/>
{{/each~}}
{{#if enable_elevated_update_task}}
<ComponentRef Id="UpdateTask" />
<ComponentRef Id="UpdateTaskInstaller" />
<ComponentRef Id="UpdateTaskUninstaller" />
{{/if}}
<Feature Id="ShortcutsFeature"
Title="Shortcuts"
Level="1">
<ComponentRef Id="Path"/>
<ComponentRef Id="CMP_UninstallShortcut" />
<ComponentRef Id="ApplicationShortcut" />
<ComponentRef Id="ApplicationShortcutDesktop" />
</Feature>
<Feature
Id="Environment"
Title="PATH Environment Variable"
Description="!(loc.PathEnvVarFeature)"
Level="1"
Absent="allow">
<ComponentRef Id="Path"/>
{{#each binaries as |bin| ~}}
<ComponentRef Id="{{ bin.id }}"/>
{{/each~}}
</Feature>
</Feature>
<Feature Id="External" AllowAdvertise="no" Absent="disallow">
{{#each component_group_refs as |id| ~}}
<ComponentGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each component_refs as |id| ~}}
<ComponentRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_group_refs as |id| ~}}
<FeatureGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_refs as |id| ~}}
<FeatureRef Id="{{ id }}"/>
{{/each~}}
{{#each merge_refs as |id| ~}}
<MergeRef Id="{{ id }}"/>
{{/each~}}
</Feature>
{{#if install_webview}}
<!-- WebView2 -->
<Property Id="WVRTINSTALLED">
<RegistrySearch Id="WVRTInstalledSystem" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" Win64="no" />
<RegistrySearch Id="WVRTInstalledUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
</Property>
{{#if download_bootstrapper}}
<CustomAction Id='DownloadAndInvokeBootstrapper' Directory="INSTALLDIR" Execute="deferred" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\{] [\[]Net.ServicePointManager[\]]::SecurityProtocol = [\[]Net.SecurityProtocolType[\]]::Tls12 [\}] catch [\{][\}]; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ({{webview_installer_args}} &apos;/install&apos;) -Wait' Return='check'/>
<InstallExecuteSequence>
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded webview bootstrapper mode -->
{{#if webview2_bootstrapper_path}}
<Binary Id="MicrosoftEdgeWebview2Setup.exe" SourceFile="{{webview2_bootstrapper_path}}"/>
<CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded offline installer -->
{{#if webview2_installer_path}}
<Binary Id="MicrosoftEdgeWebView2RuntimeInstaller.exe" SourceFile="{{webview2_installer_path}}"/>
<CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeStandalone' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
{{/if}}
{{#if enable_elevated_update_task}}
<!-- Install an elevated update task within Windows Task Scheduler -->
<CustomAction
Id="CreateUpdateTask"
Return="check"
Directory="INSTALLDIR"
Execute="commit"
Impersonate="yes"
ExeCommand="powershell.exe -WindowStyle hidden .\install-task.ps1" />
<InstallExecuteSequence>
<Custom Action='CreateUpdateTask' Before='InstallFinalize'>
NOT(REMOVE)
</Custom>
</InstallExecuteSequence>
<!-- Remove elevated update task during uninstall -->
<CustomAction
Id="DeleteUpdateTask"
Return="check"
Directory="INSTALLDIR"
ExeCommand="powershell.exe -WindowStyle hidden .\uninstall-task.ps1" />
<InstallExecuteSequence>
<Custom Action="DeleteUpdateTask" Before='InstallFinalize'>
(REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE
</Custom>
</InstallExecuteSequence>
{{/if}}
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
</Product>
</Wix>

View File

@@ -13,7 +13,6 @@ pub mod profile_create;
pub mod settings;
pub mod tags;
pub mod utils;
pub mod window_ext;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
@@ -33,6 +32,10 @@ pub enum TheseusSerializableError {
#[error("IO error: {0}")]
IO(#[from] std::io::Error),
#[cfg(target_os = "macos")]
#[error("Callback error: {0}")]
Callback(String),
}
// Generic implementation of From<T> for ErrorTypeA
@@ -80,6 +83,12 @@ macro_rules! impl_serialize {
}
// Use the macro to implement Serialize for TheseusSerializableError
#[cfg(target_os = "macos")]
impl_serialize! {
IO
IO,
Callback
}
#[cfg(not(target_os = "macos"))]
impl_serialize! {
IO,
}

View File

@@ -1,5 +1,7 @@
use theseus::{handler, prelude::CommandPayload, State};
use crate::api::Result;
use std::process::Command;
use std::{env, process::Command};
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("utils")
@@ -7,6 +9,9 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
should_disable_mouseover,
show_in_folder,
progress_bars_list,
safety_check_safe_loading_bars,
get_opening_command,
await_sync,
])
.build()
}
@@ -21,6 +26,12 @@ pub async fn progress_bars_list(
Ok(res)
}
// Check if there are any safe loading bars running
#[tauri::command]
pub async fn safety_check_safe_loading_bars() -> Result<bool> {
Ok(theseus::safety::check_safe_loading_bars().await?)
}
// cfg only on mac os
// disables mouseover and fixes a random crash error only fixed by recent versions of macos
#[cfg(target_os = "macos")]
@@ -83,3 +94,34 @@ pub fn show_in_folder(path: String) -> Result<()> {
Ok(())
}
// Get opening command
// For example, if a user clicks on an .mrpack to open the app.
// This should be called once and only when the app is done booting up and ready to receive a command
// Returns a Command struct- see events.js
#[tauri::command]
pub async fn get_opening_command() -> Result<Option<CommandPayload>> {
// Tauri is not CLI, we use arguments as path to file to call
let cmd_arg = env::args_os().nth(1);
let cmd_arg = cmd_arg.map(|path| path.to_string_lossy().to_string());
if let Some(cmd) = cmd_arg {
tracing::debug!("Opening command: {:?}", cmd);
return Ok(Some(handler::parse_command(&cmd).await?));
}
Ok(None)
}
// helper function called when redirected by a weblink (ie: modrith://do-something) or when redirected by a .mrpack file (in which case its a filepath)
// We hijack the deep link library (which also contains functionality for instance-checking)
pub async fn handle_command(command: String) -> Result<()> {
Ok(theseus::handler::parse_and_emit_command(&command).await?)
}
// Waits for state to be synced
#[tauri::command]
pub async fn await_sync() -> Result<()> {
State::sync().await?;
tracing::info!("State synced");
Ok(())
}

View File

@@ -0,0 +1,98 @@
use cocoa::{
base::{id, nil},
foundation::NSAutoreleasePool,
};
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use once_cell::sync::OnceCell;
use crate::api::TheseusSerializableError;
type Callback = OnceCell<Box<dyn Fn(String) + Send + Sync + 'static>>;
static CALLBACK: Callback = OnceCell::new();
pub struct AppDelegateClass(pub *const Class);
unsafe impl Send for AppDelegateClass {}
unsafe impl Sync for AppDelegateClass {}
// Obj C class for the app delegate
// This inherits from the TaoAppDelegate (used by tauri) so we do not accidentally override any functionality
// The application_open_file method is the only method we override, as it is currently unimplemented in tauri
lazy_static::lazy_static! {
pub static ref THESEUS_APP_DELEGATE_CLASS: AppDelegateClass = unsafe {
let superclass = class!(TaoAppDelegate);
let mut decl = ClassDecl::new("TheseusAppDelegate", superclass).unwrap();
// Add the method to the class
decl.add_method(
sel!(application:openFile:),
application_open_file as extern "C" fn(&Object, Sel, id, id) -> bool,
);
// Other methods are inherited
AppDelegateClass(decl.register())
};
}
extern "C" fn application_open_file(
_: &Object,
_: Sel,
_: id,
file: id,
) -> bool {
let file = nsstring_to_string(file);
callback(file)
}
pub fn callback(file: String) -> bool {
if let Some(callback) = CALLBACK.get() {
callback(file);
true
} else {
false
}
}
pub fn register_open_file<T>(
callback: T,
) -> Result<(), TheseusSerializableError>
where
T: Fn(String) + Send + Sync + 'static,
{
unsafe {
// Modified from tao: https://github.com/tauri-apps/tao
// sets the current app delegate to be the inherited app delegate rather than the default tauri/tao one
let app: id = msg_send![class!(TaoApp), sharedApplication];
let delegate: id = msg_send![THESEUS_APP_DELEGATE_CLASS.0, new];
let pool = NSAutoreleasePool::new(nil);
let _: () = msg_send![app, setDelegate: delegate];
let _: () = msg_send![pool, drain];
}
CALLBACK.set(Box::new(callback)).map_err(|_| {
TheseusSerializableError::Callback("Callback already set".to_string())
})
}
/// Convert an NSString to a Rust `String`
/// From 'fruitbasket' https://github.com/mrmekon/fruitbasket/
#[allow(clippy::cmp_null)]
pub fn nsstring_to_string(nsstring: *mut Object) -> String {
unsafe {
let cstr: *const i8 = msg_send![nsstring, UTF8String];
if cstr != std::ptr::null() {
std::ffi::CStr::from_ptr(cstr)
.to_string_lossy()
.into_owned()
} else {
"".into()
}
}
}

View File

@@ -0,0 +1,2 @@
pub mod delegate;
pub mod window_ext;

View File

@@ -12,7 +12,6 @@ pub trait WindowExt {
impl<R: Runtime> WindowExt for Window<R> {
fn set_transparent_titlebar(&self, transparent: bool) {
use cocoa::appkit::{NSWindow, NSWindowTitleVisibility};
let window = self.ns_window().unwrap() as cocoa::base::id;
unsafe {

View File

@@ -3,16 +3,15 @@
windows_subsystem = "windows"
)]
use theseus::prelude::*;
use tauri::Manager;
use tracing_error::ErrorLayer;
use tracing_subscriber::EnvFilter;
use theseus::prelude::*;
mod api;
mod error;
#[cfg(target_os = "macos")]
mod macos;
// Should be called in launcher initialization
#[tauri::command]
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
@@ -27,89 +26,95 @@ fn is_dev() -> bool {
cfg!(debug_assertions)
}
use tracing_subscriber::prelude::*;
#[derive(Clone, serde::Serialize)]
struct Payload {
args: Vec<String>,
cwd: String,
}
// if Tauri app is called with arguments, then those arguments will be treated as commands
// ie: deep links or filepaths for .mrpacks
fn main() {
//let client = sentry::init("https://19a14416dafc4b4a858fa1a38db3b704@o485889.ingest.sentry.io/4505349067374592");
tauri_plugin_deep_link::prepare("com.modrinth.theseus");
//let _guard = sentry_rust_minidump::init(&client);
/*
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
ERROR > WARN > INFO > DEBUG > TRACE
eg. RUST_LOG=info will show info, warn, and error logs
RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates)
RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates)
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
ERROR > WARN > INFO > DEBUG > TRACE
eg. RUST_LOG=info will show info, warn, and error logs
RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates)
RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates)
Error messages returned to Tauri will display as traced error logs if they return an error.
This will also include an attached span trace if the error is from a tracing error, and the level is set to info, debug, or trace
Error messages returned to Tauri will display as traced error logs if they return an error.
This will also include an attached span trace if the error is from a tracing error, and the level is set to info, debug, or trace
on unix:
RUST_LOG="theseus=trace" {run command}
on unix:
RUST_LOG="theseus=trace" {run command}
*/
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("theseus=info"));
let _log_guard = theseus::start_logger();
let subscriber = tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(filter)
.with(ErrorLayer::default());
tracing::info!("Initialized tracing subscriber. Loading Modrinth App!");
tracing::subscriber::set_global_default(subscriber)
.expect("setting default subscriber failed");
let mut builder = tauri::Builder::default()
let mut builder = tauri::Builder::default();
builder = builder
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
app.emit_all("single-instance", Payload { args: argv, cwd })
.unwrap();
}))
.plugin(tauri_plugin_window_state::Builder::default().build());
.plugin(tauri_plugin_window_state::Builder::default().build())
.setup(|app| {
// Register deep link handler, allowing reading of modrinth:// links
if let Err(e) = tauri_plugin_deep_link::register(
"modrinth",
|request: String| {
tauri::async_runtime::spawn(api::utils::handle_command(
request,
));
},
) {
// Allow it to fail- see https://github.com/FabianLars/tauri-plugin-deep-link/issues/19
tracing::error!("Error registering deep link handler: {}", e);
}
#[cfg(not(target_os = "macos"))]
{
builder = builder.setup(|app| {
let win = app.get_window("main").unwrap();
win.set_decorations(false).unwrap();
Ok(())
})
}
#[cfg(not(target_os = "linux"))]
{
use window_shadows::set_shadow;
set_shadow(&win, true).unwrap();
}
#[cfg(target_os = "macos")]
{
use macos::window_ext::WindowExt;
win.set_transparent_titlebar(true);
win.position_traffic_lights(9.0, 16.0);
}
#[cfg(not(target_os = "macos"))]
{
win.set_decorations(false).unwrap();
}
#[cfg(target_os = "macos")]
{
macos::delegate::register_open_file(|filename| {
tauri::async_runtime::spawn(api::utils::handle_command(
filename,
));
})
.unwrap();
}
#[cfg(not(target_os = "linux"))]
{
use window_shadows::set_shadow;
builder = builder.setup(|app| {
let win = app.get_window("main").unwrap();
set_shadow(&win, true).unwrap();
Ok(())
});
}
#[cfg(target_os = "macos")]
{
use tauri::WindowEvent;
builder = builder
.setup(|app| {
use api::window_ext::WindowExt;
let win = app.get_window("main").unwrap();
win.set_transparent_titlebar(true);
builder = builder.on_window_event(|e| {
use macos::window_ext::WindowExt;
if let WindowEvent::Resized(..) = e.event() {
let win = e.window();
win.position_traffic_lights(9.0, 16.0);
Ok(())
})
.on_window_event(|e| {
use api::window_ext::WindowExt;
if let WindowEvent::Resized(..) = e.event() {
let win = e.window();
win.position_traffic_lights(9.0, 16.0);
}
})
}
})
}
let builder = builder
.plugin(api::auth::init())

View File

@@ -13,6 +13,7 @@
"tauri": {
"allowlist": {
"dialog": {
"confirm": true,
"open": true
},
"protocol": {
@@ -75,7 +76,10 @@
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com"
"timestampUrl": "http://timestamp.digicert.com",
"wix": {
"template": "./msi/main.wxs"
}
}
},
"security": {

View File

@@ -29,6 +29,10 @@ import mixpanel from 'mixpanel-browser'
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
import OnboardingModal from '@/components/OnboardingModal.vue'
import { getVersion } from '@tauri-apps/api/app'
import { window } from '@tauri-apps/api'
import { TauriEvent } from '@tauri-apps/api/event'
import { await_sync, check_safe_loading_bars_complete } from './helpers/state'
import { confirm } from '@tauri-apps/api/dialog'
const themeStore = useTheming()
@@ -69,6 +73,33 @@ defineExpose({
},
})
const confirmClose = async () => {
const confirmed = await confirm(
'An action is currently in progress. Are you sure you want to exit?',
{
title: 'Modrinth',
type: 'warning',
}
)
return confirmed
}
const handleClose = async () => {
const isSafe = await check_safe_loading_bars_complete()
if (!isSafe) {
const response = await confirmClose()
if (!response) {
return
}
}
await await_sync()
window.getCurrent().close()
}
window.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
await handleClose()
})
const router = useRouter()
router.afterEach((to, from, failure) => {
if (mixpanel.__loaded) {
@@ -220,7 +251,7 @@ const accounts = ref(null)
@click="
() => {
saveWindowState(StateFlags.ALL)
appWindow.close()
handleClose()
}
"
>

View File

@@ -70,6 +70,19 @@ export async function profile_listener(callback) {
return await listen('profile', (event) => callback(event.payload))
}
/// Payload for the 'command' event
/*
CommandPayload {
event: event type ("InstallMod", "InstallModpack", "InstallVersion"),
id: string id of the mod/modpack/version to install
}
*/
export async function command_listener(callback) {
return await listen('command', (event) => {
callback(event.payload)
})
}
/// Payload for the 'warning' event
/*
WarningPayload {

View File

@@ -15,3 +15,21 @@ export async function initialize_state() {
export async function progress_bars_list() {
return await invoke('plugin:utils|progress_bars_list')
}
// Check if any safe loading bars are active
export async function check_safe_loading_bars_complete() {
return await invoke('plugin:utils|safety_check_safe_loading_bars')
}
// Get opening command
// For example, if a user clicks on an .mrpack to open the app.
// This should be called once and only when the app is done booting up and ready to receive a command
// Returns a Command struct- see events.js
export async function get_opening_command() {
return await invoke('plugin:utils|get_opening_command')
}
// Wait for settings to sync
export async function await_sync() {
return await invoke('plugin:utils|await_sync')
}

View File

@@ -6,8 +6,9 @@ import 'omorphia/dist/style.css'
import '@/assets/stylesheets/global.scss'
import 'floating-vue/dist/style.css'
import FloatingVue from 'floating-vue'
import { initialize_state } from '@/helpers/state'
import { get_opening_command, initialize_state } from '@/helpers/state'
import loadCssMixin from './mixins/macCssFix.js'
import { get } from '@/helpers/settings'
const pinia = createPinia()
@@ -20,7 +21,24 @@ app.mixin(loadCssMixin)
const mountedApp = app.mount('#app')
initialize_state()
.then(() => mountedApp.initialize())
.then(() => {
// First, redirect to other landing page if we have that setting
get()
.then((fetchSettings) => {
if (fetchSettings?.default_page && fetchSettings?.default_page !== 'Home') {
router.push({ name: fetchSettings?.default_page })
}
})
.catch((err) => {
console.error(err)
})
.finally(() => {
mountedApp.initialize()
get_opening_command().then((command) => {
console.log(JSON.stringify(command)) // change me to use whatever FE command handler is made
})
})
})
.catch((err) => {
console.error(err)
})

View File

@@ -7,6 +7,8 @@ import { get_max_memory } from '@/helpers/jre'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import mixpanel from 'mixpanel-browser'
const pageOptions = ['Home', 'Library']
const themeStore = useTheming()
const fetchSettings = await get().catch(handleError)
@@ -143,6 +145,43 @@ watch(
"
/>
</div>
<div class="adjacent-input">
<label for="minimize-launcher">
<span class="label__title">Minimize launcher</span>
<span class="label__description"
>Minimize the launcher when a Minecraft process starts.</span
>
</label>
<Toggle
id="minimize-launcher"
:model-value="settings.hide_on_process"
:checked="settings.hide_on_process"
@update:model-value="
(e) => {
settings.hide_on_process = e
}
"
/>
</div>
<div class="opening-page">
<label for="opening-page">
<span class="label__title">Default landing page</span>
<span class="label__description">Change the page to which the launcher opens on.</span>
</label>
<DropdownSelect
id="opening-page"
name="Opening page dropdown"
:options="pageOptions"
:default-value="settings.default_page"
:model-value="settings.default_page"
class="opening-page"
@change="
(e) => {
settings.default_page = e.option
}
"
/>
</div>
</Card>
<Card>
<div class="label">

View File

@@ -0,0 +1,3 @@
HTML Testing playground for Theseus:
<br><br><a href="modrinth://mod/test_id">Install mod 'test_id'</a>

View File

@@ -9,9 +9,6 @@ use theseus::prelude::*;
use theseus::profile_create::profile_create;
use tokio::time::{sleep, Duration};
use tracing_error::ErrorLayer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
// A simple Rust implementation of the authentication run
// 1) call the authenticate_begin_flow() function to get the URL to open (like you would in the frontend)
@@ -35,16 +32,7 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
async fn main() -> theseus::Result<()> {
println!("Starting.");
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("theseus=info"));
let subscriber = tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(filter)
.with(ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber)
.expect("setting default subscriber failed");
let _log_guard = theseus::start_logger();
// Initialize state
let st = State::get().await?;