mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
fix(macOS): gracefully quit app (#7733)
* fix(macOS): gracefully quit app When macOS App is closed via menu item "Quit", make sure that IPC flow completes first. - close main window instead of directly invoking `quitApp()` * fix(macOS): use quit-intent flag - do not touch "minimizeToTray" setting - instead set "quitRequested" flag to indicate quit intent - set timeout to cancel quit request, if app fails to respond within 5 seconds * refactor(macOS): split quit-intent state and add timeout rationale - Separate the dual-purpose timer handle into an explicit boolean + named timer to make the semantics obvious. - Extract the 5s window as a named constant with a comment on why this particular trade-off (long enough for sync/finish-day, short enough to not block a subsequent tray-hide click after cancel). - Fix the misleading "set flag" comment in the no-window branch (no flag is set there; none is needed). - Note that the `closed` reset clears the pending timer so it doesn't keep the event loop alive. Pure cleanup, no behavior change. --------- Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
This commit is contained in:
parent
9a7a86c8ec
commit
18472e86b9
2 changed files with 45 additions and 14 deletions
|
|
@ -62,6 +62,42 @@ export const getWin = (): BrowserWindow => {
|
|||
return mainWinModule.win;
|
||||
};
|
||||
|
||||
// How long the "quit requested" intent survives before auto-clearing.
|
||||
// Long enough to cover normal before-close IPC (sync, finish-day prompt);
|
||||
// short enough that if the user cancels finish-day and then clicks the
|
||||
// window close button, they get their normal minimize-to-tray behavior
|
||||
// rather than being re-prompted indefinitely.
|
||||
const QUIT_REQUEST_TIMEOUT_MS = 5_000;
|
||||
|
||||
let isQuitRequested = false;
|
||||
let quitRequestResetTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
const getIsQuitRequested = (): boolean => isQuitRequested;
|
||||
|
||||
const setIsQuitRequested = (flag: boolean): void => {
|
||||
if (quitRequestResetTimer) clearTimeout(quitRequestResetTimer);
|
||||
isQuitRequested = flag;
|
||||
quitRequestResetTimer = flag
|
||||
? setTimeout(() => {
|
||||
isQuitRequested = false;
|
||||
quitRequestResetTimer = undefined;
|
||||
}, QUIT_REQUEST_TIMEOUT_MS)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export const closeWinAndQuit = (quitApp: () => void): void => {
|
||||
if (mainWin && !mainWin.isDestroyed()) {
|
||||
// Ensure the close handler takes the real close path (not the minimize-to-tray
|
||||
// hide branch) so the before-close IPC flow (sync, finish-day) completes.
|
||||
setIsQuitRequested(true);
|
||||
mainWin.close();
|
||||
} else {
|
||||
// No window to drive the IPC flow through — quit directly. No flag
|
||||
// needed: the close handler that reads it cannot run without a window.
|
||||
quitApp();
|
||||
}
|
||||
};
|
||||
|
||||
export const getIsAppReady = (): boolean => {
|
||||
return mainWinModule.isAppReady;
|
||||
};
|
||||
|
|
@ -466,7 +502,7 @@ function createMenu(quitApp: () => void): void {
|
|||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'CmdOrCtrl+Q',
|
||||
click: quitApp,
|
||||
click: () => closeWinAndQuit(quitApp),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -519,7 +555,7 @@ const appCloseHandler = (app: App): void => {
|
|||
// NOTE: this might not work if we run a second instance of the app
|
||||
log('close event: isQuiting=', getIsQuiting(), 'pendingBeforeCloseIds=', ids);
|
||||
if (!getIsQuiting()) {
|
||||
if (getIsMinimizeToTray()) {
|
||||
if (getIsMinimizeToTray() && !getIsQuitRequested()) {
|
||||
const indicator = ensureIndicator();
|
||||
if (indicator) {
|
||||
event.preventDefault();
|
||||
|
|
@ -542,6 +578,10 @@ const appCloseHandler = (app: App): void => {
|
|||
});
|
||||
|
||||
mainWin.on('closed', () => {
|
||||
// Clear any pending reset timer so it doesn't keep the event loop alive
|
||||
// after the window is gone.
|
||||
setIsQuitRequested(false);
|
||||
|
||||
// Dereference the window object
|
||||
mainWin = null;
|
||||
mainWinModule.win = null;
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ import { CONFIG } from './CONFIG';
|
|||
import { lazySetInterval } from './shared-with-frontend/lazy-set-interval';
|
||||
import { initIndicator } from './indicator';
|
||||
import { quitApp, showOrFocus } from './various-shared';
|
||||
import { createWindow, getWin } from './main-window';
|
||||
import { closeWinAndQuit, createWindow } from './main-window';
|
||||
import { IdleTimeHandler } from './idle-time-handler';
|
||||
import { destroyTaskWidget } from './task-widget/task-widget';
|
||||
import {
|
||||
initializeProtocolHandling,
|
||||
processPendingProtocolUrls,
|
||||
} from './protocol-handler';
|
||||
import { getIsQuiting, setIsLocked, setIsMinimizeToTray } from './shared-state';
|
||||
import { getIsQuiting, setIsLocked } from './shared-state';
|
||||
import { clearStaleLevelDbLocks } from './clear-stale-idb-locks';
|
||||
import { evaluateGpuStartupGuard } from './gpu-startup-guard';
|
||||
import * as fs from 'fs';
|
||||
|
|
@ -430,16 +430,7 @@ export const startApp = (): void => {
|
|||
// which manages the before-close callback flow (sync, finish-day, etc.)
|
||||
// and sets isQuiting=true before re-quitting.
|
||||
event.preventDefault();
|
||||
const win = getWin();
|
||||
if (win && !win.isDestroyed()) {
|
||||
// Ensure the close handler takes the real close path (not the minimize-to-tray
|
||||
// hide branch) so the before-close IPC flow (sync, finish-day) completes.
|
||||
setIsMinimizeToTray(false);
|
||||
win.close();
|
||||
} else {
|
||||
// No window to close — set flag and re-trigger quit directly.
|
||||
quitApp();
|
||||
}
|
||||
closeWinAndQuit(quitApp);
|
||||
return;
|
||||
}
|
||||
// isQuiting=true: all before-close IPC work is complete — safe to clean up.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue