// 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/mac" "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}, // This block has to EXIST, not merely be empty. Wails computes // `zoomable = !Mac.DisableZoom` inside `if frontendOptions.Mac != // nil`, and the variable defaults to 0 - so leaving Mac unset does not // mean "defaults", it means the green maximise button is created dead. // There was a Windows block and no Mac one, so the window could not be // zoomed on macOS and nothing anywhere said why. Mac: &mac.Options{ WebviewIsTransparent: false, WindowIsTranslucent: false, DisableZoom: false, }, 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) } }