diff --git a/desktop/darwin_link.go b/desktop/darwin_link.go new file mode 100644 index 0000000..7d11b49 --- /dev/null +++ b/desktop/darwin_link.go @@ -0,0 +1,25 @@ +//go:build darwin + +// Link the framework Wails' darwin frontend forgets. +// +// It references UTType (UniformTypeIdentifiers) without linking it, so a macOS +// build fails at the LINK step with `Undefined symbols: _OBJC_CLASS_$_UTType` +// - after compiling everything successfully, which makes it read like a broken +// toolchain rather than one missing flag. That is why there was no Mac build: +// not a design limit, a link error nobody had chased. +// +// Declared in the source rather than passed as CGO_LDFLAGS on the command +// line, for the same reason deploy.sh now finds Go itself: a build that needs +// the operator to know an incantation is a build that does not happen. Plain +// `go build` and `wails build` both work on a Mac with this file present, and +// the build tag makes it inert everywhere else. +// +// Note for anyone editing: the comment directly above `import "C"` is cgo's C +// PREAMBLE, not documentation. This paragraph sits above `package main` on +// purpose - put it there and the prose is compiled as C, which is how the +// first attempt failed. + +package main + +// #cgo LDFLAGS: -framework UniformTypeIdentifiers +import "C" diff --git a/desktop/main.go b/desktop/main.go index a17bcf6..6226888 100644 --- a/desktop/main.go +++ b/desktop/main.go @@ -43,8 +43,13 @@ func main() { UniqueId: "ai.loyaly.behavision.desktop", OnSecondInstanceLaunch: func(options.SecondInstanceData) { if ctxRef != nil { - runtime.Show(ctxRef) - runtime.WindowUnminimise(ctxRef) + // 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) } }, } @@ -68,8 +73,13 @@ func main() { tray.start(ctx) }, OnBeforeClose: func(ctx context.Context) bool { - runtime.Hide(ctx) - return true // prevent the close + // 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() diff --git a/desktop/tray.go b/desktop/tray.go index 9a393db..83dea70 100644 --- a/desktop/tray.go +++ b/desktop/tray.go @@ -99,7 +99,9 @@ func (t *tray) onReady(ctx context.Context) { case <-t.quit: return case <-t.mOpen.ClickedCh: - runtime.Show(ctx) + // In a goroutine, like Start and Stop: this sleeps, and a menu + // loop that sleeps is a tray that ignores the next click. + go openWindow(ctx) case <-t.mStart.ClickedCh: // Never on the menu loop itself: a stop waits for the process to // exit, and a menu that is deaf for the duration looks broken. @@ -214,3 +216,36 @@ func firstLine(s string) string { } return s } + +// openWindow brings the dashboard back, and it takes four calls rather than +// the one that was here. +// +// `runtime.Show` was wrong three times over, and the first is the one that +// made it fail rather than merely misbehave: +// +// 1. WRONG THREAD. Wails implements Show() as a bare `mainWindow.Show()`, +// while WindowShow() wraps the same work in runtime.LockOSThread. Win32 +// window operations have to run on the thread owning the window's message +// pump; this is called from the SYSTRAY's goroutine, which is never that +// thread. An unlocked call from an arbitrary goroutine is why clicking +// "Open dashboard" did nothing reliable. +// +// 2. Showing is not un-minimising. A hidden window and a minimised one are +// different states and Show only fixes the first, so a window the user +// minimised stayed minimised. +// +// 3. Windows will not let a process that is not already in the foreground +// take it - the shell refuses, and the window comes back BEHIND whatever +// is being looked at. Clicking a tray icon is by definition a moment when +// this application is not in the foreground, so that is not an edge case +// here, it is every time. +// +// The always-on-top flip is the ordinary way to ask for the foreground anyway. +// It is brief and it is why this cannot run on the menu loop. +func openWindow(ctx context.Context) { + runtime.WindowUnminimise(ctx) + runtime.WindowShow(ctx) + runtime.WindowSetAlwaysOnTop(ctx, true) + time.Sleep(200 * time.Millisecond) + runtime.WindowSetAlwaysOnTop(ctx, false) +}