From e33761d6d0acfd5e6abe5e7daadd73aedf521c93 Mon Sep 17 00:00:00 2001 From: Suriyakumarvijayanayagam Date: Wed, 30 Sep 2026 11:44:49 +0530 Subject: [PATCH] A Mac build, and the two ways it crashed first Chosen deliberately as a DEVELOPER build, not a product. Indian retail counters are Windows; shipping a Mac product means an Apple Developer account, notarisation, a second installer format, a second frozen engine and DPAPI having no macOS equivalent - a permanent second platform for customers who do not have Macs. What a Mac build is worth is demoing the desktop app on the machine it is written on, without needing the Windows box. It built after one missing framework (previous commit) and then crashed within a second, twice, both times in the tray: systray.Run SIGTRAP inside cgo. nativeLoop() takes the macOS main run loop for itself and Wails already has it. macOS has exactly one. RunWithExternalLoop "NSWindow should only be instantiated on the main thread!" - it registers in the existing NSApplication rather than starting a second, but still builds AppKit objects, and Wails' OnStartup is not the main thread. Making it work needs the status item created through a main-queue dispatch inside Wails' lifecycle. That is real work for a build whose purpose is a demo, so macOS has no tray and the file says so at length rather than leaving the next person to rediscover both crashes. The consequence is handled rather than left lying. With no tray there is no way back from a hidden window and no way to quit, so hiding on close would strand a running engine behind no window, no tray and no control - force-quit or nothing. On macOS closing the window therefore quits, and OnShutdown stops the engine. Same rule the tray's Quit already follows: never leave it watching with no visible control. Windows is untouched, where hiding is correct because the tray is how it comes back. The runner is split by build tag rather than branched at runtime because the two platforms need different systray ENTRY POINTS, not different arguments. Verified: 18 seconds up, zero crash markers, 88 MB resident, and an honest "engine not installed yet" instead of a crash - against a throwaway data dir so it claimed nothing and touched no camera. The frozen Mac engine is deliberately not built; the app takes an engine command from config, which is how the dev setup already points at the venv. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj --- desktop/main.go | 13 ++++++++++++- desktop/tray.go | 4 +--- desktop/tray_run_darwin.go | 28 ++++++++++++++++++++++++++++ desktop/tray_run_windows.go | 10 ++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 desktop/tray_run_darwin.go create mode 100644 desktop/tray_run_windows.go diff --git a/desktop/main.go b/desktop/main.go index 6226888..0f35345 100644 --- a/desktop/main.go +++ b/desktop/main.go @@ -12,6 +12,7 @@ import ( "context" "embed" "log" + runtime2 "runtime" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" @@ -66,13 +67,23 @@ func main() { // Closing the window hides it rather than quitting: the engine must // keep recognising after a shop assistant clicks the X, and the tray // is where they get the window back. - HideWindowOnClose: true, + // Windows hides on close because the tray is how the window comes + // back and how recognition is stopped. macOS has no tray here (see + // tray_run_darwin.go), so hiding would leave a running engine with no + // window, no tray and no way to reach either - force-quit or nothing. + // Closing the window therefore quits, which also stops the engine + // through OnShutdown. Same rule as the tray's Quit: never leave it + // watching with no visible control. + HideWindowOnClose: runtime2.GOOS == "windows", OnStartup: func(ctx context.Context) { ctxRef = ctx app.startup(ctx) tray.start(ctx) }, OnBeforeClose: func(ctx context.Context) bool { + if runtime2.GOOS != "windows" { + return false // let it close, and OnShutdown stops the engine + } // WindowHide, not Hide. They are different calls on Windows - // WindowHide locks the OS thread for the Win32 work and Hide does // not - and the tray's reopen uses WindowShow, so hiding through diff --git a/desktop/tray.go b/desktop/tray.go index 83dea70..9c04879 100644 --- a/desktop/tray.go +++ b/desktop/tray.go @@ -55,9 +55,7 @@ func (t *tray) start(ctx context.Context) { if trayDisabled() { return } - t.once.Do(func() { - go systray.Run(func() { t.onReady(ctx) }, func() {}) - }) + t.once.Do(func() { startSystray(func() { t.onReady(ctx) }) }) } func (t *tray) stop() { diff --git a/desktop/tray_run_darwin.go b/desktop/tray_run_darwin.go new file mode 100644 index 0000000..559cbdf --- /dev/null +++ b/desktop/tray_run_darwin.go @@ -0,0 +1,28 @@ +//go:build darwin + +package main + +// There is no tray on macOS, and that is a decision rather than an omission. +// +// macOS has exactly ONE main run loop and AppKit insists that windows and +// status items are created on it. Wails already owns that loop. Two attempts, +// both crashing within a second of launch: +// +// systray.Run -> SIGTRAP inside cgo: nativeLoop() takes the +// main loop for itself, and Wails has it +// systray.RunWithExternalLoop -> "NSWindow should only be instantiated on +// the main thread!" - it registers in the +// existing NSApplication but still builds +// AppKit objects, and Wails' OnStartup is not +// the main thread +// +// Making it work needs the status item created through a main-queue dispatch +// inside Wails' own lifecycle, which is real work for a build whose entire +// purpose is demoing on a developer's Mac. Windows is the platform this ships +// to and its tray is the shop manager's only control surface; here the window +// is right there in the Dock. +// +// The consequence is handled rather than left: with no tray there would be no +// way back from a hidden window and no way to quit, so on macOS closing the +// window stops the engine and exits. See main.go. +func startSystray(onReady func()) {} diff --git a/desktop/tray_run_windows.go b/desktop/tray_run_windows.go new file mode 100644 index 0000000..30d727a --- /dev/null +++ b/desktop/tray_run_windows.go @@ -0,0 +1,10 @@ +//go:build windows + +package main + +import "fyne.io/systray" + +// systray.Run owns a message loop, and on Windows it is free to have its own: +// the tray lives in its own thread with its own pump, beside the one Wails +// runs for the window. A goroutine is all it needs. +func startSystray(onReady func()) { go systray.Run(onReady, func() {}) }