* ci: add temporary test workflow for portable ZIP validation * ci: fix mock setup in test workflow * ci: remove temporary test workflow * feat: add portable mode option to NSIS installer Custom NSIS template based on tauri-v2.9.1 upstream with 6 targeted modifications for portable mode support: - New PortableMode variable and /PORTABLE CLI flag for silent installs - Install type selection page with Normal/Portable radio buttons - Conditional default directory (Desktop for portable, LocalAppData for normal) - Skip registry writes, file associations, deep links, and uninstaller for portable - Skip Start Menu and desktop shortcuts for portable installs - Create portable marker file and Data/ directory on portable install The existing portable.rs runtime detection is unchanged — it looks for the same marker file this installer creates. Usage: GUI: select "Portable Installation" on the install type page Silent: Handy_setup.exe /S /PORTABLE /D=C:\path\to\Handy Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: implement true portable mode via source code path override Detect a `portable` marker file next to the executable at startup. When present, redirect all data paths (settings, models, recordings, database, logs) to a local `Data/` directory instead of %APPDATA%. This approach works because it intercepts path resolution at the Rust level, bypassing Tauri/Windows Shell API limitations that made the previous environment-variable and junction approaches unreliable. Changes: - Add src-tauri/src/portable.rs with OnceLock-based detection - Replace all app.path().app_data_dir() calls with portable helper - Configure tauri-plugin-log to use Folder target when portable - Configure tauri-plugin-store with absolute path when portable - Simplify build.yml portable ZIP (exe + resources + marker file) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add missing Manager trait import in portable.rs The .path() method on AppHandle requires `tauri::Manager` in scope. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: redirect WebView2 cache to portable Data dir Move main window creation from tauri.conf.json to setup closure so we can set data_directory when in portable mode. This prevents WebView2 from creating %LOCALAPPDATA%\com.pais.handy. Also set data_directory on the recording overlay window. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: auto-detect portable mode during updates When the Tauri updater runs the installer with /UPDATE, the install type page is skipped and /PORTABLE is not passed. Detect the existing portable marker file in .onInit so updates preserve portable mode automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: fix prettier formatting in portable.rs and lib.rs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: CJ Pais <cj@cjpais.com>
77 lines
2.5 KiB
Rust
77 lines
2.5 KiB
Rust
use std::path::PathBuf;
|
|
use std::sync::OnceLock;
|
|
use tauri::Manager;
|
|
|
|
/// Portable mode support for Handy.
|
|
///
|
|
/// When a file named `portable` exists next to the executable, all user data
|
|
/// (settings, models, recordings, database, logs) is stored in a `Data/`
|
|
/// directory alongside the executable instead of `%APPDATA%`.
|
|
|
|
static PORTABLE_DATA_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
|
|
|
|
/// Detect portable mode by looking for a `portable` marker file next to the exe.
|
|
/// Must be called once at startup before Tauri initializes.
|
|
pub fn init() {
|
|
PORTABLE_DATA_DIR.get_or_init(|| {
|
|
let exe_path = std::env::current_exe().ok()?;
|
|
let exe_dir = exe_path.parent()?;
|
|
|
|
if exe_dir.join("portable").exists() {
|
|
let data_dir = exe_dir.join("Data");
|
|
if !data_dir.exists() {
|
|
std::fs::create_dir_all(&data_dir).ok()?;
|
|
}
|
|
eprintln!("[portable] data dir: {}", data_dir.display());
|
|
Some(data_dir)
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Returns `true` if running in portable mode.
|
|
pub fn is_portable() -> bool {
|
|
PORTABLE_DATA_DIR.get().and_then(|v| v.as_ref()).is_some()
|
|
}
|
|
|
|
/// Get the portable data dir (if active). Does not require an AppHandle.
|
|
/// Returns `None` when not in portable mode.
|
|
pub fn data_dir() -> Option<&'static PathBuf> {
|
|
PORTABLE_DATA_DIR.get().and_then(|v| v.as_ref())
|
|
}
|
|
|
|
/// Portable-aware replacement for `app.path().app_data_dir()`.
|
|
pub fn app_data_dir(app: &tauri::AppHandle) -> Result<PathBuf, tauri::Error> {
|
|
if let Some(dir) = data_dir() {
|
|
Ok(dir.clone())
|
|
} else {
|
|
app.path().app_data_dir()
|
|
}
|
|
}
|
|
|
|
/// Portable-aware replacement for `app.path().app_log_dir()`.
|
|
pub fn app_log_dir(app: &tauri::AppHandle) -> Result<PathBuf, tauri::Error> {
|
|
if let Some(dir) = data_dir() {
|
|
Ok(dir.join("logs"))
|
|
} else {
|
|
app.path().app_log_dir()
|
|
}
|
|
}
|
|
|
|
/// Resolve a relative path against the app data directory (portable-aware).
|
|
/// Replaces `app.path().resolve(path, BaseDirectory::AppData)`.
|
|
pub fn resolve_app_data(app: &tauri::AppHandle, relative: &str) -> Result<PathBuf, tauri::Error> {
|
|
Ok(app_data_dir(app)?.join(relative))
|
|
}
|
|
|
|
/// Get the path to use with `tauri-plugin-store`.
|
|
/// Returns an absolute path in portable mode (so the store plugin writes to
|
|
/// the portable Data dir) or the original relative path otherwise.
|
|
pub fn store_path(relative: &str) -> PathBuf {
|
|
if let Some(dir) = data_dir() {
|
|
dir.join(relative)
|
|
} else {
|
|
PathBuf::from(relative)
|
|
}
|
|
}
|