//go:build windows // Package desktop holds the small Windows shell integrations that do not // belong to any one feature: opening a URL in the default browser, and the // HKCU Run key that starts ipswap with Windows. package desktop import ( "fmt" "os" "path/filepath" "syscall" "unsafe" "golang.org/x/sys/windows/registry" "gitea.apointless.space/bsncubed/ipswap/internal/config" ) var ( shell32 = syscall.NewLazyDLL("shell32.dll") shellExecuteW = shell32.NewProc("ShellExecuteW") ) const runKeyPath = `Software\Microsoft\Windows\CurrentVersion\Run` // OpenURL opens a URL in the default browser. // // ShellExecuteW rather than `cmd /c start`: no console window to suppress and // no quoting rules to get wrong around a URL that already contains "?" and "&". func OpenURL(url string) error { verb, err := syscall.UTF16PtrFromString("open") if err != nil { return err } target, err := syscall.UTF16PtrFromString(url) if err != nil { return err } ret, _, callErr := shellExecuteW.Call( 0, uintptr(unsafe.Pointer(verb)), uintptr(unsafe.Pointer(target)), 0, 0, 1, // no args, no cwd, SW_SHOWNORMAL ) if ret <= 32 { return fmt.Errorf("could not open %s (ShellExecuteW returned %d: %v)", url, ret, callErr) } return nil } // SetRunAtLogin adds or removes ipswap from the current user's Run key. // // HKCU rather than HKLM deliberately: HKLM needs elevation, and ipswap is // designed to run unelevated. It also keeps the setting per-user on a shared // support laptop. func SetRunAtLogin(enabled bool) error { key, _, err := registry.CreateKey(registry.CURRENT_USER, runKeyPath, registry.SET_VALUE) if err != nil { return fmt.Errorf("opening the Run key: %w", err) } defer key.Close() if !enabled { err := key.DeleteValue(config.AppName) if err != nil && err != registry.ErrNotExist { return fmt.Errorf("removing the Run key value: %w", err) } return nil } exe, err := os.Executable() if err != nil { return fmt.Errorf("locating the running executable: %w", err) } exe, err = filepath.Abs(exe) if err != nil { return err } // Quoted: the path routinely contains spaces (the install lives under // %APPDATA%, and a user profile can have a space in its name) and the Run // key value is a command line, not a path. // // --background is what stops a browser tab appearing at every login. A // launch from the shortcut has no such flag and does open the editor. if err := key.SetStringValue(config.AppName, `"`+exe+`" --background`); err != nil { return fmt.Errorf("writing the Run key value: %w", err) } return nil } // RunAtLogin reports whether the Run key entry is present. func RunAtLogin() bool { key, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.QUERY_VALUE) if err != nil { return false } defer key.Close() v, _, err := key.GetStringValue(config.AppName) return err == nil && v != "" }