Files
Behavision/desktop/main.go
Suriyakumarvijayanayagam e33761d6d0 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-30 11:44:49 +05:30

113 lines
4.2 KiB
Go

// Command behavision-desktop is the store-facing application: a tray icon, a
// window, and the supervisor for the recognition engine.
//
// It is one process rather than three because the tray, the window and the
// supervisor all need the same state, and because a user who quits the tray
// expects recognition to stop. It is deliberately NOT a Windows service: a
// service runs in session 0 and cannot draw a tray icon, and spawning a child
// process needs no elevation while controlling a service does.
package main
import (
"context"
"embed"
"log"
runtime2 "runtime"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
app := NewApp()
tray := newTray(app)
// One process per PC, enforced by the OS rather than by hoping.
//
// The window hides to the tray on close, so the ordinary next thing a shop
// assistant does is double-click the desktop shortcut again to get it
// back. Without this lock that started a SECOND complete copy: a second
// tray icon, a second engine supervisor on the same SQLite WAL and the
// same port - the "start twice" failure the agent package exists to
// prevent, on the one binary that never had the guard. Seen on a Windows
// install as a row of Behavision icons in the tray. A second launch now
// only brings the existing window to the front, which is what the person
// wanted in the first place.
var ctxRef context.Context
single := &options.SingleInstanceLock{
UniqueId: "ai.loyaly.behavision.desktop",
OnSecondInstanceLaunch: func(options.SecondInstanceData) {
if ctxRef != nil {
// The same four calls the tray uses, and for the same reasons:
// this runs on Wails' own listener goroutine rather than the
// window's thread, and the launching process holds the
// foreground, so without the flip the window comes back behind
// it. Double-clicking the desktop icon while it is already
// running is the single most common way anyone reaches this.
go openWindow(ctxRef)
}
},
}
err := wails.Run(&options.App{
SingleInstanceLock: single,
Title: "Behavision",
Width: 1280,
Height: 820,
// Small enough to still be usable on a cramped shop-counter monitor.
MinWidth: 1024,
MinHeight: 640,
AssetServer: &assetserver.Options{Assets: assets},
// 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.
// 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
// the other one leaves the pair mismatched. Same call, opposite
// direction.
runtime.WindowHide(ctx)
return true // prevent the close; the tray is how it comes back
},
OnShutdown: func(ctx context.Context) {
tray.stop()
app.proxy.stop()
app.StopEngine()
},
Bind: []any{app},
Windows: &windows.Options{
WebviewIsTransparent: false,
WindowIsTranslucent: false,
// A shop PC is not a developer machine; a stray right-click that
// opens devtools looks like the software is broken.
DisableWindowIcon: false,
},
})
if err != nil {
log.Fatalf("behavision-desktop: %v", err)
}
}