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 } autoErr := a.auto.Sync(ctx, prefs) a.tray.Sync(ctx, prefs) a.notify.Sync(ctx, prefs) return autoErr } 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) ExportConfig() (app.ConfigExportView, error) { return a.bindings.ExportConfig(a.callContext()) } func (a *App) ImportConfig(in app.ConfigImportInput) (app.ConfigImportResult, error) { result, err := a.bindings.ImportConfig(a.callContext(), in) if err != nil { return app.ConfigImportResult{}, err } if syncErr := a.SyncDesktopPreferences(a.callContext()); syncErr != nil { result.Warnings = append(result.Warnings, fmt.Sprintf("imported config but could not sync desktop integrations: %v", syncErr)) } a.tray.RefreshProxyStatus(a.callContext()) return result, nil } 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 }