From 4445f736d9b192e45df49538919ba07efa3b40c9 Mon Sep 17 00:00:00 2001 From: MiguVT Date: Thu, 14 Aug 2025 18:08:11 +0200 Subject: [PATCH 01/15] feat: add portable mode detection and configuration handling --- apps/app/src/main.rs | 17 +++++++++++++++ packages/app-lib/src/state/dirs.rs | 33 ++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 09723d39f9..ccd069513a 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -125,6 +125,22 @@ fn is_dev() -> bool { cfg!(debug_assertions) } +#[tauri::command] +fn is_portable() -> bool { + // Check for portable mode indicators + if std::env::var("MODRINTH_PORTABLE").is_ok() { + return true; + } + + if let Ok(exe_path) = std::env::current_exe() { + if let Some(exe_dir) = exe_path.parent() { + return exe_dir.join("portable.txt").exists(); + } + } + + false +} + // Toggles decorations #[tauri::command] async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> { @@ -264,6 +280,7 @@ fn main() { .invoke_handler(tauri::generate_handler![ initialize_state, is_dev, + is_portable, toggle_decorations, show_window, restart_app, diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index 1f577f7c45..11e4003fb7 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -23,10 +23,33 @@ impl DirectoryInfo { // Get the settings directory // init() is not needed for this function pub fn get_initial_settings_dir() -> Option { + // Check for portable mode first + if let Some(portable_dir) = Self::get_portable_dir() { + return Some(portable_dir); + } + Self::env_path("THESEUS_CONFIG_DIR") .or_else(|| Some(dirs::data_dir()?.join("ModrinthApp"))) } + /// Check if we're running in portable mode and return the portable directory + fn get_portable_dir() -> Option { + let exe_path = std::env::current_exe().ok()?; + let exe_dir = exe_path.parent()?; + + // Check if MODRINTH_PORTABLE environment variable is set + if std::env::var("MODRINTH_PORTABLE").is_ok() { + return Some(exe_dir.join("ModrinthAppData")); + } + + // Check if a "portable.txt" file exists next to the executable + if exe_dir.join("portable.txt").exists() { + return Some(exe_dir.join("ModrinthAppData")); + } + + None + } + /// Get all paths needed for Theseus to operate properly #[tracing::instrument] pub async fn init(config_dir: Option) -> crate::Result { @@ -42,8 +65,14 @@ impl DirectoryInfo { )) })?; - let config_dir = - config_dir.map_or_else(|| settings_dir.clone(), PathBuf::from); + let config_dir = config_dir.map_or_else(|| { + // In portable mode, use the same directory for both settings and config + if Self::get_portable_dir().is_some() { + settings_dir.clone() + } else { + settings_dir.clone() + } + }, PathBuf::from); Ok(Self { settings_dir, From c3b4b2846cb13711dca65d993511986f7494f240 Mon Sep 17 00:00:00 2001 From: MiguVT Date: Thu, 14 Aug 2025 18:19:08 +0200 Subject: [PATCH 02/15] feat: add documentation for portable mode functionality and usage --- .../src/content/docs/guide/portable-mode.md | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 apps/docs/src/content/docs/guide/portable-mode.md diff --git a/apps/docs/src/content/docs/guide/portable-mode.md b/apps/docs/src/content/docs/guide/portable-mode.md new file mode 100644 index 0000000000..972e29f9e0 --- /dev/null +++ b/apps/docs/src/content/docs/guide/portable-mode.md @@ -0,0 +1,187 @@ +--- +title: Portable Mode +description: Learn how to use Modrinth App in portable mode for maximum flexibility and convenience. +--- + +The Modrinth App supports **portable mode**, which allows you to run the application from any location without requiring installation to system directories. In portable mode, all application data is stored alongside the executable, making it perfect for USB drives, shared computers, or when you want a completely self-contained setup. + +## What is Portable Mode? + +Portable mode changes how the Modrinth App stores its data. Instead of using system directories like: + +- `%APPDATA%\ModrinthApp` (Windows) +- `~/Library/Application Support/ModrinthApp` (macOS) +- `~/.local/share/ModrinthApp` (Linux) + +All data is stored in a `ModrinthAppData` folder right next to the executable, making the entire installation truly portable. + +## Benefits of Portable Mode + +- **No Installation Required**: Run directly from any location +- **System Independence**: No registry entries or system directory modifications +- **Easy Backup**: Simply copy the entire folder to backup everything +- **Multi-System**: Use the same setup across different computers +- **Clean Removal**: Delete the folder to completely remove all traces +- **Isolation**: Perfect for testing without affecting main installations + +## Enabling Portable Mode + +There are two methods to enable portable mode: + +### Method 1: portable.txt File (Recommended) + +1. Create an empty file named `portable.txt` in the same directory as your Modrinth App executable +2. Launch the app normally +3. The app will automatically detect the file and enable portable mode + +``` +YourFolder/ +├── Modrinth App.exe (or modrinth-app on Linux/macOS) +├── portable.txt ← Create this file +└── ModrinthAppData/ ← Created automatically on first run +``` + +### Method 2: Environment Variable + +Set the `MODRINTH_PORTABLE` environment variable to any value before launching the app: + +```bash +# Windows (PowerShell) +$env:MODRINTH_PORTABLE="true" +.\ModrinthApp.exe + +# Windows (Command Prompt) +set MODRINTH_PORTABLE=true +ModrinthApp.exe + +# Linux/macOS +export MODRINTH_PORTABLE=true +./modrinth-app +``` + +## Directory Structure + +When portable mode is enabled, your directory structure will look like this: + +``` +YourPortableFolder/ +├── Modrinth App.exe # The application executable +├── portable.txt # Enables portable mode (Method 1) +└── ModrinthAppData/ # All app data stored here + ├── profiles/ # Minecraft instances and profiles + │ ├── vanilla/ + │ ├── modded/ + │ └── ... + ├── caches/ # Downloaded files cache + │ ├── mods/ + │ ├── resource_packs/ + │ └── ... + ├── launcher_logs/ # Application logs + ├── meta/ # Metadata files + └── app.db # Application database +``` + +## Creating a Portable Installation + +### From Source (Developers) + +If you're building from source: + +1. Build the application: + + ```bash + pnpm app:build + ``` + +2. Copy the executable from `target/release/` to your portable folder + +3. Create the portable indicator: + ```bash + # Create portable.txt file + echo "Portable mode enabled" > portable.txt + ``` + +### From Release Binary + +1. Download the Modrinth App executable from [modrinth.com/app](https://modrinth.com/app) +2. Place it in your desired portable folder +3. Create an empty `portable.txt` file next to the executable +4. Launch the app + +## Use Cases + +### USB Drive Setup + +Perfect for carrying your complete Minecraft setup on a USB drive: + +``` +USB_Drive/ +├── ModrinthApp/ +│ ├── Modrinth App.exe +│ ├── portable.txt +│ └── ModrinthAppData/ +└── other_files/ +``` + +### Shared Computer + +Use your personal setup on shared computers without affecting other users or requiring installation permissions. + +### Testing Environment + +Test different modrinth configurations or app versions without affecting your main installation. + +### System Migration + +Easily move your entire Minecraft setup to a new computer by copying the portable folder. + +## Switching Between Modes + +### Converting Regular Installation to Portable + +1. Create a portable folder with the executable and `portable.txt` +2. Copy your existing data from the system directory to `ModrinthAppData/` +3. Launch the portable version + +### Converting Portable to Regular Installation + +1. Install the app normally +2. Copy data from `ModrinthAppData/` to the system directory +3. Remove the `portable.txt` file or unset the environment variable + +## Technical Notes + +- **Detection Priority**: Environment variable takes precedence over `portable.txt` file +- **Performance**: Portable mode has identical performance to regular installations +- **Platform Support**: Available on Windows, macOS, and Linux +- **First Launch**: The `ModrinthAppData` directory is created automatically on first run +- **File Permissions**: Ensure the portable folder has write permissions + +## Troubleshooting + +### App Not Detecting Portable Mode + +- Verify `portable.txt` is in the same directory as the executable +- Check file permissions on the portable folder +- Try using the environment variable method instead + +### Data Not Saving + +- Ensure the portable folder has write permissions +- Check that antivirus software isn't blocking file creation +- Verify there's sufficient disk space + +### Performance Issues + +- Portable mode shouldn't affect performance +- If using a USB drive, ensure it has adequate read/write speeds +- Consider using USB 3.0+ for better performance + +## Security Considerations + +- **Portable Storage**: Be mindful of where you store portable installations +- **Data Encryption**: Consider encrypting USB drives with sensitive data +- **Access Control**: Set appropriate file permissions on shared systems +- **Backup**: Regularly backup your portable installation + +Portable mode makes the Modrinth App incredibly flexible and convenient for various use cases while maintaining all the functionality of a standard installation. From 37e5c87d7ae1bd94e88f7b3e65cd9260b4ca327d Mon Sep 17 00:00:00 2001 From: MiguVT Date: Thu, 14 Aug 2025 23:01:05 +0200 Subject: [PATCH 03/15] refactor: simplify config directory handling in portable mode --- packages/app-lib/src/state/dirs.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index 11e4003fb7..8dc4212dc5 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -67,11 +67,7 @@ impl DirectoryInfo { let config_dir = config_dir.map_or_else(|| { // In portable mode, use the same directory for both settings and config - if Self::get_portable_dir().is_some() { - settings_dir.clone() - } else { - settings_dir.clone() - } + settings_dir.clone() }, PathBuf::from); Ok(Self { From 98854eb6a31755980d645b0555aa9b76b381c4ae Mon Sep 17 00:00:00 2001 From: MiguVT Date: Fri, 15 Aug 2025 23:06:22 +0200 Subject: [PATCH 04/15] Improves portable mode handling Centralizes portable mode detection and setup to ensure consistent behavior across the application, especially for the window state plugin and directory lookups. This change introduces a `setup_portable_env_vars` function that sets environment variables based on the detected portable directory. It ensures that `tauri-plugin-window-state` and `dirs.rs` correctly use the portable directory for storing application data and configuration. The logic for detecting portable mode has been moved to `DirectoryInfo`. The `is_portable` command in `main.rs` now utilizes the centralized portable directory check. The portable mode detection logic has been removed from `DirectoryInfo::get_initial_settings_dir` and `is_portable` to prevent duplicated checks. --- apps/app/src/main.rs | 60 ++++++++++++++++++++++++------ packages/app-lib/src/state/dirs.rs | 29 +-------------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index ccd069513a..0391060f25 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -14,6 +14,47 @@ mod error; #[cfg(target_os = "macos")] mod macos; +/// Set up environment variables for portable mode to ensure the window state plugin +/// respects the portable directory configuration. +fn setup_portable_env_vars() { + // Use the same portable detection logic as DirectoryInfo + let portable_dir = match theseus::state::DirectoryInfo::get_portable_dir() { + Some(dir) => dir, + None => return, // Not in portable mode + }; + + // Override the appropriate environment variable for each platform + // This ensures both tauri-plugin-window-state and dirs.rs use the portable directory + #[cfg(target_os = "windows")] + { + unsafe { + std::env::set_var("APPDATA", &portable_dir); + } + // Note: We can't use tracing yet since logger isn't initialized + eprintln!("Set APPDATA to {} for portable mode", portable_dir.display()); + } + + #[cfg(target_os = "macos")] + { + let config_dir = portable_dir.join("Library").join("Application Support"); + let _ = std::fs::create_dir_all(&config_dir); + unsafe { + std::env::set_var("HOME", &portable_dir); + } + eprintln!("Set HOME to {} for portable mode", portable_dir.display()); + } + + #[cfg(target_os = "linux")] + { + let config_dir = portable_dir.join(".config"); + let _ = std::fs::create_dir_all(&config_dir); + unsafe { + std::env::set_var("XDG_CONFIG_HOME", &config_dir); + } + eprintln!("Set XDG_CONFIG_HOME to {} for portable mode", config_dir.display()); + } +} + // Should be called in launcher initialization #[tracing::instrument(skip_all)] #[tauri::command] @@ -127,18 +168,8 @@ fn is_dev() -> bool { #[tauri::command] fn is_portable() -> bool { - // Check for portable mode indicators - if std::env::var("MODRINTH_PORTABLE").is_ok() { - return true; - } - - if let Ok(exe_path) = std::env::current_exe() { - if let Some(exe_dir) = exe_path.parent() { - return exe_dir.join("portable.txt").exists(); - } - } - - false + // Use the same portable detection logic as DirectoryInfo + theseus::state::DirectoryInfo::get_portable_dir().is_some() } // Toggles decorations @@ -174,6 +205,11 @@ fn main() { RUST_LOG="theseus=trace" {run command} */ + + // Set up portable mode environment variables FIRST, before any other initialization + // This ensures both dirs.rs and window state plugin use the portable directory consistently + setup_portable_env_vars(); + let _log_guard = theseus::start_logger(); tracing::info!("Initialized tracing subscriber. Loading Modrinth App!"); diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index 8dc4212dc5..1f577f7c45 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -23,33 +23,10 @@ impl DirectoryInfo { // Get the settings directory // init() is not needed for this function pub fn get_initial_settings_dir() -> Option { - // Check for portable mode first - if let Some(portable_dir) = Self::get_portable_dir() { - return Some(portable_dir); - } - Self::env_path("THESEUS_CONFIG_DIR") .or_else(|| Some(dirs::data_dir()?.join("ModrinthApp"))) } - /// Check if we're running in portable mode and return the portable directory - fn get_portable_dir() -> Option { - let exe_path = std::env::current_exe().ok()?; - let exe_dir = exe_path.parent()?; - - // Check if MODRINTH_PORTABLE environment variable is set - if std::env::var("MODRINTH_PORTABLE").is_ok() { - return Some(exe_dir.join("ModrinthAppData")); - } - - // Check if a "portable.txt" file exists next to the executable - if exe_dir.join("portable.txt").exists() { - return Some(exe_dir.join("ModrinthAppData")); - } - - None - } - /// Get all paths needed for Theseus to operate properly #[tracing::instrument] pub async fn init(config_dir: Option) -> crate::Result { @@ -65,10 +42,8 @@ impl DirectoryInfo { )) })?; - let config_dir = config_dir.map_or_else(|| { - // In portable mode, use the same directory for both settings and config - settings_dir.clone() - }, PathBuf::from); + let config_dir = + config_dir.map_or_else(|| settings_dir.clone(), PathBuf::from); Ok(Self { settings_dir, From 42525c9d8f789ee2493476310270901a2502bf51 Mon Sep 17 00:00:00 2001 From: MiguVT Date: Fri, 15 Aug 2025 23:16:33 +0200 Subject: [PATCH 05/15] Consolidates portable mode detection Unifies portable mode detection logic into a single function and reuses it across the application. This ensures consistency in determining whether the application is running in portable mode, particularly for setting environment variables and checking portable status. --- apps/app/src/main.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 0391060f25..ddf104bc9b 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -14,11 +14,29 @@ mod error; #[cfg(target_os = "macos")] mod macos; +/// Check if we're running in portable mode and return the portable directory +fn get_portable_dir() -> Option { + let exe_path = std::env::current_exe().ok()?; + let exe_dir = exe_path.parent()?; + + // Method 1: Check if MODRINTH_PORTABLE environment variable is set + if std::env::var("MODRINTH_PORTABLE").is_ok() { + return Some(exe_dir.join("ModrinthAppData")); + } + + // Method 2: Check if a "portable.txt" file exists next to the executable + if exe_dir.join("portable.txt").exists() { + return Some(exe_dir.join("ModrinthAppData")); + } + + None +} + /// Set up environment variables for portable mode to ensure the window state plugin /// respects the portable directory configuration. fn setup_portable_env_vars() { - // Use the same portable detection logic as DirectoryInfo - let portable_dir = match theseus::state::DirectoryInfo::get_portable_dir() { + // Check if we're running in portable mode + let portable_dir = match get_portable_dir() { Some(dir) => dir, None => return, // Not in portable mode }; @@ -168,8 +186,8 @@ fn is_dev() -> bool { #[tauri::command] fn is_portable() -> bool { - // Use the same portable detection logic as DirectoryInfo - theseus::state::DirectoryInfo::get_portable_dir().is_some() + // Use the same portable detection logic + get_portable_dir().is_some() } // Toggles decorations From 6bbe5aa61b6ba3f5e6ff6063614605fcfe72eae5 Mon Sep 17 00:00:00 2001 From: MiguVT Date: Sat, 16 Aug 2025 14:36:07 +0200 Subject: [PATCH 06/15] Enhances portable mode support by adding environment variable checks and setting process environment variables for portable data directory --- packages/app-lib/src/state/dirs.rs | 68 +++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index 1f577f7c45..fea043d551 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -5,6 +5,8 @@ use crate::state::{JavaVersion, Profile, Settings}; use crate::util::fetch::IoSemaphore; use dashmap::DashSet; use std::path::{Path, PathBuf}; +// use std::fs as stdfs; // (removed, unused) +use std::env; use std::sync::Arc; use tokio::fs; @@ -22,9 +24,71 @@ pub struct DirectoryInfo { impl DirectoryInfo { // Get the settings directory // init() is not needed for this function + /// Returns the settings dir, considering portable mode if enabled. pub fn get_initial_settings_dir() -> Option { - Self::env_path("THESEUS_CONFIG_DIR") - .or_else(|| Some(dirs::data_dir()?.join("ModrinthApp"))) + // 1. Check for THESEUS_CONFIG_DIR override + if let Some(path) = Self::env_path("THESEUS_CONFIG_DIR") { + return Some(path); + } + + // 2. Check for portable mode (env or portable.txt) + if Self::is_portable_mode() { + if let Some(portable_dir) = Self::portable_data_dir() { + // Set process env vars for this process to use portable dir + Self::set_portable_envs(&portable_dir); + return Some(portable_dir); + } + } + + // 3. Default: system data dir + Some(dirs::data_dir()?.join("ModrinthApp")) + } + + /// Returns true if portable mode is enabled (env or portable.txt) + fn is_portable_mode() -> bool { + // Environment variable takes precedence + if env::var_os("MODRINTH_PORTABLE").is_some() { + return true; + } + // Check for portable.txt next to the executable + if let Ok(exe_path) = env::current_exe() { + let portable_txt = exe_path.parent().map(|p| p.join("portable.txt")); + if let Some(ref path) = portable_txt { + if path.exists() { + return true; + } + } + } + false + } + + /// Returns the portable data dir (next to exe, ModrinthAppData) + fn portable_data_dir() -> Option { + let exe_dir = env::current_exe().ok()?.parent()?.to_path_buf(); + Some(exe_dir.join("ModrinthAppData")) + } + + /// Sets process env vars to use the portable data dir for this process + fn set_portable_envs(portable_dir: &Path) { + // Windows: APPDATA, LOCALAPPDATA + // Linux: XDG_DATA_HOME + // macOS: HOME/Library/Application Support + #[cfg(target_os = "windows")] + unsafe { + env::set_var("APPDATA", portable_dir); + env::set_var("LOCALAPPDATA", portable_dir); + } + #[cfg(target_os = "linux")] + { + env::set_var("XDG_DATA_HOME", portable_dir); + } + #[cfg(target_os = "macos")] + { + // Not perfect, but set HOME to exe dir for child processes + if let Some(parent) = portable_dir.parent() { + env::set_var("HOME", parent); + } + } } /// Get all paths needed for Theseus to operate properly From 22f27ca52605c2dd2d43054dbe158434558c3989 Mon Sep 17 00:00:00 2001 From: MiguVT Date: Sat, 16 Aug 2025 14:42:00 +0200 Subject: [PATCH 07/15] Refactors portable mode handling by consolidating environment variable setup and directory checks --- apps/app/src/main.rs | 71 ------------------------ packages/app-lib/src/state/dirs.rs | 86 ++++++++++++------------------ 2 files changed, 34 insertions(+), 123 deletions(-) diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index ddf104bc9b..09723d39f9 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -14,65 +14,6 @@ mod error; #[cfg(target_os = "macos")] mod macos; -/// Check if we're running in portable mode and return the portable directory -fn get_portable_dir() -> Option { - let exe_path = std::env::current_exe().ok()?; - let exe_dir = exe_path.parent()?; - - // Method 1: Check if MODRINTH_PORTABLE environment variable is set - if std::env::var("MODRINTH_PORTABLE").is_ok() { - return Some(exe_dir.join("ModrinthAppData")); - } - - // Method 2: Check if a "portable.txt" file exists next to the executable - if exe_dir.join("portable.txt").exists() { - return Some(exe_dir.join("ModrinthAppData")); - } - - None -} - -/// Set up environment variables for portable mode to ensure the window state plugin -/// respects the portable directory configuration. -fn setup_portable_env_vars() { - // Check if we're running in portable mode - let portable_dir = match get_portable_dir() { - Some(dir) => dir, - None => return, // Not in portable mode - }; - - // Override the appropriate environment variable for each platform - // This ensures both tauri-plugin-window-state and dirs.rs use the portable directory - #[cfg(target_os = "windows")] - { - unsafe { - std::env::set_var("APPDATA", &portable_dir); - } - // Note: We can't use tracing yet since logger isn't initialized - eprintln!("Set APPDATA to {} for portable mode", portable_dir.display()); - } - - #[cfg(target_os = "macos")] - { - let config_dir = portable_dir.join("Library").join("Application Support"); - let _ = std::fs::create_dir_all(&config_dir); - unsafe { - std::env::set_var("HOME", &portable_dir); - } - eprintln!("Set HOME to {} for portable mode", portable_dir.display()); - } - - #[cfg(target_os = "linux")] - { - let config_dir = portable_dir.join(".config"); - let _ = std::fs::create_dir_all(&config_dir); - unsafe { - std::env::set_var("XDG_CONFIG_HOME", &config_dir); - } - eprintln!("Set XDG_CONFIG_HOME to {} for portable mode", config_dir.display()); - } -} - // Should be called in launcher initialization #[tracing::instrument(skip_all)] #[tauri::command] @@ -184,12 +125,6 @@ fn is_dev() -> bool { cfg!(debug_assertions) } -#[tauri::command] -fn is_portable() -> bool { - // Use the same portable detection logic - get_portable_dir().is_some() -} - // Toggles decorations #[tauri::command] async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> { @@ -223,11 +158,6 @@ fn main() { RUST_LOG="theseus=trace" {run command} */ - - // Set up portable mode environment variables FIRST, before any other initialization - // This ensures both dirs.rs and window state plugin use the portable directory consistently - setup_portable_env_vars(); - let _log_guard = theseus::start_logger(); tracing::info!("Initialized tracing subscriber. Loading Modrinth App!"); @@ -334,7 +264,6 @@ fn main() { .invoke_handler(tauri::generate_handler![ initialize_state, is_dev, - is_portable, toggle_decorations, show_window, restart_app, diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index fea043d551..13119529ab 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -5,7 +5,6 @@ use crate::state::{JavaVersion, Profile, Settings}; use crate::util::fetch::IoSemaphore; use dashmap::DashSet; use std::path::{Path, PathBuf}; -// use std::fs as stdfs; // (removed, unused) use std::env; use std::sync::Arc; use tokio::fs; @@ -22,73 +21,56 @@ pub struct DirectoryInfo { } impl DirectoryInfo { - // Get the settings directory - // init() is not needed for this function - /// Returns the settings dir, considering portable mode if enabled. - pub fn get_initial_settings_dir() -> Option { - // 1. Check for THESEUS_CONFIG_DIR override - if let Some(path) = Self::env_path("THESEUS_CONFIG_DIR") { - return Some(path); - } - - // 2. Check for portable mode (env or portable.txt) - if Self::is_portable_mode() { - if let Some(portable_dir) = Self::portable_data_dir() { - // Set process env vars for this process to use portable dir - Self::set_portable_envs(&portable_dir); - return Some(portable_dir); - } - } - - // 3. Default: system data dir - Some(dirs::data_dir()?.join("ModrinthApp")) + /// Returns the path to the directory containing the running executable, if possible. + fn exe_dir() -> Option { + std::env::current_exe().ok().and_then(|p| p.parent().map(|p| p.to_path_buf())) } - /// Returns true if portable mode is enabled (env or portable.txt) - fn is_portable_mode() -> bool { - // Environment variable takes precedence + /// Checks if portable mode is enabled (env or portable.txt) and returns the portable data dir if so. + fn portable_data_dir() -> Option { + // 1. Environment variable takes precedence if env::var_os("MODRINTH_PORTABLE").is_some() { - return true; + if let Some(exe_dir) = Self::exe_dir() { + return Some(exe_dir.join("ModrinthAppData")); + } } - // Check for portable.txt next to the executable - if let Ok(exe_path) = env::current_exe() { - let portable_txt = exe_path.parent().map(|p| p.join("portable.txt")); - if let Some(ref path) = portable_txt { - if path.exists() { - return true; - } + // 2. portable.txt file next to executable + if let Some(exe_dir) = Self::exe_dir() { + let portable_txt = exe_dir.join("portable.txt"); + if portable_txt.exists() { + return Some(exe_dir.join("ModrinthAppData")); } } - false - } - - /// Returns the portable data dir (next to exe, ModrinthAppData) - fn portable_data_dir() -> Option { - let exe_dir = env::current_exe().ok()?.parent()?.to_path_buf(); - Some(exe_dir.join("ModrinthAppData")) + None } - /// Sets process env vars to use the portable data dir for this process - fn set_portable_envs(portable_dir: &Path) { - // Windows: APPDATA, LOCALAPPDATA - // Linux: XDG_DATA_HOME - // macOS: HOME/Library/Application Support + /// Sets the process environment variable for app data (e.g., APPDATA) to the portable dir if in portable mode. + fn set_portable_env(portable_dir: &Path) { #[cfg(target_os = "windows")] unsafe { env::set_var("APPDATA", portable_dir); - env::set_var("LOCALAPPDATA", portable_dir); } - #[cfg(target_os = "linux")] + #[cfg(target_os = "macos")] { + // macOS typically uses HOME/Library/Application Support, but we can override XDG_DATA_HOME for some libs env::set_var("XDG_DATA_HOME", portable_dir); } - #[cfg(target_os = "macos")] + #[cfg(target_os = "linux")] { - // Not perfect, but set HOME to exe dir for child processes - if let Some(parent) = portable_dir.parent() { - env::set_var("HOME", parent); - } + env::set_var("XDG_DATA_HOME", portable_dir); + } + } + + // Get the settings directory + // init() is not needed for this function + pub fn get_initial_settings_dir() -> Option { + if let Some(portable_dir) = Self::portable_data_dir() { + // Set env var for the process so all code uses portable dir + Self::set_portable_env(&portable_dir); + return Some(portable_dir); } + Self::env_path("THESEUS_CONFIG_DIR") + .or_else(|| Some(dirs::data_dir()?.join("ModrinthApp"))) } /// Get all paths needed for Theseus to operate properly @@ -232,7 +214,7 @@ impl DirectoryInfo { /// Get path from environment variable #[inline] fn env_path(name: &str) -> Option { - std::env::var_os(name).map(PathBuf::from) + env::var_os(name).map(PathBuf::from) } #[tracing::instrument(skip(settings, exec, io_semaphore))] From d39c3b4588c75e796b7732d62644cbb841ace7fe Mon Sep 17 00:00:00 2001 From: MiguVT Date: Sat, 16 Aug 2025 15:13:46 +0200 Subject: [PATCH 08/15] Refactors environment variable setup for macOS and Linux in portable mode to use unsafe blocks --- packages/app-lib/src/state/dirs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index 13119529ab..c9cb9b6acd 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -51,12 +51,12 @@ impl DirectoryInfo { env::set_var("APPDATA", portable_dir); } #[cfg(target_os = "macos")] - { + unsafe { // macOS typically uses HOME/Library/Application Support, but we can override XDG_DATA_HOME for some libs env::set_var("XDG_DATA_HOME", portable_dir); } #[cfg(target_os = "linux")] - { + unsafe { env::set_var("XDG_DATA_HOME", portable_dir); } } From e1ca7e4bd25cd8a94f93e75687743e8d6f3bb1a2 Mon Sep 17 00:00:00 2001 From: MiguVT Date: Sat, 16 Aug 2025 15:42:42 +0200 Subject: [PATCH 09/15] Adds Windows executable to the artifact upload step in the build workflow --- .github/workflows/theseus-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 64ae2b3349..1eced404b6 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -150,3 +150,4 @@ jobs: target/universal-apple-darwin/release/bundle/dmg/Modrinth App_*.dmg* target/release/bundle/nsis/Modrinth App_*-setup.exe* target/release/bundle/nsis/Modrinth App_*-setup.nsis.zip* + target/release/Modrinth App.exe From ef4dc7dce0869547165af6209d60e8d5e59548bd Mon Sep 17 00:00:00 2001 From: MiguVT <71216796+MiguVT@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:41:41 +0200 Subject: [PATCH 10/15] Made public DirectoryInfo in lib.rs Signed-off-by: MiguVT <71216796+MiguVT@users.noreply.github.com> --- packages/app-lib/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app-lib/src/lib.rs b/packages/app-lib/src/lib.rs index 258e72423a..a7ac8a15bc 100644 --- a/packages/app-lib/src/lib.rs +++ b/packages/app-lib/src/lib.rs @@ -25,3 +25,4 @@ pub use event::{ }; pub use logger::start_logger; pub use state::State; +pub use state::dirs::DirectoryInfo; From 1baacf316db295e22271b5bef4638c5f0bc1ba57 Mon Sep 17 00:00:00 2001 From: MiguVT <71216796+MiguVT@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:42:41 +0200 Subject: [PATCH 11/15] Updated mod dirs to be public Signed-off-by: MiguVT <71216796+MiguVT@users.noreply.github.com> --- packages/app-lib/src/state/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index ab7a5e3e94..c1502f8aad 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -7,7 +7,7 @@ use crate::state::fs_watcher::FileWatcher; use sqlx::SqlitePool; // Submodules -mod dirs; +pub mod dirs; pub use self::dirs::*; mod profiles; From c8560af589782bc1341930c4d5963cd56147fb4c Mon Sep 17 00:00:00 2001 From: MiguVT Date: Sun, 17 Aug 2025 16:24:12 +0200 Subject: [PATCH 12/15] Improves portable mode handling Refactors portable mode detection and initialization. - Simplifies portable mode enablement to rely solely on the presence of `portable.txt`. - Removes environment variable method for enabling portable mode. - Ensures portable environment is set up as early as possible in the application lifecycle. - Sets `THESEUS_CONFIG_DIR` environment variable when in portable mode, ensuring consistent data directory usage across components. - Updates documentation to reflect the simplified portable mode activation. - Fixes window state location when using portable mode. --- apps/app/src/main.rs | 10 +++++- .../src/content/docs/guide/portable-mode.md | 34 +++---------------- packages/app-lib/src/lib.rs | 2 +- packages/app-lib/src/state/dirs.rs | 28 +++++++++------ 4 files changed, 33 insertions(+), 41 deletions(-) diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 09723d39f9..17140e4d47 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -7,6 +7,7 @@ use native_dialog::{DialogBuilder, MessageLevel}; use std::env; use tauri::{Listener, Manager}; use theseus::prelude::*; +use theseus::DirectoryInfo; mod api; mod error; @@ -144,6 +145,8 @@ fn restart_app(app: tauri::AppHandle) { // if Tauri app is called with arguments, then those arguments will be treated as commands // ie: deep links or filepaths for .mrpacks fn main() { + // Set up portable environment as early as possible (before any other initialization) + DirectoryInfo::setup_portable_env(); /* tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show ERROR > WARN > INFO > DEBUG > TRACE @@ -162,6 +165,11 @@ fn main() { tracing::info!("Initialized tracing subscriber. Loading Modrinth App!"); + // Get the settings directory (portable or not) for window state + let portable_dir = DirectoryInfo::get_initial_settings_dir() + .expect("Failed to determine Modrinth App settings directory"); + let window_state_path = portable_dir.join("app-window-state.json"); + let mut builder = tauri::Builder::default(); #[cfg(feature = "updater")] @@ -190,7 +198,7 @@ fn main() { .plugin(tauri_plugin_opener::init()) .plugin( tauri_plugin_window_state::Builder::default() - .with_filename("app-window-state.json") + .with_filename(window_state_path.to_string_lossy()) .build(), ) .setup(|app| { diff --git a/apps/docs/src/content/docs/guide/portable-mode.md b/apps/docs/src/content/docs/guide/portable-mode.md index 972e29f9e0..8f97d1ec05 100644 --- a/apps/docs/src/content/docs/guide/portable-mode.md +++ b/apps/docs/src/content/docs/guide/portable-mode.md @@ -26,37 +26,13 @@ All data is stored in a `ModrinthAppData` folder right next to the executable, m ## Enabling Portable Mode -There are two methods to enable portable mode: - -### Method 1: portable.txt File (Recommended) - -1. Create an empty file named `portable.txt` in the same directory as your Modrinth App executable -2. Launch the app normally -3. The app will automatically detect the file and enable portable mode +To enable portable mode, simply create an empty file named `portable.txt` in the same directory as your Modrinth App executable, then launch the app. The app will automatically detect the file and enable portable mode. All app data will be stored in a `ModrinthAppData` folder next to the executable. ``` YourFolder/ ├── Modrinth App.exe (or modrinth-app on Linux/macOS) ├── portable.txt ← Create this file -└── ModrinthAppData/ ← Created automatically on first run -``` - -### Method 2: Environment Variable - -Set the `MODRINTH_PORTABLE` environment variable to any value before launching the app: - -```bash -# Windows (PowerShell) -$env:MODRINTH_PORTABLE="true" -.\ModrinthApp.exe - -# Windows (Command Prompt) -set MODRINTH_PORTABLE=true -ModrinthApp.exe - -# Linux/macOS -export MODRINTH_PORTABLE=true -./modrinth-app +└── ModrinthAppData/ ← Created automatically on first run ``` ## Directory Structure @@ -147,11 +123,12 @@ Easily move your entire Minecraft setup to a new computer by copying the portabl 1. Install the app normally 2. Copy data from `ModrinthAppData/` to the system directory -3. Remove the `portable.txt` file or unset the environment variable +3. Remove the `portable.txt` file ## Technical Notes -- **Detection Priority**: Environment variable takes precedence over `portable.txt` file +- **Detection**: Portable mode is enabled only by the presence of `portable.txt` next to the executable +- **Config Directory**: The app sets the `THESEUS_CONFIG_DIR` environment variable automatically when portable mode is detected, ensuring all components use the correct data directory - **Performance**: Portable mode has identical performance to regular installations - **Platform Support**: Available on Windows, macOS, and Linux - **First Launch**: The `ModrinthAppData` directory is created automatically on first run @@ -163,7 +140,6 @@ Easily move your entire Minecraft setup to a new computer by copying the portabl - Verify `portable.txt` is in the same directory as the executable - Check file permissions on the portable folder -- Try using the environment variable method instead ### Data Not Saving diff --git a/packages/app-lib/src/lib.rs b/packages/app-lib/src/lib.rs index a7ac8a15bc..70aa817657 100644 --- a/packages/app-lib/src/lib.rs +++ b/packages/app-lib/src/lib.rs @@ -15,7 +15,7 @@ mod error; mod event; mod launcher; mod logger; -mod state; +pub mod state; pub use api::*; pub use error::*; diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index c9cb9b6acd..9f6b14958a 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -21,20 +21,23 @@ pub struct DirectoryInfo { } impl DirectoryInfo { + /// Call this as early as possible in main() to ensure all dependencies use the correct portable directory. + /// + /// Example usage (at the very top of main.rs): + /// theseus::DirectoryInfo::setup_portable_env(); + pub fn setup_portable_env() { + if let Some(portable_dir) = Self::portable_data_dir() { + Self::set_portable_env(&portable_dir); + } + } /// Returns the path to the directory containing the running executable, if possible. fn exe_dir() -> Option { std::env::current_exe().ok().and_then(|p| p.parent().map(|p| p.to_path_buf())) } - /// Checks if portable mode is enabled (env or portable.txt) and returns the portable data dir if so. + /// Checks if portable mode is enabled (portable.txt) and returns the portable data dir if so. fn portable_data_dir() -> Option { - // 1. Environment variable takes precedence - if env::var_os("MODRINTH_PORTABLE").is_some() { - if let Some(exe_dir) = Self::exe_dir() { - return Some(exe_dir.join("ModrinthAppData")); - } - } - // 2. portable.txt file next to executable + // portable.txt file next to executable if let Some(exe_dir) = Self::exe_dir() { let portable_txt = exe_dir.join("portable.txt"); if portable_txt.exists() { @@ -44,8 +47,12 @@ impl DirectoryInfo { None } - /// Sets the process environment variable for app data (e.g., APPDATA) to the portable dir if in portable mode. + /// Sets the process environment variable for config dir to the portable dir if in portable mode. fn set_portable_env(portable_dir: &Path) { + // Always set THESEUS_CONFIG_DIR for portable mode + unsafe { + env::set_var("THESEUS_CONFIG_DIR", portable_dir); + } #[cfg(target_os = "windows")] unsafe { env::set_var("APPDATA", portable_dir); @@ -64,11 +71,12 @@ impl DirectoryInfo { // Get the settings directory // init() is not needed for this function pub fn get_initial_settings_dir() -> Option { + // If portable mode, set env and use portable dir if let Some(portable_dir) = Self::portable_data_dir() { - // Set env var for the process so all code uses portable dir Self::set_portable_env(&portable_dir); return Some(portable_dir); } + // Otherwise, use THESEUS_CONFIG_DIR if set Self::env_path("THESEUS_CONFIG_DIR") .or_else(|| Some(dirs::data_dir()?.join("ModrinthApp"))) } From 19907915a96c49d217cb3bc57bec0dd0d753516d Mon Sep 17 00:00:00 2001 From: MiguVT Date: Sun, 17 Aug 2025 17:05:08 +0200 Subject: [PATCH 13/15] feat: add window state plugin conditionally based on portable mode --- apps/app/src/main.rs | 106 ++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 17140e4d47..6126a9e861 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -165,6 +165,7 @@ fn main() { tracing::info!("Initialized tracing subscriber. Loading Modrinth App!"); + // Get the settings directory (portable or not) for window state let portable_dir = DirectoryInfo::get_initial_settings_dir() .expect("Failed to determine Modrinth App settings directory"); @@ -177,6 +178,13 @@ fn main() { builder = builder.plugin(tauri_plugin_updater::Builder::new().build()); } + // Only add the window state plugin if not in portable mode + let is_portable = { + // This matches the logic in DirectoryInfo::portable_data_dir + let exe_dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|p| p.to_path_buf())); + exe_dir.as_ref().map(|d| d.join("portable.txt").exists()).unwrap_or(false) + }; + builder = builder .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { if let Some(payload) = args.get(1) { @@ -195,61 +203,65 @@ fn main() { .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_deep_link::init()) - .plugin(tauri_plugin_opener::init()) - .plugin( + .plugin(tauri_plugin_opener::init()); + + if !is_portable { + builder = builder.plugin( tauri_plugin_window_state::Builder::default() .with_filename(window_state_path.to_string_lossy()) .build(), - ) - .setup(|app| { - #[cfg(target_os = "macos")] - { - let payload = macos::deep_link::get_or_init_payload(app); - - let mtx_copy = payload.payload; - app.listen("deep-link://new-url", move |url| { - let mtx_copy_copy = mtx_copy.clone(); - let request = url.payload().to_owned(); - - let actual_request = - serde_json::from_str::>(&request) - .ok() - .map(|mut x| x.remove(0)) - .unwrap_or(request); - - tauri::async_runtime::spawn(async move { - tracing::info!("Handling deep link {actual_request}"); - - let mut payload = mtx_copy_copy.lock().await; - if payload.is_none() { - *payload = Some(actual_request.clone()); - } - - let _ = - api::utils::handle_command(actual_request).await; - }); - }); - }; + ); + } - #[cfg(not(target_os = "macos"))] - app.listen("deep-link://new-url", |url| { - let payload = url.payload().to_owned(); - tracing::info!("Handling deep link {payload}"); - tauri::async_runtime::spawn(api::utils::handle_command( - payload, - )); - }); + builder = builder.setup(|app| { + #[cfg(target_os = "macos")] + { + let payload = macos::deep_link::get_or_init_payload(app); - #[cfg(not(target_os = "linux"))] - if let Some(window) = app.get_window("main") - && let Err(e) = window.set_shadow(true) - { - tracing::warn!("Failed to set window shadow: {e}"); - } + let mtx_copy = payload.payload; + app.listen("deep-link://new-url", move |url| { + let mtx_copy_copy = mtx_copy.clone(); + let request = url.payload().to_owned(); + + let actual_request = + serde_json::from_str::>(&request) + .ok() + .map(|mut x| x.remove(0)) + .unwrap_or(request); + + tauri::async_runtime::spawn(async move { + tracing::info!("Handling deep link {actual_request}"); + + let mut payload = mtx_copy_copy.lock().await; + if payload.is_none() { + *payload = Some(actual_request.clone()); + } - Ok(()) + let _ = + api::utils::handle_command(actual_request).await; + }); + }); + }; + + #[cfg(not(target_os = "macos"))] + app.listen("deep-link://new-url", |url| { + let payload = url.payload().to_owned(); + tracing::info!("Handling deep link {payload}"); + tauri::async_runtime::spawn(api::utils::handle_command( + payload, + )); }); + #[cfg(not(target_os = "linux"))] + if let Some(window) = app.get_window("main") + && let Err(e) = window.set_shadow(true) + { + tracing::warn!("Failed to set window shadow: {e}"); + } + + Ok(()) + }); + builder = builder .plugin(api::auth::init()) .plugin(api::mr_auth::init()) From 4790620271bd70daa922e2b730c77a6ea5a614c0 Mon Sep 17 00:00:00 2001 From: MiguVT Date: Sun, 17 Aug 2025 17:56:46 +0200 Subject: [PATCH 14/15] feat: conditionally save window state based on portable mode --- apps/app-frontend/src/App.vue | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 11cc6600e6..36b4de28e4 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -255,7 +255,17 @@ initialize_state() }) const handleClose = async () => { - await saveWindowState(StateFlags.ALL) + let isPortable = false + try { + isPortable = await invoke('is_portable') + } catch (e) { + // fallback: assume not portable if command missing + console.warn('Error checking if portable:', e) + isPortable = false + } + if (!isPortable) { + await saveWindowState(StateFlags.ALL) + } await getCurrentWindow().close() } From e25f5f00bbfed64209240704f56d062bbfc17131 Mon Sep 17 00:00:00 2001 From: MiguVT Date: Sun, 17 Aug 2025 20:43:00 +0200 Subject: [PATCH 15/15] Improves portable mode detection Refactors portable mode detection to use a dedicated command and improves reliability. This change introduces a `is_portable_mode` command in Rust to check for portable mode, replacing the previous inline check. It also fixes an issue where the portable mode was not being correctly detected in the frontend, potentially leading to window state saving in portable mode. The Vue component now calls the new tauri command. --- apps/app-frontend/src/App.vue | 9 ++- apps/app/src/main.rs | 110 ++++++++++++++--------------- packages/app-lib/src/state/dirs.rs | 5 ++ 3 files changed, 61 insertions(+), 63 deletions(-) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 36b4de28e4..0a493eb293 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -257,11 +257,10 @@ initialize_state() const handleClose = async () => { let isPortable = false try { - isPortable = await invoke('is_portable') - } catch (e) { - // fallback: assume not portable if command missing - console.warn('Error checking if portable:', e) - isPortable = false + isPortable = !!(await invoke('is_portable_mode')) + console.log('Portable mode:', isPortable) + } catch (err) { + console.warn('Failed to check portable mode:', err) } if (!isPortable) { await saveWindowState(StateFlags.ALL) diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 6126a9e861..4ec08f4ff0 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -142,6 +142,11 @@ fn restart_app(app: tauri::AppHandle) { app.restart(); } +#[tauri::command] +fn is_portable_mode() -> bool { + theseus::DirectoryInfo::is_portable_mode() +} + // if Tauri app is called with arguments, then those arguments will be treated as commands // ie: deep links or filepaths for .mrpacks fn main() { @@ -165,12 +170,6 @@ fn main() { tracing::info!("Initialized tracing subscriber. Loading Modrinth App!"); - - // Get the settings directory (portable or not) for window state - let portable_dir = DirectoryInfo::get_initial_settings_dir() - .expect("Failed to determine Modrinth App settings directory"); - let window_state_path = portable_dir.join("app-window-state.json"); - let mut builder = tauri::Builder::default(); #[cfg(feature = "updater")] @@ -178,13 +177,6 @@ fn main() { builder = builder.plugin(tauri_plugin_updater::Builder::new().build()); } - // Only add the window state plugin if not in portable mode - let is_portable = { - // This matches the logic in DirectoryInfo::portable_data_dir - let exe_dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|p| p.to_path_buf())); - exe_dir.as_ref().map(|d| d.join("portable.txt").exists()).unwrap_or(false) - }; - builder = builder .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { if let Some(payload) = args.get(1) { @@ -205,62 +197,63 @@ fn main() { .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()); - if !is_portable { + // Only add window state plugin if not portable + if !theseus::DirectoryInfo::is_portable_mode() { builder = builder.plugin( tauri_plugin_window_state::Builder::default() - .with_filename(window_state_path.to_string_lossy()) + .with_filename("app-window-state.json") .build(), ); } builder = builder.setup(|app| { - #[cfg(target_os = "macos")] - { - let payload = macos::deep_link::get_or_init_payload(app); - - let mtx_copy = payload.payload; - app.listen("deep-link://new-url", move |url| { - let mtx_copy_copy = mtx_copy.clone(); - let request = url.payload().to_owned(); - - let actual_request = - serde_json::from_str::>(&request) - .ok() - .map(|mut x| x.remove(0)) - .unwrap_or(request); - - tauri::async_runtime::spawn(async move { - tracing::info!("Handling deep link {actual_request}"); - - let mut payload = mtx_copy_copy.lock().await; - if payload.is_none() { - *payload = Some(actual_request.clone()); - } - - let _ = - api::utils::handle_command(actual_request).await; + #[cfg(target_os = "macos")] + { + let payload = macos::deep_link::get_or_init_payload(app); + + let mtx_copy = payload.payload; + app.listen("deep-link://new-url", move |url| { + let mtx_copy_copy = mtx_copy.clone(); + let request = url.payload().to_owned(); + + let actual_request = + serde_json::from_str::>(&request) + .ok() + .map(|mut x| x.remove(0)) + .unwrap_or(request); + + tauri::async_runtime::spawn(async move { + tracing::info!("Handling deep link {actual_request}"); + + let mut payload = mtx_copy_copy.lock().await; + if payload.is_none() { + *payload = Some(actual_request.clone()); + } + + let _ = + api::utils::handle_command(actual_request).await; + }); }); + }; + + #[cfg(not(target_os = "macos"))] + app.listen("deep-link://new-url", |url| { + let payload = url.payload().to_owned(); + tracing::info!("Handling deep link {payload}"); + tauri::async_runtime::spawn(api::utils::handle_command( + payload, + )); }); - }; - - #[cfg(not(target_os = "macos"))] - app.listen("deep-link://new-url", |url| { - let payload = url.payload().to_owned(); - tracing::info!("Handling deep link {payload}"); - tauri::async_runtime::spawn(api::utils::handle_command( - payload, - )); - }); - #[cfg(not(target_os = "linux"))] - if let Some(window) = app.get_window("main") - && let Err(e) = window.set_shadow(true) - { - tracing::warn!("Failed to set window shadow: {e}"); - } + #[cfg(not(target_os = "linux"))] + if let Some(window) = app.get_window("main") + && let Err(e) = window.set_shadow(true) + { + tracing::warn!("Failed to set window shadow: {e}"); + } - Ok(()) - }); + Ok(()) + }); builder = builder .plugin(api::auth::init()) @@ -287,6 +280,7 @@ fn main() { toggle_decorations, show_window, restart_app, + is_portable_mode, ]); tracing::info!("Initializing app..."); diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index 9f6b14958a..d8e61b61f5 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -21,6 +21,11 @@ pub struct DirectoryInfo { } impl DirectoryInfo { + /// Returns true if portable mode is enabled (portable.txt next to executable) + pub fn is_portable_mode() -> bool { + Self::portable_data_dir().is_some() + } + /// Call this as early as possible in main() to ensure all dependencies use the correct portable directory. /// /// Example usage (at the very top of main.rs):