opencode-provider-switch/internal/desktop/app.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

245 lines
6.4 KiB
Go

package desktop
import (
"context"
"fmt"
"time"
"github.com/Apale7/opencode-provider-switch/internal/app"
)
type trayAdapter interface {
Attach(context.Context)
Detach()
Sync(context.Context, app.DesktopPrefsView)
RefreshProxyStatus(context.Context)
BeforeClose(context.Context) (bool, error)
}
type notifierAdapter interface {
Attach(context.Context)
Detach()
Sync(context.Context, app.DesktopPrefsView)
Send(context.Context, string, string) error
}
type autoStartAdapter interface {
Attach(context.Context)
Detach()
Sync(context.Context, app.DesktopPrefsView) error
}
// App is the desktop-shell composition root. Native shell integrations are kept
// out of internal/app so CLI and GUI can share the same workflows.
type App struct {
service *app.Service
bindings *Bindings
tray trayAdapter
notify notifierAdapter
auto autoStartAdapter
ctx context.Context
version string
}
func New(configPath string) *App {
svc := app.NewService(configPath)
instance := &App{service: svc}
instance.bindings = NewBindings(svc)
instance.tray = NewTray(svc)
instance.notify = NewNotifier(svc)
instance.auto = NewAutoStart(svc)
return instance
}
func (a *App) SetVersion(version string) {
a.version = version
}
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
a.tray.Attach(ctx)
a.notify.Attach(ctx)
a.auto.Attach(ctx)
_ = a.SyncDesktopPreferences(ctx)
a.tray.RefreshProxyStatus(ctx)
}
func (a *App) BeforeClose(ctx context.Context) bool {
a.ctx = ctx
prevent, _ := a.tray.BeforeClose(ctx)
return prevent
}
func (a *App) Shutdown(ctx context.Context) {
a.ctx = ctx
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = a.service.StopProxy(shutdownCtx)
a.tray.Detach()
a.notify.Detach()
a.auto.Detach()
}
func (a *App) SyncDesktopPreferences(ctx context.Context) error {
prefs, err := a.bindings.GetDesktopPrefs(ctx)
if err != nil {
return err
}
if err := a.auto.Sync(ctx, prefs); err != nil {
return err
}
a.tray.Sync(ctx, prefs)
a.notify.Sync(ctx, prefs)
return nil
}
func (a *App) SaveDesktopPrefs(ctx context.Context, in app.DesktopPrefsInput) (app.DesktopPrefsSaveResult, error) {
previous, _ := a.bindings.GetDesktopPrefs(ctx)
prefs, err := a.bindings.SaveDesktopPrefs(ctx, in)
if err != nil {
return app.DesktopPrefsSaveResult{}, err
}
warnings := []string{}
if err := a.auto.Sync(ctx, prefs); err != nil {
warnings = append(warnings, fmt.Sprintf("saved preferences but could not update launch-at-login integration: %v", err))
}
a.tray.Sync(ctx, prefs)
a.notify.Sync(ctx, prefs)
if prefs.Notifications && !previous.Notifications {
_ = a.notify.Send(ctx, "Notifications enabled", "ocswitch desktop will now show native alerts.")
}
return app.DesktopPrefsSaveResult{Prefs: prefs, Warnings: warnings}, nil
}
func (a *App) SavePrefs(in app.DesktopPrefsInput) (app.DesktopPrefsSaveResult, error) {
return a.SaveDesktopPrefs(a.callContext(), in)
}
func (a *App) Meta() map[string]string {
return map[string]string{
"version": a.version,
"shell": a.shellName(),
}
}
func (a *App) Overview() (app.Overview, error) {
return a.bindings.GetOverview(a.callContext())
}
func (a *App) Providers() ([]app.ProviderView, error) {
return a.bindings.ListProviders(a.callContext())
}
func (a *App) Aliases() ([]app.AliasView, error) {
return a.bindings.ListAliases(a.callContext())
}
func (a *App) SaveProvider(in app.ProviderUpsertInput) (app.ProviderSaveResult, error) {
return a.bindings.UpsertProvider(a.callContext(), in)
}
func (a *App) SetProviderState(in app.ProviderStateInput) (app.ProviderView, error) {
return a.bindings.SetProviderDisabled(a.callContext(), in)
}
func (a *App) DeleteProvider(id string) error {
return a.bindings.RemoveProvider(a.callContext(), id)
}
func (a *App) ImportProviders(in app.ProviderImportInput) (app.ProviderImportResult, error) {
return a.bindings.ImportProviders(a.callContext(), in)
}
func (a *App) SaveAlias(in app.AliasUpsertInput) (app.AliasView, error) {
return a.bindings.UpsertAlias(a.callContext(), in)
}
func (a *App) DeleteAlias(name string) error {
return a.bindings.RemoveAlias(a.callContext(), name)
}
func (a *App) BindTarget(in app.AliasTargetInput) (app.AliasView, error) {
return a.bindings.BindAliasTarget(a.callContext(), in)
}
func (a *App) SetTargetState(in app.AliasTargetInput) (app.AliasView, error) {
return a.bindings.SetAliasTargetDisabled(a.callContext(), in)
}
func (a *App) UnbindTarget(in app.AliasTargetInput) (app.AliasView, error) {
return a.bindings.UnbindAliasTarget(a.callContext(), in)
}
func (a *App) DoctorRun() (app.DoctorRunResult, error) {
report, err := a.bindings.RunDoctor(a.callContext())
return app.DoctorRunResult{Report: report, Error: errorString(err)}, nil
}
func (a *App) ProxyStatus() (app.ProxyStatusView, error) {
return a.bindings.GetProxyStatus(a.callContext())
}
func (a *App) StartProxy() (app.ProxyStatusView, error) {
status, err := a.bindings.StartProxy(a.callContext())
if err != nil {
return app.ProxyStatusView{}, err
}
a.tray.RefreshProxyStatus(a.callContext())
_ = a.notify.Send(a.callContext(), "Proxy started", status.BindAddress)
return status, nil
}
func (a *App) StopProxy() (app.ProxyStatusView, error) {
ctx, cancel := context.WithTimeout(a.callContext(), 5*time.Second)
defer cancel()
status, err := a.bindings.StopProxy(ctx)
if err != nil {
return app.ProxyStatusView{}, err
}
a.tray.RefreshProxyStatus(a.callContext())
_ = a.notify.Send(a.callContext(), "Proxy stopped", status.BindAddress)
return status, nil
}
func (a *App) DesktopPrefs() (app.DesktopPrefsView, error) {
return a.bindings.GetDesktopPrefs(a.callContext())
}
func (a *App) PreviewSync(in app.SyncInput) (app.SyncPreview, error) {
return a.bindings.PreviewOpenCodeSync(a.callContext(), in)
}
func (a *App) ApplySync(in app.SyncInput) (app.SyncResult, error) {
result, err := a.bindings.SyncOpenCode(a.callContext(), in)
if err != nil {
return app.SyncResult{}, err
}
if result.Changed && !result.DryRun {
_ = a.notify.Send(a.callContext(), "OpenCode sync applied", result.TargetPath)
}
return result, nil
}
func (a *App) callContext() context.Context {
if a.ctx != nil {
return a.ctx
}
return context.Background()
}
func (a *App) shellName() string {
if a.ctx != nil {
return "wails"
}
return "browser"
}
func (a *App) Service() *app.Service {
return a.service
}
func (a *App) Bindings() *Bindings {
return a.bindings
}