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() {}) }