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

152 lines
3.9 KiB
Go

package desktop
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/Apale7/opencode-provider-switch/internal/app"
"github.com/Apale7/opencode-provider-switch/internal/config"
)
// AutoStart manages real launch-at-login integration where the platform permits
// it. Linux uses XDG autostart; Windows uses Startup folder scripts.
type AutoStart struct {
service *app.Service
ctx context.Context
}
func NewAutoStart(service *app.Service) *AutoStart {
return &AutoStart{service: service}
}
func (a *AutoStart) Attach(ctx context.Context) {
a.ctx = ctx
}
func (a *AutoStart) Detach() {
a.ctx = nil
}
func (a *AutoStart) Sync(ctx context.Context, prefs app.DesktopPrefsView) error {
_ = ctx
if runtime.GOOS != "linux" && runtime.GOOS != "windows" {
return nil
}
entryPath, err := a.entryPathForOS(runtime.GOOS)
if err != nil {
return err
}
if prefs.LaunchAtLogin {
return a.writeEntry(entryPath, runtime.GOOS)
}
if err := os.Remove(entryPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove autostart entry: %w", err)
}
return nil
}
func (a *AutoStart) entryPath() (string, error) {
return a.entryPathForOS(runtime.GOOS)
}
func (a *AutoStart) entryPathForOS(goos string) (string, error) {
switch goos {
case "linux":
return a.linuxEntryPath()
case "windows":
return a.windowsEntryPath()
default:
return "", fmt.Errorf("autostart unsupported on %s", goos)
}
}
func (a *AutoStart) linuxEntryPath() (string, error) {
base := strings.TrimSpace(os.Getenv("XDG_CONFIG_HOME"))
if base == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home for autostart: %w", err)
}
base = filepath.Join(home, ".config")
}
return filepath.Join(base, "autostart", "ocswitch-desktop.desktop"), nil
}
func (a *AutoStart) windowsEntryPath() (string, error) {
base := strings.TrimSpace(os.Getenv("APPDATA"))
if base == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home for autostart: %w", err)
}
base = filepath.Join(home, "AppData", "Roaming")
}
return filepath.Join(base, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "ocswitch-desktop.cmd"), nil
}
func (a *AutoStart) writeEntry(path string, goos string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("mkdir autostart dir: %w", err)
}
execPath, err := os.Executable()
if err != nil {
return fmt.Errorf("resolve desktop executable: %w", err)
}
configPath := config.DefaultPath()
if a.service != nil {
configPath = a.service.ConfigPath()
}
var content string
switch goos {
case "linux":
content = linuxAutostartEntry(execPath, configPath)
case "windows":
content = windowsAutostartScript(execPath, configPath)
default:
return fmt.Errorf("autostart unsupported on %s", goos)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
return fmt.Errorf("write autostart entry: %w", err)
}
return nil
}
func linuxAutostartEntry(execPath string, configPath string) string {
content := strings.Join([]string{
"[Desktop Entry]",
"Type=Application",
"Version=1.0",
"Name=ocswitch desktop",
"Comment=OpenCode provider switch desktop shell",
fmt.Sprintf("Exec=%s --config %s", shellQuote(execPath), shellQuote(configPath)),
"Terminal=false",
"X-GNOME-Autostart-enabled=true",
"Categories=Network;Development;",
"StartupNotify=false",
"",
}, "\n")
return content
}
func shellQuote(value string) string {
replacer := strings.NewReplacer("\\", "\\\\", `"`, `\\"`)
return `"` + replacer.Replace(value) + `"`
}
func windowsAutostartScript(execPath string, configPath string) string {
return strings.Join([]string{
"@echo off",
fmt.Sprintf("start \"\" %s --config %s", cmdQuote(execPath), cmdQuote(configPath)),
"",
}, "\r\n")
}
func cmdQuote(value string) string {
replacer := strings.NewReplacer(`"`, `""`)
return `"` + replacer.Replace(value) + `"`
}