diff --git a/mkdocs-website/docs/en/changelog.md b/mkdocs-website/docs/en/changelog.md index c0913a9de23..74ba755510c 100644 --- a/mkdocs-website/docs/en/changelog.md +++ b/mkdocs-website/docs/en/changelog.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added Support for StartAtLogin by [ansxuman](https://github.com/ansxuman) in [#3910](https://github.com/wailsapp/wails/pull/3910) - Support of linux packaging of deb,rpm, and arch linux packager builds by @atterpac in [#3909](https://github.com/wailsapp/wails/3909) - Added Support for darwin universal builds and packages by [ansxuman](https://github.com/ansxuman) in [#3902](https://github.com/wailsapp/wails/pull/3902) - Events documentation to the mkdocs webite by [atterpac](https://github.com/atterpac) in [#3867](https://github.com/wailsapp/wails/pull/3867) diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 33d47a70cfc..1f9c5af61ec 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -169,6 +169,8 @@ func New(appOptions Options) *App { result.OnShutdown(appOptions.OnShutdown) } + result.startAtLoginOnRun = appOptions.StartAtLogin + return result } @@ -202,6 +204,8 @@ type ( GetFlags(options Options) map[string]any isOnMainThread() bool isDarkMode() bool + setStartAtLogin(enabled bool) error + canStartAtLogin() bool } runnable interface { @@ -352,6 +356,8 @@ type App struct { // Wails ApplicationEvent Listener related wailsEventListenerLock sync.Mutex wailsEventListeners []WailsEventListener + + startAtLoginOnRun bool } func (a *App) handleWarning(msg string) { @@ -639,6 +645,11 @@ func (a *App) Run() error { a.impl.setIcon(a.options.Icon) } + if err := a.SetStartAtLogin(a.startAtLoginOnRun); err != nil { + a.Logger.Error("SetStartAtLogin() failed:", + "error", err.Error()) + } + err = a.impl.run() if err != nil { return err @@ -1044,3 +1055,11 @@ func (a *App) Path(selector Path) string { func (a *App) Paths(selector Paths) []string { return pathdirs[selector] } + +func (a *App) SetStartAtLogin(enabled bool) error { + if !a.impl.canStartAtLogin() { + a.Logger.Warn("SetStartAtLogin: Not supported in current configuration") + return nil + } + return a.impl.setStartAtLogin(enabled) +} diff --git a/v3/pkg/application/application_darwin.go b/v3/pkg/application/application_darwin.go index 0e8fa1c1235..443b5c131cc 100644 --- a/v3/pkg/application/application_darwin.go +++ b/v3/pkg/application/application_darwin.go @@ -161,12 +161,19 @@ static const char* serializationNSDictionary(void *dict) { import "C" import ( "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" "unsafe" + "github.com/pkg/errors" "github.com/wailsapp/wails/v3/internal/operatingsystem" "github.com/wailsapp/wails/v3/internal/assetserver/webview" "github.com/wailsapp/wails/v3/pkg/events" + "github.com/wailsapp/wails/v3/pkg/mac" ) type macosApp struct { @@ -377,3 +384,42 @@ func HandleOpenFile(filePath *C.char) { ctx: eventContext, } } + +func (m *macosApp) setStartAtLogin(enabled bool) error { + exe, err := os.Executable() + if err != nil { + return errors.Wrap(err, "Error running os.Executable:") + } + + binName := filepath.Base(exe) + if !strings.HasSuffix(exe, "/Contents/MacOS/"+binName) { + return fmt.Errorf("app needs to be running as package.app file to start at login") + } + + appPath := strings.TrimSuffix(exe, "/Contents/MacOS/"+binName) + var command string + if enabled { + command = fmt.Sprintf("tell application \"System Events\" to make login item at end with properties {name: \"%s\",path:\"%s\", hidden:false}", binName, appPath) + } else { + command = fmt.Sprintf("tell application \"System Events\" to delete login item \"%s\"", binName) + } + + cmd := exec.Command("osascript", "-e", command) + _, err = cmd.CombinedOutput() + return err +} + +func (m *macosApp) canStartAtLogin() bool { + bundleID := mac.GetBundleID() + if bundleID == "" { + return false + } + + exe, err := os.Executable() + if err != nil { + return false + } + + binName := filepath.Base(exe) + return strings.HasSuffix(exe, "/Contents/MacOS/"+binName) +} diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index 19f789c05af..205532d71c2 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -16,8 +16,10 @@ import "C" import ( "fmt" "os" + "path/filepath" "strings" "sync" + "text/template" "github.com/godbus/dbus/v5" "github.com/wailsapp/wails/v3/internal/operatingsystem" @@ -260,3 +262,68 @@ func fatalHandler(errFunc func(error)) { // Stub for windows function return } + +func (l *linuxApp) setStartAtLogin(enabled bool) error { + homedir, err := os.UserHomeDir() + if err != nil { + return err + } + + bin, err := os.Executable() + if err != nil { + return err + } + + name := filepath.Base(bin) + autostartFile := fmt.Sprintf("%s-autostart.desktop", name) + autostartPath := filepath.Join(homedir, ".config", "autostart", autostartFile) + + if !enabled { + err := os.Remove(autostartPath) + if os.IsNotExist(err) { + return nil + } + return err + } + + const tpl = `[Desktop Entry] +Name={{.Name}} +Comment=Autostart service for {{.Name}} +Type=Application +Exec={{.Cmd}} +Hidden=true +X-GNOME-Autostart-enabled=true +` + if err := os.MkdirAll(filepath.Dir(autostartPath), 0755); err != nil { + return err + } + + file, err := os.OpenFile(autostartPath, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return err + } + defer file.Close() + + t := template.Must(template.New("autostart").Parse(tpl)) + return t.Execute(file, struct { + Name string + Cmd string + }{ + Name: name, + Cmd: bin, + }) +} + +func (l *linuxApp) canStartAtLogin() bool { + homedir, err := os.UserHomeDir() + if err != nil { + return false + } + + autostartDir := filepath.Join(homedir, ".config", "autostart") + if err := os.MkdirAll(autostartDir, 0755); err != nil { + return false + } + + return true +} diff --git a/v3/pkg/application/application_options.go b/v3/pkg/application/application_options.go index 171b7e8a543..428766d7d13 100644 --- a/v3/pkg/application/application_options.go +++ b/v3/pkg/application/application_options.go @@ -117,6 +117,9 @@ type Options struct { // The '.' is required FileAssociations []string + // StartAtLogin determines if the application should start when the user logs in + StartAtLogin bool + // This blank field ensures types from other packages // are never convertible to Options. // This property, in turn, improves the accuracy of the binding generator. diff --git a/v3/pkg/application/application_windows.go b/v3/pkg/application/application_windows.go index 78d75f17a70..cb797034381 100644 --- a/v3/pkg/application/application_windows.go +++ b/v3/pkg/application/application_windows.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "slices" + "strings" "sync" "syscall" "unsafe" @@ -16,6 +17,7 @@ import ( "github.com/wailsapp/wails/v3/pkg/events" "github.com/wailsapp/wails/v3/pkg/w32" + "golang.org/x/sys/windows/registry" ) type windowsApp struct { @@ -369,3 +371,33 @@ func fatalHandler(errFunc func(error)) { w32.Fatal = errFunc return } + +func (m *windowsApp) setStartAtLogin(enabled bool) error { + exePath, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %s", err) + } + + registryKey := strings.Split(filepath.Base(exePath), ".")[0] + + key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.ALL_ACCESS) + if err != nil { + return fmt.Errorf("failed to open registry key: %s", err) + } + defer key.Close() + + if enabled { + err = key.SetStringValue(registryKey, exePath) + } else { + err = key.DeleteValue(registryKey) + if err == registry.ErrNotExist { + return nil + } + } + return err +} + +func (m *windowsApp) canStartAtLogin() bool { + // Windows generally supports start at login via registry + return true +}