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
10 changed files with 591 additions and 111 deletions
@@ -234,6 +234,8 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver<tauri_plugin_sh
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(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));
// 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) => {
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));
// Look for error indicators
+32 -34
View File
@@ -1,49 +1,47 @@
use crate::utils::add_log;
use std::sync::Mutex;
// Store the opened file path globally
static OPENED_FILE: Mutex<Option<String>> = Mutex::new(None);
// Store the opened file paths globally (supports multiple files)
static OPENED_FILES: Mutex<Vec<String>> = Mutex::new(Vec::new());
// Set the opened file path (called by macOS file open events)
#[cfg(target_os = "macos")]
pub fn set_opened_file(file_path: String) {
let mut opened_file = OPENED_FILE.lock().unwrap();
*opened_file = Some(file_path.clone());
add_log(format!("📂 File opened via file open event: {}", file_path));
// Add an opened file path
pub fn add_opened_file(file_path: String) {
let mut opened_files = OPENED_FILES.lock().unwrap();
opened_files.push(file_path.clone());
add_log(format!("📂 File stored for later retrieval: {}", 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]
pub async fn get_opened_file() -> Result<Option<String>, String> {
// First check if we have a file from macOS file open events
{
let opened_file = OPENED_FILE.lock().unwrap();
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)
pub async fn get_opened_files() -> Result<Vec<String>, String> {
let mut all_files: Vec<String> = Vec::new();
// Get files from command line arguments (Windows/Linux 'Open With Stirling' behaviour)
let args: Vec<String> = std::env::args().collect();
// Look for a PDF file argument (skip the first arg which is the executable)
for arg in args.iter().skip(1) {
if arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
add_log(format!("📂 PDF file opened via command line: {}", arg));
return Ok(Some(arg.clone()));
}
let pdf_files: Vec<String> = args.iter()
.skip(1)
.filter(|arg| std::path::Path::new(arg).exists())
.cloned()
.collect();
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]
pub async fn clear_opened_file() -> Result<(), String> {
let mut opened_file = OPENED_FILE.lock().unwrap();
*opened_file = None;
add_log("📂 Cleared opened file".to_string());
pub async fn clear_opened_files() -> Result<(), String> {
let mut opened_files = OPENED_FILES.lock().unwrap();
opened_files.clear();
add_log("📂 Cleared opened files".to_string());
Ok(())
}
+1 -3
View File
@@ -4,6 +4,4 @@ pub mod files;
pub use backend::{start_backend, cleanup_backend};
pub use health::check_backend_health;
pub use files::{get_opened_file, clear_opened_file};
#[cfg(target_os = "macos")]
pub use files::set_opened_file;
pub use files::{get_opened_files, clear_opened_files, add_opened_file};
+30 -10
View File
@@ -1,13 +1,9 @@
use tauri::{RunEvent, WindowEvent};
#[cfg(target_os = "macos")]
use tauri::Emitter;
use tauri::{RunEvent, WindowEvent, Emitter, Manager};
mod utils;
mod commands;
use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend};
#[cfg(target_os = "macos")]
use commands::set_opened_file;
use commands::{start_backend, check_backend_health, get_opened_files, clear_opened_files, cleanup_backend, add_opened_file};
use utils::{add_log, get_tauri_logs};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -15,12 +11,35 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::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| {
add_log("🚀 Tauri app setup started".to_string());
add_log("🔍 DEBUG: Setup completed".to_string());
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!())
.expect("error while building tauri application")
.run(|app_handle, event| {
@@ -45,8 +64,9 @@ pub fn run() {
let file_path = url_str.strip_prefix("file://").unwrap_or(url_str);
if file_path.ends_with(".pdf") {
add_log(format!("📂 Processing opened PDF: {}", file_path));
set_opened_file(file_path.to_string());
let _ = app_handle.emit("macos://open-file", file_path.to_string());
add_opened_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() {
}
}
});
}
}