diff --git a/src/core/autostart_linux.go b/src/core/autostart_linux.go index 856670a..a657b7e 100644 --- a/src/core/autostart_linux.go +++ b/src/core/autostart_linux.go @@ -19,6 +19,11 @@ func SetAutostart(enabled bool, executablePath string, iconPath string) error { if err != nil { return err } + // A desktop scheduler with a tray icon belongs to the graphical session, so + // Linux autostart is implemented through XDG Autostart instead of a systemd + // user service. systemd is tempting because it is explicit and scriptable, + // but it is the wrong owner for a windowed app that should inherit the + // desktop session environment and appear in the tray predictably. if err := cleanupLegacySystemdAutostart(); err != nil { return err } @@ -138,6 +143,9 @@ func cleanupLegacyDesktopAutostart() error { if err != nil { return err } + // The old PySentry desktop file is removed proactively instead of tolerated + // alongside the new one. Leaving both files in place would risk duplicate + // launches or confusing status diagnostics after the rename. if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) { return err } diff --git a/src/core/autostart_windows.go b/src/core/autostart_windows.go index 7e0b25e..484b084 100644 --- a/src/core/autostart_windows.go +++ b/src/core/autostart_windows.go @@ -13,6 +13,11 @@ const legacyAutostartName = "PySentry" const startupShortcutFile = autostartName + ".lnk" func SetAutostart(enabled bool, executablePath string, iconPath string) error { + // Windows autostart used to write HKCU\Run values, but that approach became + // brittle once paths with spaces and the "--start-in-tray" argument entered + // the picture. A Startup-folder shortcut stores target path and arguments as + // separate structured fields, so it avoids quoting bugs and more closely + // matches how a user would configure a GUI app by hand. if err := cleanupLegacyRegistryAutostart(); err != nil { return err } @@ -87,6 +92,10 @@ func createStartupShortcut(shortcutPath string, executablePath string, iconPath if iconPath == "" { iconPath = executablePath } + // WScript.Shell is used here deliberately instead of a third-party Go COM + // wrapper. The PowerShell bridge is not glamorous, but it is already present + // on supported Windows systems and keeps the dependency surface much smaller + // for a project that otherwise aims to stay light. script := `$shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); $shortcut.TargetPath = $env:GOSENTRY_TARGET_PATH; $shortcut.Arguments = $env:GOSENTRY_ARGUMENTS; $shortcut.WorkingDirectory = $env:GOSENTRY_WORKING_DIRECTORY; $shortcut.IconLocation = $env:GOSENTRY_ICON_PATH; $shortcut.Save()` command := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script) command.Env = append(os.Environ(), diff --git a/src/core/store.go b/src/core/store.go index 5cfd88b..5ba0e60 100644 --- a/src/core/store.go +++ b/src/core/store.go @@ -81,6 +81,11 @@ func loadOrCreateConfig(paths Paths) (Config, error) { if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) { legacyPath := filepath.Join(paths.AppDir, LegacyConfigFileName) if _, legacyErr := os.Stat(legacyPath); legacyErr == nil { + // The rename from PySentry to GoSentry changed the preferred config + // filename. Read the old file once if it is still present so portable + // installs continue to start without a manual migration step. The + // caller later saves the loaded config back through SaveConfig, which + // naturally rewrites it under gosentry.yaml. configPath = legacyPath } else { return config, writeYAML(paths.ConfigPath, config) diff --git a/src/gui/app.go b/src/gui/app.go index 3958f57..4e6c50f 100644 --- a/src/gui/app.go +++ b/src/gui/app.go @@ -64,10 +64,17 @@ func Run(startInTray bool) { w.SetContent(content) serveSingleInstance(instanceListener, w) if startInTray { + // Autostart launches intentionally stay hidden, so "window shown" would be + // a misleading metric. Record a separate startup event for the tray path + // instead of forcing one timing definition onto two different UX flows. recordStartup(time.Since(started), false) a.Run() return } + // Show the window before recording startup time. Measuring earlier, during + // widget construction, looked cheaper in History than the user-perceived + // startup really was. The current point is less abstract: it ends when the + // window has actually been handed to the desktop for display. w.Show() recordStartup(time.Since(started), true) a.Run() @@ -112,6 +119,10 @@ func acquireSingleInstance(showExisting bool) (net.Listener, bool) { connection, dialErr := net.DialTimeout("tcp", singleInstanceAddress, time.Second) if dialErr == nil { + // The first instance listens only on localhost and understands one tiny + // command: "show". That keeps the implementation dependency-free and easy + // to inspect, which matters more here than introducing a named-pipe or + // platform-specific IPC abstraction just to focus an existing window. if showExisting { _, _ = io.WriteString(connection, singleInstanceShowCommand) } @@ -183,6 +194,10 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) { commandOutputScroll.SetMinSize(fyne.NewSize(520, 160)) history := newHistoryView(&events) recordStartup := func(duration time.Duration, windowShown bool) { + // Startup is recorded as an in-memory History event instead of being + // persisted into jobs.yaml. It is session diagnostics, not durable job + // state, and keeping it ephemeral avoids polluting the human-editable YAML + // file with process-lifetime bookkeeping. detail := "Window shown in " + duration.Round(time.Millisecond).String() if !windowShown { detail = "Started in tray in " + duration.Round(time.Millisecond).String()