Fix issues with opening files in desktop app (#4876)

# Description of Changes
Locking to just having one instance of the app running unifies the
experience across all OSs. Opening new files in Stirling will cause the
files to be opened in the existing window rather than spawning a new
instance of the app with just that file in the new instance.

There's much more to explore here to allow multiple windows open at
once, but that can be done all from one instance of the app, and will
likely make it easier to allow movement of files etc. across different
windows.

Also fixes extra newlines in the logs and directly builds to `.app` on
Mac because it's frustrating during development to have to repeatedly
mount & unmount the `.dmg`.
This commit is contained in:
James Brunton 2025-11-12 15:47:37 +00:00 committed by GitHub
parent a05c5a53c7
commit eb5f36aa15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 591 additions and 111 deletions

View File

@ -81,6 +81,137 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"pin-project-lite",
"slab",
]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-lock"
version = "3.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener",
"futures-lite",
"rustix",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]]
name = "async-signal"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]] [[package]]
name = "atk" name = "atk"
version = "0.18.2" version = "0.18.2"
@ -182,6 +313,19 @@ dependencies = [
"objc2 0.6.3", "objc2 0.6.3",
] ]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]] [[package]]
name = "borsh" name = "borsh"
version = "1.5.7" version = "1.5.7"
@ -424,6 +568,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -774,6 +927,33 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "endi"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]] [[package]]
name = "env_filter" name = "env_filter"
version = "0.1.4" version = "0.1.4"
@ -811,6 +991,27 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@ -966,6 +1167,19 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.31" version = "0.3.31"
@ -1352,6 +1566,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@ -2079,6 +2299,19 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]] [[package]]
name = "nodrop" name = "nodrop"
version = "0.1.14" version = "0.1.14"
@ -2463,6 +2696,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]] [[package]]
name = "os_pipe" name = "os_pipe"
version = "1.2.3" version = "1.2.3"
@ -2498,6 +2741,12 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@ -2679,6 +2928,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.32" version = "0.3.32"
@ -2711,6 +2971,20 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "polling"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.3" version = "0.1.3"
@ -3657,6 +3931,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "stirling-pdf" name = "stirling-pdf"
version = "0.1.0" version = "0.1.0"
@ -3670,6 +3950,7 @@ dependencies = [
"tauri-plugin-fs", "tauri-plugin-fs",
"tauri-plugin-log", "tauri-plugin-log",
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-single-instance",
"tokio", "tokio",
] ]
@ -4056,6 +4337,21 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710"
dependencies = [
"serde",
"serde_json",
"tauri",
"thiserror 2.0.17",
"tracing",
"windows-sys 0.60.2",
"zbus",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.9.1" version = "2.9.1"
@ -4463,9 +4759,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [ dependencies = [
"pin-project-lite", "pin-project-lite",
"tracing-attributes",
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-attributes"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.34" version = "0.1.34"
@ -4515,6 +4823,17 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "uds_windows"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
dependencies = [
"memoffset",
"tempfile",
"winapi",
]
[[package]] [[package]]
name = "unic-char-property" name = "unic-char-property"
version = "0.9.0" version = "0.9.0"
@ -5530,6 +5849,67 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zbus"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91"
dependencies = [
"async-broadcast",
"async-executor",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"nix",
"ordered-stream",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow 0.7.13",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314"
dependencies = [
"proc-macro-crate 3.4.0",
"proc-macro2",
"quote",
"syn 2.0.108",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
dependencies = [
"serde",
"static_assertions",
"winnow 0.7.13",
"zvariant",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.27" version = "0.8.27"
@ -5603,3 +5983,43 @@ dependencies = [
"quote", "quote",
"syn 2.0.108", "syn 2.0.108",
] ]
[[package]]
name = "zvariant"
version = "5.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow 0.7.13",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006"
dependencies = [
"proc-macro-crate 3.4.0",
"proc-macro2",
"quote",
"syn 2.0.108",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.108",
"winnow 0.7.13",
]

View File

@ -28,5 +28,6 @@ tauri = { version = "2.9.0", features = [ "devtools"] }
tauri-plugin-log = "2.0.0-rc" tauri-plugin-log = "2.0.0-rc"
tauri-plugin-shell = "2.1.0" tauri-plugin-shell = "2.1.0"
tauri-plugin-fs = "2.4.4" tauri-plugin-fs = "2.4.4"
tauri-plugin-single-instance = "2.0.1"
tokio = { version = "1.0", features = ["time"] } tokio = { version = "1.0", features = ["time"] }
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }

View File

@ -234,6 +234,8 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver<tauri_plugin_sh
match event { match event {
tauri_plugin_shell::process::CommandEvent::Stdout(output) => { tauri_plugin_shell::process::CommandEvent::Stdout(output) => {
let output_str = String::from_utf8_lossy(&output); let output_str = String::from_utf8_lossy(&output);
// Strip exactly one trailing newline to avoid double newlines
let output_str = output_str.strip_suffix('\n').unwrap_or(&output_str);
add_log(format!("📤 Backend: {}", output_str)); add_log(format!("📤 Backend: {}", output_str));
// Look for startup indicators // Look for startup indicators
@ -250,6 +252,8 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver<tauri_plugin_sh
} }
tauri_plugin_shell::process::CommandEvent::Stderr(output) => { tauri_plugin_shell::process::CommandEvent::Stderr(output) => {
let output_str = String::from_utf8_lossy(&output); let output_str = String::from_utf8_lossy(&output);
// Strip exactly one trailing newline to avoid double newlines
let output_str = output_str.strip_suffix('\n').unwrap_or(&output_str);
add_log(format!("📥 Backend Error: {}", output_str)); add_log(format!("📥 Backend Error: {}", output_str));
// Look for error indicators // Look for error indicators

View File

@ -1,49 +1,47 @@
use crate::utils::add_log; use crate::utils::add_log;
use std::sync::Mutex; use std::sync::Mutex;
// Store the opened file path globally // Store the opened file paths globally (supports multiple files)
static OPENED_FILE: Mutex<Option<String>> = Mutex::new(None); static OPENED_FILES: Mutex<Vec<String>> = Mutex::new(Vec::new());
// Set the opened file path (called by macOS file open events) // Add an opened file path
#[cfg(target_os = "macos")] pub fn add_opened_file(file_path: String) {
pub fn set_opened_file(file_path: String) { let mut opened_files = OPENED_FILES.lock().unwrap();
let mut opened_file = OPENED_FILE.lock().unwrap(); opened_files.push(file_path.clone());
*opened_file = Some(file_path.clone()); add_log(format!("📂 File stored for later retrieval: {}", file_path));
add_log(format!("📂 File opened via file open event: {}", file_path));
} }
// Command to get opened file path (if app was launched with a file) // Command to get opened file paths (if app was launched with files)
#[tauri::command] #[tauri::command]
pub async fn get_opened_file() -> Result<Option<String>, String> { pub async fn get_opened_files() -> Result<Vec<String>, String> {
// First check if we have a file from macOS file open events let mut all_files: Vec<String> = Vec::new();
{
let opened_file = OPENED_FILE.lock().unwrap(); // Get files from command line arguments (Windows/Linux 'Open With Stirling' behaviour)
if let Some(ref file_path) = *opened_file {
add_log(format!("📂 Returning stored opened file: {}", file_path));
return Ok(Some(file_path.clone()));
}
}
// Fallback to command line arguments (Windows/Linux)
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
let pdf_files: Vec<String> = args.iter()
// Look for a PDF file argument (skip the first arg which is the executable) .skip(1)
for arg in args.iter().skip(1) { .filter(|arg| std::path::Path::new(arg).exists())
if arg.ends_with(".pdf") && std::path::Path::new(arg).exists() { .cloned()
add_log(format!("📂 PDF file opened via command line: {}", arg)); .collect();
return Ok(Some(arg.clone()));
} all_files.extend(pdf_files);
// Add any files sent via events or other instances (macOS 'Open With Stirling' behaviour, also Windows/Linux extra files)
{
let opened_files = OPENED_FILES.lock().unwrap();
all_files.extend(opened_files.clone());
} }
Ok(None) add_log(format!("📂 Returning {} opened file(s)", all_files.len()));
Ok(all_files)
} }
// Command to clear the opened file (after processing) // Command to clear the opened files (after processing)
#[tauri::command] #[tauri::command]
pub async fn clear_opened_file() -> Result<(), String> { pub async fn clear_opened_files() -> Result<(), String> {
let mut opened_file = OPENED_FILE.lock().unwrap(); let mut opened_files = OPENED_FILES.lock().unwrap();
*opened_file = None; opened_files.clear();
add_log("📂 Cleared opened file".to_string()); add_log("📂 Cleared opened files".to_string());
Ok(()) Ok(())
} }

View File

@ -4,6 +4,4 @@ pub mod files;
pub use backend::{start_backend, cleanup_backend}; pub use backend::{start_backend, cleanup_backend};
pub use health::check_backend_health; pub use health::check_backend_health;
pub use files::{get_opened_file, clear_opened_file}; pub use files::{get_opened_files, clear_opened_files, add_opened_file};
#[cfg(target_os = "macos")]
pub use files::set_opened_file;

View File

@ -1,13 +1,9 @@
use tauri::{RunEvent, WindowEvent}; use tauri::{RunEvent, WindowEvent, Emitter, Manager};
#[cfg(target_os = "macos")]
use tauri::Emitter;
mod utils; mod utils;
mod commands; mod commands;
use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend}; use commands::{start_backend, check_backend_health, get_opened_files, clear_opened_files, cleanup_backend, add_opened_file};
#[cfg(target_os = "macos")]
use commands::set_opened_file;
use utils::{add_log, get_tauri_logs}; use utils::{add_log, get_tauri_logs};
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
@ -15,12 +11,35 @@ pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
// This callback runs when a second instance tries to start
add_log(format!("📂 Second instance detected with args: {:?}", args));
// Scan args for PDF files (skip first arg which is the executable)
for arg in args.iter().skip(1) {
if std::path::Path::new(arg).exists() {
add_log(format!("📂 Forwarding file to existing instance: {}", arg));
// Store file for later retrieval (in case frontend isn't ready yet)
add_opened_file(arg.clone());
// Also emit event for immediate handling if frontend is ready
let _ = app.emit("file-opened", arg.clone());
// Bring the existing window to front
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_focus();
let _ = window.unminimize();
}
}
}
}))
.setup(|_app| { .setup(|_app| {
add_log("🚀 Tauri app setup started".to_string()); add_log("🚀 Tauri app setup started".to_string());
add_log("🔍 DEBUG: Setup completed".to_string()); add_log("🔍 DEBUG: Setup completed".to_string());
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_file, clear_opened_file, get_tauri_logs]) .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_files, clear_opened_files, get_tauri_logs])
.build(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("error while building tauri application") .expect("error while building tauri application")
.run(|app_handle, event| { .run(|app_handle, event| {
@ -45,8 +64,9 @@ pub fn run() {
let file_path = url_str.strip_prefix("file://").unwrap_or(url_str); let file_path = url_str.strip_prefix("file://").unwrap_or(url_str);
if file_path.ends_with(".pdf") { if file_path.ends_with(".pdf") {
add_log(format!("📂 Processing opened PDF: {}", file_path)); add_log(format!("📂 Processing opened PDF: {}", file_path));
set_opened_file(file_path.to_string()); add_opened_file(file_path.to_string());
let _ = app_handle.emit("macos://open-file", file_path.to_string()); // Use unified event name for consistency across platforms
let _ = app_handle.emit("file-opened", file_path.to_string());
} }
} }
} }
@ -58,4 +78,4 @@ pub fn run() {
} }
} }
}); });
} }

View File

@ -22,7 +22,7 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": ["deb", "rpm", "dmg", "msi"], "targets": ["deb", "rpm", "dmg", "app", "msi"],
"icon": [ "icon": [
"icons/icon.png", "icons/icon.png",
"icons/icon.icns", "icons/icon.icns",

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react'; import { useEffect, useRef } from 'react';
import { useBackendInitializer } from '@app/hooks/useBackendInitializer'; import { useBackendInitializer } from '@app/hooks/useBackendInitializer';
import { useOpenedFile } from '@app/hooks/useOpenedFile'; import { useOpenedFile } from '@app/hooks/useOpenedFile';
import { fileOpenService } from '@app/services/fileOpenService'; import { fileOpenService } from '@app/services/fileOpenService';
@ -17,31 +17,78 @@ export function useAppInitialization(): void {
// Get file management actions // Get file management actions
const { addFiles } = useFileManagement(); const { addFiles } = useFileManagement();
// Handle file opened with app (Tauri mode) // Handle files opened with app (Tauri mode)
const { openedFilePath, loading: openedFileLoading } = useOpenedFile(); const { openedFilePaths, loading: openedFileLoading } = useOpenedFile();
// Load opened file and add directly to FileContext // Track if we've already loaded the initial files to prevent duplicate loads
const initialFilesLoadedRef = useRef(false);
// Load opened files and add directly to FileContext
useEffect(() => { useEffect(() => {
if (openedFilePath && !openedFileLoading) { if (openedFilePaths.length > 0 && !openedFileLoading && !initialFilesLoadedRef.current) {
const loadOpenedFile = async () => { initialFilesLoadedRef.current = true;
try {
const fileData = await fileOpenService.readFileAsArrayBuffer(openedFilePath);
if (fileData) {
// Create a File object from the ArrayBuffer
const file = new File([fileData.arrayBuffer], fileData.fileName, {
type: 'application/pdf'
});
// Add directly to FileContext const loadOpenedFiles = async () => {
await addFiles([file]); try {
console.log('[Desktop] Opened file added to FileContext:', fileData.fileName); const filesArray: File[] = [];
// Load all files in parallel
await Promise.all(
openedFilePaths.map(async (filePath) => {
try {
const fileData = await fileOpenService.readFileAsArrayBuffer(filePath);
if (fileData) {
const file = new File([fileData.arrayBuffer], fileData.fileName, {
type: 'application/pdf'
});
filesArray.push(file);
console.log('[Desktop] Loaded file:', fileData.fileName);
}
} catch (error) {
console.error('[Desktop] Failed to load file:', filePath, error);
}
})
);
if (filesArray.length > 0) {
// Add all files to FileContext at once
await addFiles(filesArray);
console.log(`[Desktop] ${filesArray.length} opened file(s) added to FileContext`);
} }
} catch (error) { } catch (error) {
console.error('[Desktop] Failed to load opened file:', error); console.error('[Desktop] Failed to load opened files:', error);
} }
}; };
loadOpenedFile(); loadOpenedFiles();
} }
}, [openedFilePath, openedFileLoading, addFiles]); }, [openedFilePaths, openedFileLoading, addFiles]);
// Listen for runtime file-opened events (from second instances on Windows/Linux)
useEffect(() => {
const handleRuntimeFileOpen = async (filePath: string) => {
try {
console.log('[Desktop] Runtime file-opened event received:', filePath);
const fileData = await fileOpenService.readFileAsArrayBuffer(filePath);
if (fileData) {
// Create a File object from the ArrayBuffer
const file = new File([fileData.arrayBuffer], fileData.fileName, {
type: 'application/pdf'
});
// Add directly to FileContext
await addFiles([file]);
console.log('[Desktop] Runtime opened file added to FileContext:', fileData.fileName);
}
} catch (error) {
console.error('[Desktop] Failed to load runtime opened file:', error);
}
};
// Set up event listener and get cleanup function
const unlisten = fileOpenService.onFileOpened(handleRuntimeFileOpen);
// Clean up listener on unmount
return unlisten;
}, [addFiles]);
} }

View File

@ -2,28 +2,28 @@ import { useState, useEffect } from 'react';
import { fileOpenService } from '@app/services/fileOpenService'; import { fileOpenService } from '@app/services/fileOpenService';
export function useOpenedFile() { export function useOpenedFile() {
const [openedFilePath, setOpenedFilePath] = useState<string | null>(null); const [openedFilePaths, setOpenedFilePaths] = useState<string[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const checkForOpenedFile = async () => { const checkForOpenedFile = async () => {
console.log('🔍 Checking for opened file...'); console.log('🔍 Checking for opened file(s)...');
try { try {
const filePath = await fileOpenService.getOpenedFile(); const filePaths = await fileOpenService.getOpenedFiles();
console.log('🔍 fileOpenService.getOpenedFile() returned:', filePath); console.log('🔍 fileOpenService.getOpenedFiles() returned:', filePaths);
if (filePath) { if (filePaths.length > 0) {
console.log('✅ App opened with file:', filePath); console.log(`✅ App opened with ${filePaths.length} file(s):`, filePaths);
setOpenedFilePath(filePath); setOpenedFilePaths(filePaths);
// Clear the file from service state after consuming it // Clear the files from service state after consuming them
await fileOpenService.clearOpenedFile(); await fileOpenService.clearOpenedFiles();
} else { } else {
console.log(' No file was opened with the app'); console.log(' No files were opened with the app');
} }
} catch (error) { } catch (error) {
console.error('❌ Failed to check for opened file:', error); console.error('❌ Failed to check for opened files:', error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -34,7 +34,7 @@ export function useOpenedFile() {
// Listen for runtime file open events (abstracted through service) // Listen for runtime file open events (abstracted through service)
const unlistenRuntimeEvents = fileOpenService.onFileOpened((filePath: string) => { const unlistenRuntimeEvents = fileOpenService.onFileOpened((filePath: string) => {
console.log('📂 Runtime file open event:', filePath); console.log('📂 Runtime file open event:', filePath);
setOpenedFilePath(filePath); setOpenedFilePaths(prev => [...prev, filePath]);
}); });
// Cleanup function // Cleanup function
@ -43,5 +43,5 @@ export function useOpenedFile() {
}; };
}, []); }, []);
return { openedFilePath, loading }; return { openedFilePaths, loading };
} }

View File

@ -1,22 +1,22 @@
import { invoke, isTauri } from '@tauri-apps/api/core'; import { invoke, isTauri } from '@tauri-apps/api/core';
export interface FileOpenService { export interface FileOpenService {
getOpenedFile(): Promise<string | null>; getOpenedFiles(): Promise<string[]>;
readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>; readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>;
clearOpenedFile(): Promise<void>; clearOpenedFiles(): Promise<void>;
onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function
} }
class TauriFileOpenService implements FileOpenService { class TauriFileOpenService implements FileOpenService {
async getOpenedFile(): Promise<string | null> { async getOpenedFiles(): Promise<string[]> {
try { try {
console.log('🔍 Calling invoke(get_opened_file)...'); console.log('🔍 Calling invoke(get_opened_files)...');
const result = await invoke<string | null>('get_opened_file'); const result = await invoke<string[]>('get_opened_files');
console.log('🔍 invoke(get_opened_file) returned:', result); console.log('🔍 invoke(get_opened_files) returned:', result);
return result; return result;
} catch (error) { } catch (error) {
console.error('❌ Failed to get opened file:', error); console.error('❌ Failed to get opened files:', error);
return null; return [];
} }
} }
@ -37,13 +37,13 @@ class TauriFileOpenService implements FileOpenService {
} }
} }
async clearOpenedFile(): Promise<void> { async clearOpenedFiles(): Promise<void> {
try { try {
console.log('🔍 Calling invoke(clear_opened_file)...'); console.log('🔍 Calling invoke(clear_opened_files)...');
await invoke('clear_opened_file'); await invoke('clear_opened_files');
console.log('✅ Successfully cleared opened file'); console.log('✅ Successfully cleared opened files');
} catch (error) { } catch (error) {
console.error('❌ Failed to clear opened file:', error); console.error('❌ Failed to clear opened files:', error);
} }
} }
@ -67,15 +67,9 @@ class TauriFileOpenService implements FileOpenService {
return; return;
} }
// Listen for macOS native file open events // Listen for unified file open events (all platforms)
const unlistenMacOS = await listen('macos://open-file', (event) => { const unlisten = await listen('file-opened', (event) => {
console.log('📂 macOS native file open event:', event.payload); console.log('📂 File open event received:', event.payload);
callback(event.payload as string);
});
// Listen for fallback file open events
const unlistenFallback = await listen('file-opened', (event) => {
console.log('📂 Fallback file open event:', event.payload);
callback(event.payload as string); callback(event.payload as string);
}); });
@ -83,8 +77,7 @@ class TauriFileOpenService implements FileOpenService {
if (!isCleanedUp) { if (!isCleanedUp) {
cleanup = () => { cleanup = () => {
try { try {
unlistenMacOS(); unlisten();
unlistenFallback();
console.log('✅ File event listeners cleaned up'); console.log('✅ File event listeners cleaned up');
} catch (error) { } catch (error) {
console.error('❌ Error during file event cleanup:', error); console.error('❌ Error during file event cleanup:', error);
@ -93,8 +86,7 @@ class TauriFileOpenService implements FileOpenService {
} else { } else {
// Clean up immediately if cleanup was called during setup // Clean up immediately if cleanup was called during setup
try { try {
unlistenMacOS(); unlisten();
unlistenFallback();
} catch (error) { } catch (error) {
console.error('❌ Error during immediate cleanup:', error); console.error('❌ Error during immediate cleanup:', error);
} }
@ -118,9 +110,9 @@ class TauriFileOpenService implements FileOpenService {
} }
class WebFileOpenService implements FileOpenService { class WebFileOpenService implements FileOpenService {
async getOpenedFile(): Promise<string | null> { async getOpenedFiles(): Promise<string[]> {
// In web mode, there's no file association support // In web mode, there's no file association support
return null; return [];
} }
async readFileAsArrayBuffer(_filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null> { async readFileAsArrayBuffer(_filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null> {
@ -128,7 +120,7 @@ class WebFileOpenService implements FileOpenService {
return null; return null;
} }
async clearOpenedFile(): Promise<void> { async clearOpenedFiles(): Promise<void> {
// In web mode, no file clearing needed // In web mode, no file clearing needed
} }