Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39a8443676 |
@@ -80,12 +80,6 @@ tasks:
|
||||
OPEN: '{{.OPEN | default ""}}'
|
||||
env:
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
# Dev-only browser-tab label so concurrent worktrees are distinguishable.
|
||||
# Only the worktree folder basename (e.g. "wt1") is exposed — never the
|
||||
# full path, hostname, or user. Consumed at dev-serve time by vite.config
|
||||
# and dropped from production builds.
|
||||
STIRLING_DEV_LABEL:
|
||||
sh: basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
cmds:
|
||||
- npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent};
|
||||
|
||||
mod utils;
|
||||
pub mod commands;
|
||||
mod menu;
|
||||
mod state;
|
||||
|
||||
use commands::{
|
||||
@@ -74,6 +75,12 @@ fn parse_launch_files(args: &[String]) -> Vec<String> {
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.menu(menu::build_app_menu)
|
||||
.on_menu_event(|app, event| {
|
||||
if event.id() == menu::GRACEFUL_QUIT_MENU_ID {
|
||||
menu::request_graceful_quit(app);
|
||||
}
|
||||
})
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::new()
|
||||
.level(log::LevelFilter::Info)
|
||||
@@ -209,6 +216,10 @@ pub fn run() {
|
||||
// Use Tauri's built-in cleanup
|
||||
app_handle.cleanup_before_exit();
|
||||
}
|
||||
RunEvent::Exit => {
|
||||
add_log("🔄 App exiting, ensuring backend cleanup...".to_string());
|
||||
cleanup_backend();
|
||||
}
|
||||
RunEvent::WindowEvent { event: WindowEvent::CloseRequested {.. }, .. } => {
|
||||
add_log("🔄 Window close requested (will cleanup on actual exit)...".to_string());
|
||||
// Don't cleanup here - let JavaScript handler prevent close if needed
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
use tauri::menu::Menu;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::menu::{
|
||||
AboutMetadata, MenuItem, PredefinedMenuItem, Submenu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID,
|
||||
};
|
||||
|
||||
use crate::utils::add_log;
|
||||
|
||||
// Id of our custom Quit item. macOS routes Cmd+Q / the app-menu Quit through
|
||||
// [NSApp terminate:], which fires RunEvent::Exit (not ExitRequested) and never
|
||||
// asks windows to close - so the frontend's unsaved-changes guard is bypassed.
|
||||
// We intercept the shortcut with this item and route it through each window's
|
||||
// close flow instead.
|
||||
pub const GRACEFUL_QUIT_MENU_ID: &str = "graceful-quit";
|
||||
|
||||
// macOS: mirror Tauri's default menu (Menu::default) but replace the standard
|
||||
// Quit item with a custom one so Cmd+Q goes through request_graceful_quit().
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn build_app_menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
|
||||
let pkg = app.package_info();
|
||||
let config = app.config();
|
||||
let about = AboutMetadata {
|
||||
name: Some(pkg.name.clone()),
|
||||
version: Some(pkg.version.to_string()),
|
||||
copyright: config.bundle.copyright.clone(),
|
||||
authors: config.bundle.publisher.clone().map(|p| vec![p]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let quit = MenuItem::with_id(
|
||||
app,
|
||||
GRACEFUL_QUIT_MENU_ID,
|
||||
format!("Quit {}", pkg.name),
|
||||
true,
|
||||
Some("CmdOrCtrl+Q"),
|
||||
)?;
|
||||
|
||||
let app_menu = Submenu::with_items(
|
||||
app,
|
||||
pkg.name.clone(),
|
||||
true,
|
||||
&[
|
||||
&PredefinedMenuItem::about(app, None, Some(about))?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&PredefinedMenuItem::services(app, None)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&PredefinedMenuItem::hide(app, None)?,
|
||||
&PredefinedMenuItem::hide_others(app, None)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&quit,
|
||||
],
|
||||
)?;
|
||||
|
||||
let file_menu = Submenu::with_items(
|
||||
app,
|
||||
"File",
|
||||
true,
|
||||
&[&PredefinedMenuItem::close_window(app, None)?],
|
||||
)?;
|
||||
|
||||
let edit_menu = Submenu::with_items(
|
||||
app,
|
||||
"Edit",
|
||||
true,
|
||||
&[
|
||||
&PredefinedMenuItem::undo(app, None)?,
|
||||
&PredefinedMenuItem::redo(app, None)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&PredefinedMenuItem::cut(app, None)?,
|
||||
&PredefinedMenuItem::copy(app, None)?,
|
||||
&PredefinedMenuItem::paste(app, None)?,
|
||||
&PredefinedMenuItem::select_all(app, None)?,
|
||||
],
|
||||
)?;
|
||||
|
||||
let view_menu = Submenu::with_items(
|
||||
app,
|
||||
"View",
|
||||
true,
|
||||
&[&PredefinedMenuItem::fullscreen(app, None)?],
|
||||
)?;
|
||||
|
||||
let window_menu = Submenu::with_id_and_items(
|
||||
app,
|
||||
WINDOW_SUBMENU_ID,
|
||||
"Window",
|
||||
true,
|
||||
&[
|
||||
&PredefinedMenuItem::minimize(app, None)?,
|
||||
&PredefinedMenuItem::maximize(app, None)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&PredefinedMenuItem::close_window(app, None)?,
|
||||
],
|
||||
)?;
|
||||
|
||||
let help_menu = Submenu::with_id_and_items(app, HELP_SUBMENU_ID, "Help", true, &[])?;
|
||||
|
||||
Menu::with_items(
|
||||
app,
|
||||
&[
|
||||
&app_menu,
|
||||
&file_menu,
|
||||
&edit_menu,
|
||||
&view_menu,
|
||||
&window_menu,
|
||||
&help_menu,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// Other platforms keep Tauri's default menu unchanged; the RunEvent::Exit
|
||||
// cleanup path in lib.rs already guards against orphaned backends there.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn build_app_menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
|
||||
Menu::default(app)
|
||||
}
|
||||
|
||||
// Route a quit request through each window's close flow so the frontend's
|
||||
// unsaved-changes guard (useExitWarning) runs. Closing the last window causes
|
||||
// the runtime to emit RunEvent::ExitRequested, which kills the bundled backend.
|
||||
// If somehow no windows exist, exit directly.
|
||||
pub fn request_graceful_quit<R: Runtime>(app: &AppHandle<R>) {
|
||||
let windows = app.webview_windows();
|
||||
if windows.is_empty() {
|
||||
add_log("Quit requested with no open windows; exiting.".to_string());
|
||||
app.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
add_log(format!(
|
||||
"Quit requested; closing {} window(s) via close flow",
|
||||
windows.len()
|
||||
));
|
||||
for (_, window) in windows {
|
||||
let _ = window.close();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Prefixes the browser tab title with the current worktree name during local
|
||||
* development so multiple concurrently-running worktrees (e.g. wt1, wt2, spdf1)
|
||||
* are distinguishable at a glance instead of all showing "Stirling PDF".
|
||||
*
|
||||
* The label is injected as a build-time constant by vite.config, sourced from
|
||||
* the top-level dev tasks. It is an empty string in production builds, so this
|
||||
* whole feature compiles away to a no-op outside `vite` dev-serve.
|
||||
*/
|
||||
|
||||
const LABEL =
|
||||
typeof __DEV_WORKTREE_LABEL__ === "string" ? __DEV_WORKTREE_LABEL__ : "";
|
||||
|
||||
export function applyDevWorktreeLabel(): void {
|
||||
if (!LABEL || typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = `[${LABEL}] `;
|
||||
|
||||
const ensurePrefixed = () => {
|
||||
if (!document.title.startsWith(prefix)) {
|
||||
// The app rewrites document.title on route/tool changes; re-apply the
|
||||
// prefix on top of whatever the app just set.
|
||||
document.title = prefix + document.title;
|
||||
}
|
||||
};
|
||||
|
||||
ensurePrefixed();
|
||||
|
||||
const titleEl = document.querySelector("title");
|
||||
if (titleEl) {
|
||||
new MutationObserver(ensurePrefixed).observe(titleEl, {
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,9 @@ import { BrowserRouter } from "react-router-dom";
|
||||
import App from "@app/App";
|
||||
import "@app/i18n"; // Initialize i18next
|
||||
import { BASE_PATH } from "@app/constants/app";
|
||||
import { applyDevWorktreeLabel } from "@app/utils/applyDevWorktreeLabel";
|
||||
|
||||
import { startEagerWasmCompilation } from "@app/services/wasmPrecompiler";
|
||||
|
||||
applyDevWorktreeLabel();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const scheduleCompilation = () => {
|
||||
if (typeof requestIdleCallback === "function") {
|
||||
|
||||
Vendored
-7
@@ -29,10 +29,3 @@ interface ImportMetaEnv {
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dev-only worktree folder basename injected by vite.config at dev-serve time
|
||||
* (empty string in production builds). Used to prefix the browser tab title so
|
||||
* concurrent worktrees are distinguishable.
|
||||
*/
|
||||
declare const __DEV_WORKTREE_LABEL__: string;
|
||||
|
||||
@@ -182,13 +182,7 @@ const TSCONFIG_MAP: Record<BuildMode, string> = {
|
||||
prototypes: "./tsconfig.prototypes.vite.json",
|
||||
};
|
||||
|
||||
export default defineConfig(async ({ mode, command }) => {
|
||||
// Dev-only browser-tab label (worktree folder basename) surfaced by the
|
||||
// top-level dev tasks so concurrent worktrees have distinguishable tabs.
|
||||
// Only injected during `vite` (dev serve) — never baked into a production
|
||||
// build — and carries only the folder name, no path/host/user info.
|
||||
const devWorktreeLabel =
|
||||
command === "serve" ? (process.env.STIRLING_DEV_LABEL ?? "") : "";
|
||||
export default defineConfig(async ({ mode }) => {
|
||||
// Load env files relative to this config (frontend/editor/), regardless of
|
||||
// where the build was invoked from. The previous `process.cwd()` worked when
|
||||
// this file lived at frontend/, but after the editor was moved under
|
||||
@@ -256,9 +250,6 @@ export default defineConfig(async ({ mode, command }) => {
|
||||
};
|
||||
|
||||
return {
|
||||
define: {
|
||||
__DEV_WORKTREE_LABEL__: JSON.stringify(devWorktreeLabel),
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
...(runSubpath ? [subpathBareRedirectPlugin(runSubpath)] : []),
|
||||
|
||||
Reference in New Issue
Block a user