//go:build darwin package dockicon import ( _ "embed" "log" "sync" "unsafe" "github.com/ebitengine/purego" "github.com/ebitengine/purego/objc" ) //go:embed icon_idle.png var iconIdlePNG []byte //go:embed icon_connected.png var iconConnectedPNG []byte var ( once sync.Once ready bool mu sync.Mutex last *bool selSet objc.SEL selPerf objc.SEL selShared objc.SEL selData objc.SEL selAlloc objc.SEL selInit objc.SEL selRetain objc.SEL clsApp objc.Class clsData objc.Class clsImage objc.Class ) func initAppKit() { once.Do(func() { if _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil { log.Printf("dockicon: Foundation: %v", err) return } if _, err := purego.Dlopen("/System/Library/Frameworks/AppKit.framework/AppKit", purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil { log.Printf("dockicon: AppKit: %v", err) return } clsApp = objc.GetClass("NSApplication") clsData = objc.GetClass("NSData") clsImage = objc.GetClass("NSImage") if clsApp == 0 || clsData == 0 || clsImage == 0 { log.Printf("dockicon: missing AppKit classes") return } selShared = objc.RegisterName("sharedApplication") selSet = objc.RegisterName("setApplicationIconImage:") selPerf = objc.RegisterName("performSelectorOnMainThread:withObject:waitUntilDone:") selData = objc.RegisterName("dataWithBytes:length:") selAlloc = objc.RegisterName("alloc") selInit = objc.RegisterName("initWithData:") selRetain = objc.RegisterName("retain") ready = true }) } // SetConnected swaps the Dock icon: sea-wave N when connected, original when idle. func SetConnected(connected bool) { initAppKit() if !ready { return } mu.Lock() if last != nil && *last == connected { mu.Unlock() return } v := connected last = &v mu.Unlock() png := iconIdlePNG if connected { png = iconConnectedPNG } if len(png) == 0 { return } data := objc.ID(clsData).Send(selData, unsafe.Pointer(&png[0]), len(png)) if data == 0 { return } img := objc.ID(clsImage).Send(selAlloc) img = img.Send(selInit, data) if img == 0 { return } img.Send(selRetain) // keep alive across async main-thread call app := objc.ID(clsApp).Send(selShared) if app == 0 { return } // AppKit must run on the main thread (glaze owns it). app.Send(selPerf, selSet, img, false) }