From fea1d664f10707f03353c2a3a4848fa411a4800a Mon Sep 17 00:00:00 2001 From: SivanCola Date: Fri, 5 Jun 2026 15:36:27 +0800 Subject: [PATCH] fix(desktop): let macOS system quit bypass background close (#3203) Let macOS system-level Quit (Dock Quit, app menu Quit, Cmd-Q) bypass the close-to-background preference. Swizzles AppDelegate applicationShouldTerminate: and WailsContext Quit to mark a system-quit flag that beforeClose consumes; ordinary window close behavior unchanged and covered by a regression test. --- desktop/app.go | 3 ++- desktop/app_test.go | 22 ++++++++++++++++++ desktop/system_quit.go | 13 +++++++++++ desktop/system_quit_darwin.go | 24 +++++++++++++++++++ desktop/system_quit_darwin.m | 44 +++++++++++++++++++++++++++++++++++ desktop/system_quit_stub.go | 5 ++++ 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 desktop/system_quit.go create mode 100644 desktop/system_quit_darwin.go create mode 100644 desktop/system_quit_darwin.m create mode 100644 desktop/system_quit_stub.go diff --git a/desktop/app.go b/desktop/app.go index 0ade358076..e0e4b6a49b 100644 --- a/desktop/app.go +++ b/desktop/app.go @@ -89,12 +89,13 @@ func (a *App) Platform() string { // off the initialization in a background goroutine so the webview loads immediately. func (a *App) startup(ctx context.Context) { a.ctx = ctx + installSystemQuitHook() go a.restoreOrBuildTabs() } func (a *App) beforeClose(ctx context.Context) bool { - if a.forceQuit.Swap(false) { + if a.forceQuit.Swap(false) || consumeSystemQuitRequested() { return false } cfg, _, err := a.loadDesktopUserConfigForEdit() diff --git a/desktop/app_test.go b/desktop/app_test.go index d5990483cf..28f7ca7177 100644 --- a/desktop/app_test.go +++ b/desktop/app_test.go @@ -73,6 +73,28 @@ func TestEffortDefaultsBeforeStartup(t *testing.T) { } } +func TestBeforeCloseAllowsSystemQuitWhenBackgroundCloseEnabled(t *testing.T) { + isolateDesktopUserDirs(t) + consumeSystemQuitRequested() + t.Cleanup(func() { consumeSystemQuitRequested() }) + + userCfg := config.LoadForEdit(config.UserConfigPath()) + if err := userCfg.SetDesktopCloseBehavior("background"); err != nil { + t.Fatal(err) + } + if err := userCfg.SaveTo(config.UserConfigPath()); err != nil { + t.Fatal(err) + } + + markSystemQuitRequested() + if prevent := NewApp().beforeClose(context.Background()); prevent { + t.Fatal("system quit should bypass background close-to-tray behavior") + } + if consumeSystemQuitRequested() { + t.Fatal("system quit marker should be consumed by beforeClose") + } +} + func TestEmitReadyInvokesReadyHook(t *testing.T) { app := NewApp() var calls int32 diff --git a/desktop/system_quit.go b/desktop/system_quit.go new file mode 100644 index 0000000000..e0a812fb4b --- /dev/null +++ b/desktop/system_quit.go @@ -0,0 +1,13 @@ +package main + +import "sync/atomic" + +var systemQuitRequested atomic.Bool + +func markSystemQuitRequested() { + systemQuitRequested.Store(true) +} + +func consumeSystemQuitRequested() bool { + return systemQuitRequested.Swap(false) +} diff --git a/desktop/system_quit_darwin.go b/desktop/system_quit_darwin.go new file mode 100644 index 0000000000..22c1c64a0d --- /dev/null +++ b/desktop/system_quit_darwin.go @@ -0,0 +1,24 @@ +//go:build darwin + +package main + +/* +#cgo darwin LDFLAGS: -framework Cocoa +void installReasonixSystemQuitHook(void); +*/ +import "C" + +import "sync" + +var installSystemQuitHookOnce sync.Once + +func installSystemQuitHook() { + installSystemQuitHookOnce.Do(func() { + C.installReasonixSystemQuitHook() + }) +} + +//export ReasonixMarkSystemQuit +func ReasonixMarkSystemQuit() { + markSystemQuitRequested() +} diff --git a/desktop/system_quit_darwin.m b/desktop/system_quit_darwin.m new file mode 100644 index 0000000000..d364818f66 --- /dev/null +++ b/desktop/system_quit_darwin.m @@ -0,0 +1,44 @@ +//go:build darwin + +#import +#import + +extern void ReasonixMarkSystemQuit(void); + +static NSApplicationTerminateReply (*originalApplicationShouldTerminate)(id, SEL, NSApplication *); +static void (*originalWailsContextQuit)(id, SEL); + +static NSApplicationTerminateReply reasonixApplicationShouldTerminate(id self, SEL _cmd, NSApplication *sender) { + ReasonixMarkSystemQuit(); + if (originalApplicationShouldTerminate != NULL) { + return originalApplicationShouldTerminate(self, _cmd, sender); + } + return NSTerminateNow; +} + +static void reasonixWailsContextQuit(id self, SEL _cmd) { + ReasonixMarkSystemQuit(); + if (originalWailsContextQuit != NULL) { + originalWailsContextQuit(self, _cmd); + } +} + +void installReasonixSystemQuitHook(void) { + Class appDelegate = NSClassFromString(@"AppDelegate"); + SEL selector = @selector(applicationShouldTerminate:); + Method method = appDelegate == Nil ? NULL : class_getInstanceMethod(appDelegate, selector); + if (method != NULL) { + IMP replacement = (IMP)reasonixApplicationShouldTerminate; + IMP previous = method_setImplementation(method, replacement); + originalApplicationShouldTerminate = (NSApplicationTerminateReply (*)(id, SEL, NSApplication *))previous; + } + + Class wailsContext = NSClassFromString(@"WailsContext"); + SEL quitSelector = @selector(Quit); + Method quitMethod = wailsContext == Nil ? NULL : class_getInstanceMethod(wailsContext, quitSelector); + if (quitMethod != NULL) { + IMP replacement = (IMP)reasonixWailsContextQuit; + IMP previous = method_setImplementation(quitMethod, replacement); + originalWailsContextQuit = (void (*)(id, SEL))previous; + } +} diff --git a/desktop/system_quit_stub.go b/desktop/system_quit_stub.go new file mode 100644 index 0000000000..abc739fd35 --- /dev/null +++ b/desktop/system_quit_stub.go @@ -0,0 +1,5 @@ +//go:build !darwin + +package main + +func installSystemQuitHook() {}