Release 3.8.2.2: macOS Dock icon turns sea-wave N when connected.
Swap application icon on connect/disconnect via AppKit; ship arm64 build. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+24
-2
@@ -18,6 +18,7 @@ import (
|
||||
"vpnclient/internal/config"
|
||||
"vpnclient/internal/core"
|
||||
"vpnclient/internal/corebin"
|
||||
"vpnclient/internal/dockicon"
|
||||
"vpnclient/internal/logbuf"
|
||||
"vpnclient/internal/netcheck"
|
||||
"vpnclient/internal/protocols/awg"
|
||||
@@ -264,18 +265,23 @@ func (a *App) ConnectProfile(name string) error {
|
||||
perr := a.Mgr.Probe(pctx, "")
|
||||
pcancel()
|
||||
if perr == nil {
|
||||
dockicon.SetConnected(true)
|
||||
return nil
|
||||
}
|
||||
last = fmt.Errorf("туннель не прошёл проверку: %w", perr)
|
||||
a.Mgr.SetLastError(last.Error())
|
||||
_ = a.Mgr.Disconnect()
|
||||
dockicon.SetConnected(false)
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
}
|
||||
dockicon.SetConnected(false)
|
||||
return last
|
||||
}
|
||||
|
||||
func (a *App) Disconnect() error {
|
||||
return a.Mgr.Disconnect()
|
||||
err := a.Mgr.Disconnect()
|
||||
dockicon.SetConnected(false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) GetLogs() string {
|
||||
@@ -303,9 +309,10 @@ func (a *App) SavePrefs(connectOnLaunch, autoReconnect, systemProxy bool) error
|
||||
})
|
||||
}
|
||||
|
||||
// StartBackground runs reconnect-on-crash and optional connect-on-launch.
|
||||
// StartBackground runs reconnect-on-crash, optional connect-on-launch, and Dock icon sync.
|
||||
func (a *App) StartBackground() {
|
||||
go a.watchdogLoop()
|
||||
go a.dockIconLoop()
|
||||
prefs := a.Mgr.Prefs()
|
||||
if prefs.ConnectOnLaunch {
|
||||
go func() {
|
||||
@@ -318,6 +325,21 @@ func (a *App) StartBackground() {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) dockIconLoop() {
|
||||
dockicon.SetConnected(false)
|
||||
var last bool
|
||||
first := true
|
||||
for {
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
up := a.Mgr.Status().Connected
|
||||
if first || up != last {
|
||||
first = false
|
||||
last = up
|
||||
dockicon.SetConnected(up)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) watchdogLoop() {
|
||||
for {
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
//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)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
//go:build !darwin
|
||||
|
||||
package dockicon
|
||||
|
||||
// SetConnected is a no-op outside macOS (Dock icons are a Darwin feature).
|
||||
func SetConnected(connected bool) {}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -22,7 +22,7 @@ const CurrentVersion = "3.8.2"
|
||||
|
||||
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
||||
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
||||
const BuildNumber = 1
|
||||
const BuildNumber = 2
|
||||
|
||||
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
||||
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
||||
|
||||
Reference in New Issue
Block a user