opencode-provider-switch/internal/desktop/tray_wails.go
apale7 5c4f60f2b7 feat(desktop): finish Windows GUI integration
Bring the Wails desktop shell to feature parity with the CLI for provider and alias management while keeping the browser fallback working. Wire tray, notifications, autostart, and GUI warnings so desktop flows behave predictably.
2026-04-19 01:02:16 +08:00

210 lines
4.1 KiB
Go

//go:build desktop_wails
package desktop
import (
"context"
"fmt"
"sync"
"time"
"github.com/Apale7/opencode-provider-switch/internal/app"
"github.com/getlantern/systray"
)
// Tray wires resident-mode controls into a native system tray.
type Tray struct {
service *app.Service
mu sync.Mutex
ctx context.Context
prefs app.DesktopPrefsView
registered bool
ready bool
quitting bool
statusItem *systray.MenuItem
showItem *systray.MenuItem
hideItem *systray.MenuItem
startItem *systray.MenuItem
stopItem *systray.MenuItem
quitItem *systray.MenuItem
}
func NewTray(service *app.Service) *Tray {
return &Tray{service: service}
}
func (t *Tray) Attach(ctx context.Context) {
t.mu.Lock()
t.ctx = ctx
if t.registered {
t.mu.Unlock()
return
}
t.registered = true
t.mu.Unlock()
systray.Register(t.onReady, nil)
}
func (t *Tray) Detach() {
t.mu.Lock()
t.ctx = nil
registered := t.registered
t.mu.Unlock()
if registered {
systray.Quit()
}
}
func (t *Tray) Sync(ctx context.Context, prefs app.DesktopPrefsView) {
t.mu.Lock()
if ctx != nil {
t.ctx = ctx
}
t.prefs = prefs
t.mu.Unlock()
t.refresh()
}
func (t *Tray) RefreshProxyStatus(ctx context.Context) {
t.mu.Lock()
if ctx != nil {
t.ctx = ctx
}
t.mu.Unlock()
t.refresh()
}
func (t *Tray) BeforeClose(ctx context.Context) (bool, error) {
t.mu.Lock()
t.ctx = ctx
quitting := t.quitting
minimize := t.prefs.MinimizeToTray
t.mu.Unlock()
if quitting || !minimize {
return false, nil
}
return true, hideWindow(ctx)
}
func (t *Tray) onReady() {
systray.SetTitle("ocswitch")
systray.SetTooltip("ocswitch desktop")
t.mu.Lock()
t.ready = true
t.statusItem = systray.AddMenuItem("Proxy: checking...", "Current proxy status")
t.statusItem.Disable()
systray.AddSeparator()
t.showItem = systray.AddMenuItem("Open window", "Show desktop window")
t.hideItem = systray.AddMenuItem("Hide window", "Hide desktop window")
systray.AddSeparator()
t.startItem = systray.AddMenuItem("Start proxy", "Start local proxy")
t.stopItem = systray.AddMenuItem("Stop proxy", "Stop local proxy")
systray.AddSeparator()
t.quitItem = systray.AddMenuItem("Quit", "Exit application")
t.mu.Unlock()
go t.loop()
t.refresh()
}
func (t *Tray) loop() {
for {
t.mu.Lock()
showItem := t.showItem
hideItem := t.hideItem
startItem := t.startItem
stopItem := t.stopItem
quitItem := t.quitItem
t.mu.Unlock()
select {
case <-showItem.ClickedCh:
_ = t.withContext(showWindow)
case <-hideItem.ClickedCh:
_ = t.withContext(hideWindow)
case <-startItem.ClickedCh:
t.startProxy()
case <-stopItem.ClickedCh:
t.stopProxy()
case <-quitItem.ClickedCh:
t.requestQuit()
return
}
}
}
func (t *Tray) startProxy() {
if t.service == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = t.service.StartProxy(ctx)
t.refresh()
}
func (t *Tray) stopProxy() {
if t.service == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = t.service.StopProxy(ctx)
t.refresh()
}
func (t *Tray) requestQuit() {
t.mu.Lock()
t.quitting = true
ctx := t.ctx
t.mu.Unlock()
if ctx == nil {
return
}
_ = quitWindow(ctx)
}
func (t *Tray) withContext(fn func(context.Context) error) error {
t.mu.Lock()
ctx := t.ctx
t.mu.Unlock()
if ctx == nil {
return nil
}
return fn(ctx)
}
func (t *Tray) refresh() {
t.mu.Lock()
ready := t.ready
statusItem := t.statusItem
startItem := t.startItem
stopItem := t.stopItem
t.mu.Unlock()
if !ready || statusItem == nil || startItem == nil || stopItem == nil || t.service == nil {
return
}
status, err := t.service.GetProxyStatus(context.Background())
if err != nil {
statusItem.SetTitle("Proxy: unavailable")
startItem.Enable()
stopItem.Disable()
return
}
if status.Running {
statusItem.SetTitle(fmt.Sprintf("Proxy: running (%s)", status.BindAddress))
startItem.Disable()
stopItem.Enable()
return
}
statusItem.SetTitle(fmt.Sprintf("Proxy: stopped (%s)", status.BindAddress))
startItem.Enable()
stopItem.Disable()
}