From 91d3bf825d36767c420738ab2e70e0e725f22032 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Fri, 30 Jun 2023 08:11:32 -0700 Subject: [PATCH] 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 --- Cargo.lock | 162 +++++--- theseus/Cargo.toml | 6 +- theseus/src/api/handler.rs | 72 ++++ theseus/src/api/mod.rs | 3 + theseus/src/api/safety.rs | 5 + theseus/src/api/settings.rs | 1 + theseus/src/error.rs | 4 + theseus/src/event/emit.rs | 54 ++- theseus/src/event/mod.rs | 42 ++- theseus/src/launcher/mod.rs | 13 + theseus/src/lib.rs | 2 + theseus/src/logger.rs | 75 ++++ theseus/src/state/children.rs | 11 + theseus/src/state/dirs.rs | 11 +- theseus/src/state/mod.rs | 14 +- theseus/src/state/safe_processes.rs | 69 ++++ theseus/src/state/settings.rs | 22 +- theseus_gui/src-tauri/Cargo.toml | 11 +- theseus_gui/src-tauri/Info.plist | 63 ++++ theseus_gui/src-tauri/msi/main.wxs | 346 ++++++++++++++++++ theseus_gui/src-tauri/src/api/mod.rs | 13 +- theseus_gui/src-tauri/src/api/utils.rs | 44 ++- theseus_gui/src-tauri/src/macos/delegate.rs | 98 +++++ theseus_gui/src-tauri/src/macos/mod.rs | 2 + .../src/{api => macos}/window_ext.rs | 1 - theseus_gui/src-tauri/src/main.rs | 123 ++++--- theseus_gui/src-tauri/tauri.conf.json | 6 +- theseus_gui/src/App.vue | 33 +- theseus_gui/src/helpers/events.js | 13 + theseus_gui/src/helpers/state.js | 18 + theseus_gui/src/main.js | 22 +- theseus_gui/src/pages/Settings.vue | 39 ++ theseus_playground/link_test.html | 3 + theseus_playground/src/main.rs | 14 +- 34 files changed, 1258 insertions(+), 157 deletions(-) create mode 100644 theseus/src/api/handler.rs create mode 100644 theseus/src/api/safety.rs create mode 100644 theseus/src/logger.rs create mode 100644 theseus/src/state/safe_processes.rs create mode 100644 theseus_gui/src-tauri/Info.plist create mode 100644 theseus_gui/src-tauri/msi/main.wxs create mode 100644 theseus_gui/src-tauri/src/macos/delegate.rs create mode 100644 theseus_gui/src-tauri/src/macos/mod.rs rename theseus_gui/src-tauri/src/{api => macos}/window_ext.rs (99%) create mode 100644 theseus_playground/link_test.html diff --git a/Cargo.lock b/Cargo.lock index 9432370b9..0542cc180 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index 98de7da26..c24cafca0 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -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"} diff --git a/theseus/src/api/handler.rs b/theseus/src/api/handler.rs new file mode 100644 index 000000000..8180f16ff --- /dev/null +++ b/theseus/src/api/handler.rs @@ -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 { + 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 { + 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(()) +} diff --git a/theseus/src/api/mod.rs b/theseus/src/api/mod.rs index 009ad9f0b..ce1d77d2c 100644 --- a/theseus/src/api/mod.rs +++ b/theseus/src/api/mod.rs @@ -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, diff --git a/theseus/src/api/safety.rs b/theseus/src/api/safety.rs new file mode 100644 index 000000000..fb16312b0 --- /dev/null +++ b/theseus/src/api/safety.rs @@ -0,0 +1,5 @@ +use crate::state::{ProcessType, SafeProcesses}; + +pub async fn check_safe_loading_bars() -> crate::Result { + SafeProcesses::is_complete(ProcessType::LoadingBar).await +} diff --git a/theseus/src/api/settings.rs b/theseus/src/api/settings.rs index 1a6385ea0..e5486d57c 100644 --- a/theseus/src/api/settings.rs +++ b/theseus/src/api/settings.rs @@ -1,4 +1,5 @@ //! Theseus profile management interface + pub use crate::{ state::{ Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize, diff --git a/theseus/src/error.rs b/theseus/src/error.rs index d228ae3c5..16056524a 100644 --- a/theseus/src/error.rs +++ b/theseus/src/error.rs @@ -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)] diff --git a/theseus/src/event/emit.rs b/theseus/src/event/emit.rs index 9b4d1d933..54cb1968c 100644 --- a/theseus/src/event/emit.rs +++ b/theseus/src/event/emit.rs @@ -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 { + 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 { 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(()) } diff --git a/theseus/src/event/mod.rs b/theseus/src/event/mod.rs index 08879368a..27c3a0fd2 100644 --- a/theseus/src/event/mod.rs +++ b/theseus/src/event/mod.rs @@ -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> { + 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> { @@ -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> { - Self::init().await + #[cfg(feature = "tauri")] + pub async fn get_main_window() -> crate::Result> { + 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 diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index 8c84e5633..eacd54c5d 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -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; diff --git a/theseus/src/lib.rs b/theseus/src/lib.rs index 5268455bd..c2734d9f6 100644 --- a/theseus/src/lib.rs +++ b/theseus/src/lib.rs @@ -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; diff --git a/theseus/src/logger.rs b/theseus/src/logger.rs new file mode 100644 index 000000000..05e64f58c --- /dev/null +++ b/theseus/src/logger.rs @@ -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 { + 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 { + 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) +} diff --git a/theseus/src/state/children.rs b/theseus/src/state/children.rs index 19a5304a5..bbb7163d4 100644 --- a/theseus/src/state/children.rs +++ b/theseus/src/state/children.rs @@ -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, diff --git a/theseus/src/state/dirs.rs b/theseus/src/state/dirs.rs index 5ca86c0cd..f564e7d79 100644 --- a/theseus/src/state/dirs.rs +++ b/theseus/src/state/dirs.rs @@ -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 { + pub fn init() -> crate::Result { // 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 { diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index a51dd9d10..76b4623c9 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -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> = OnceCell::const_new(); pub struct State { @@ -75,6 +77,8 @@ pub struct State { pub(crate) users: RwLock, /// Launcher tags pub(crate) tags: RwLock, + /// Launcher processes that should be safely exited on shutdown + pub(crate) safety_processes: RwLock, /// File watcher debouncer pub(crate) file_watcher: RwLock>, @@ -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), })) } diff --git a/theseus/src/state/safe_processes.rs b/theseus/src/state/safe_processes.rs new file mode 100644 index 000000000..4f3e3c579 --- /dev/null +++ b/theseus/src/state/safe_processes.rs @@ -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, +} + +#[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 { + 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 { + 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) + } +} diff --git a/theseus/src/state/settings.rs b/theseus/src/state/settings.rs index eaa295b1e..c1de10697 100644 --- a/theseus/src/state/settings.rs +++ b/theseus/src/state/settings.rs @@ -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, } + +/// 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 + } +} diff --git a/theseus_gui/src-tauri/Cargo.toml b/theseus_gui/src-tauri/Cargo.toml index 13e4e67a9..9dbc7d789 100644 --- a/theseus_gui/src-tauri/Cargo.toml +++ b/theseus_gui/src-tauri/Cargo.toml @@ -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" diff --git a/theseus_gui/src-tauri/Info.plist b/theseus_gui/src-tauri/Info.plist new file mode 100644 index 000000000..bdb6467e3 --- /dev/null +++ b/theseus_gui/src-tauri/Info.plist @@ -0,0 +1,63 @@ + + + + + CFBundleURLTypes + + + CFBundleURLName + + com.modrinth.theseus + CFBundleURLSchemes + + + modrinth + + + + + CFBundleDocumentTypes + + + CFBundleTypeName + Modrinth type + CFBundleTypeRole + Editor + LSHandlerRank + Owner + LSItemContentTypes + + com.modrinth.theseus-type + + NSDocumentClass + NSDocument + + + UTImportedTypeDeclarations + + + UTTypeConformsTo + + public.data + + UTTypeDescription + Modrinth File + UTTypeIcons + + UTTypeIdentifier + com.modrinth.theseus-type + UTTypeTagSpecification + + public.filename-extension + + mrpack + + public.mime-type + + application/x-mrpack + + + + + + diff --git a/theseus_gui/src-tauri/msi/main.wxs b/theseus_gui/src-tauri/msi/main.wxs new file mode 100644 index 000000000..8137976d3 --- /dev/null +++ b/theseus_gui/src-tauri/msi/main.wxs @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + {{#if allow_downgrades}} + + {{else}} + + {{/if}} + + + Installed AND NOT UPGRADINGPRODUCTCODE + + + + + {{#if banner_path}} + + {{/if}} + {{#if dialog_image_path}} + + {{/if}} + {{#if license}} + + {{/if}} + + + + + + + + + + + + + + + + + + + + WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed + + + + {{#unless license}} + + 1 + 1 + {{/unless}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{#each binaries as |bin| ~}} + + + + {{/each~}} + {{#if enable_elevated_update_task}} + + + + + + + + + + {{/if}} + {{resources}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{#each merge_modules as |msm| ~}} + + + + + + + + {{/each~}} + + + + + + + + + + {{#each resource_file_ids as |resource_file_id| ~}} + + {{/each~}} + + {{#if enable_elevated_update_task}} + + + + {{/if}} + + + + + + + + + + + {{#each binaries as |bin| ~}} + + {{/each~}} + + + + + {{#each component_group_refs as |id| ~}} + + {{/each~}} + {{#each component_refs as |id| ~}} + + {{/each~}} + {{#each feature_group_refs as |id| ~}} + + {{/each~}} + {{#each feature_refs as |id| ~}} + + {{/each~}} + {{#each merge_refs as |id| ~}} + + {{/each~}} + + + {{#if install_webview}} + + + + + + + {{#if download_bootstrapper}} + + + + + + + {{/if}} + + + {{#if webview2_bootstrapper_path}} + + + + + + + + {{/if}} + + + {{#if webview2_installer_path}} + + + + + + + + {{/if}} + + {{/if}} + + {{#if enable_elevated_update_task}} + + + + + NOT(REMOVE) + + + + + + + (REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE + + + {{/if}} + + + + diff --git a/theseus_gui/src-tauri/src/api/mod.rs b/theseus_gui/src-tauri/src/api/mod.rs index de1d61d07..31ff2a90a 100644 --- a/theseus_gui/src-tauri/src/api/mod.rs +++ b/theseus_gui/src-tauri/src/api/mod.rs @@ -13,7 +13,6 @@ pub mod profile_create; pub mod settings; pub mod tags; pub mod utils; -pub mod window_ext; pub type Result = std::result::Result; @@ -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 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, } diff --git a/theseus_gui/src-tauri/src/api/utils.rs b/theseus_gui/src-tauri/src/api/utils.rs index edf8463a4..e3dfcb25f 100644 --- a/theseus_gui/src-tauri/src/api/utils.rs +++ b/theseus_gui/src-tauri/src/api/utils.rs @@ -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() -> tauri::plugin::TauriPlugin { tauri::plugin::Builder::new("utils") @@ -7,6 +9,9 @@ pub fn init() -> tauri::plugin::TauriPlugin { 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 { + 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> { + // 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(()) +} diff --git a/theseus_gui/src-tauri/src/macos/delegate.rs b/theseus_gui/src-tauri/src/macos/delegate.rs new file mode 100644 index 000000000..444121bc9 --- /dev/null +++ b/theseus_gui/src-tauri/src/macos/delegate.rs @@ -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>; + +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( + 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() + } + } +} diff --git a/theseus_gui/src-tauri/src/macos/mod.rs b/theseus_gui/src-tauri/src/macos/mod.rs new file mode 100644 index 000000000..229c698ce --- /dev/null +++ b/theseus_gui/src-tauri/src/macos/mod.rs @@ -0,0 +1,2 @@ +pub mod delegate; +pub mod window_ext; diff --git a/theseus_gui/src-tauri/src/api/window_ext.rs b/theseus_gui/src-tauri/src/macos/window_ext.rs similarity index 99% rename from theseus_gui/src-tauri/src/api/window_ext.rs rename to theseus_gui/src-tauri/src/macos/window_ext.rs index 67c4cbd76..ca2866242 100644 --- a/theseus_gui/src-tauri/src/api/window_ext.rs +++ b/theseus_gui/src-tauri/src/macos/window_ext.rs @@ -12,7 +12,6 @@ pub trait WindowExt { impl WindowExt for Window { fn set_transparent_titlebar(&self, transparent: bool) { use cocoa::appkit::{NSWindow, NSWindowTitleVisibility}; - let window = self.ns_window().unwrap() as cocoa::base::id; unsafe { diff --git a/theseus_gui/src-tauri/src/main.rs b/theseus_gui/src-tauri/src/main.rs index 99fbb6ba0..99d90b9c1 100644 --- a/theseus_gui/src-tauri/src/main.rs +++ b/theseus_gui/src-tauri/src/main.rs @@ -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, 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()) diff --git a/theseus_gui/src-tauri/tauri.conf.json b/theseus_gui/src-tauri/tauri.conf.json index 77a9031f1..644ace2b8 100644 --- a/theseus_gui/src-tauri/tauri.conf.json +++ b/theseus_gui/src-tauri/tauri.conf.json @@ -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": { diff --git a/theseus_gui/src/App.vue b/theseus_gui/src/App.vue index 8dd49f0b8..298206330 100644 --- a/theseus_gui/src/App.vue +++ b/theseus_gui/src/App.vue @@ -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() } " > diff --git a/theseus_gui/src/helpers/events.js b/theseus_gui/src/helpers/events.js index 473c2549b..2f065940a 100644 --- a/theseus_gui/src/helpers/events.js +++ b/theseus_gui/src/helpers/events.js @@ -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 { diff --git a/theseus_gui/src/helpers/state.js b/theseus_gui/src/helpers/state.js index 4774544ab..9a246f0b4 100644 --- a/theseus_gui/src/helpers/state.js +++ b/theseus_gui/src/helpers/state.js @@ -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') +} diff --git a/theseus_gui/src/main.js b/theseus_gui/src/main.js index 75e1c23a0..fcdc1ac61 100644 --- a/theseus_gui/src/main.js +++ b/theseus_gui/src/main.js @@ -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) }) diff --git a/theseus_gui/src/pages/Settings.vue b/theseus_gui/src/pages/Settings.vue index 71e6a50d4..7377ab478 100644 --- a/theseus_gui/src/pages/Settings.vue +++ b/theseus_gui/src/pages/Settings.vue @@ -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( " /> +
+ + +
+
+ + +
diff --git a/theseus_playground/link_test.html b/theseus_playground/link_test.html new file mode 100644 index 000000000..a3a264a98 --- /dev/null +++ b/theseus_playground/link_test.html @@ -0,0 +1,3 @@ +HTML Testing playground for Theseus: + +

Install mod 'test_id' diff --git a/theseus_playground/src/main.rs b/theseus_playground/src/main.rs index fcef62807..0542b4f0c 100644 --- a/theseus_playground/src/main.rs +++ b/theseus_playground/src/main.rs @@ -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 { 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?;