Swap application icon on connect/disconnect via AppKit; ship arm64 build. Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
2.4 KiB
Go
107 lines
2.4 KiB
Go
//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)
|
|
}
|