Start autostart launches in tray

Add a shared --start-in-tray argument that lets autostart start the scheduler and tray integration without opening the main window.

Write the argument into Windows Startup shortcuts and Linux XDG Autostart desktop entries, and verify existing autostart entries include it.

Keep manual launches unchanged and let a manual second launch reveal an already-running instance while duplicate autostart launches stay hidden.
This commit is contained in:
mixeme
2026-06-16 21:52:00 +03:00
parent 44f24ab3d8
commit b1fe8bd675
9 changed files with 99 additions and 20 deletions
+4 -3
View File
@@ -30,11 +30,11 @@ func SetAutostart(enabled bool, executablePath string, iconPath string) error {
Type=Application
Name=PySentry
Comment=PySentry desktop scheduler
Exec=%s
Exec=%s %s
%s
Terminal=false
X-GNOME-Autostart-enabled=true
`, quoteDesktopExec(executablePath), desktopIconLine(iconPath))
`, quoteDesktopExec(executablePath), StartInTrayArgument, desktopIconLine(iconPath))
return os.WriteFile(desktopPath, []byte(desktopFile), 0o644)
}
@@ -63,7 +63,8 @@ func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string)
if readErr != nil {
return false, "Autostart desktop entry is missing"
}
if !strings.Contains(string(data), "Exec="+quoteDesktopExec(executablePath)) {
expectedExec := "Exec=" + quoteDesktopExec(executablePath) + " " + StartInTrayArgument
if !strings.Contains(string(data), expectedExec) {
return false, "Autostart desktop entry points to another executable"
}
return true, "Autostart is configured"
+32
View File
@@ -0,0 +1,32 @@
//go:build linux
package core
import (
"os"
"strings"
"testing"
)
func TestLinuxAutostartStartsInTray(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
executablePath := "/opt/Go Sentry/gosentry"
if err := SetAutostart(true, executablePath, "/opt/Go Sentry/gosentry.png"); err != nil {
t.Fatalf("enable autostart: %v", err)
}
desktopPath, err := autostartDesktopPath()
if err != nil {
t.Fatalf("resolve desktop path: %v", err)
}
data, err := os.ReadFile(desktopPath)
if err != nil {
t.Fatalf("read desktop entry: %v", err)
}
expectedExec := "Exec=" + quoteDesktopExec(executablePath) + " " + StartInTrayArgument
if !strings.Contains(string(data), expectedExec) {
t.Fatalf("desktop entry does not start in tray: %s", data)
}
}
+21 -6
View File
@@ -57,13 +57,16 @@ func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string)
return false, "Autostart shortcut cannot be checked"
}
actual, err := readShortcutTarget(shortcutPath)
actual, arguments, err := readShortcut(shortcutPath)
if err != nil {
return false, "Autostart shortcut cannot be read"
}
if !sameWindowsPath(actual, executablePath) {
return false, "Autostart shortcut points to another executable"
}
if strings.TrimSpace(arguments) != StartInTrayArgument {
return false, "Autostart shortcut does not start in tray"
}
return true, "Autostart is configured"
}
@@ -84,11 +87,12 @@ func createStartupShortcut(shortcutPath string, executablePath string, iconPath
if iconPath == "" {
iconPath = executablePath
}
script := `$shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); $shortcut.TargetPath = $env:GOSENTRY_TARGET_PATH; $shortcut.WorkingDirectory = $env:GOSENTRY_WORKING_DIRECTORY; $shortcut.IconLocation = $env:GOSENTRY_ICON_PATH; $shortcut.Save()`
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(),
"GOSENTRY_SHORTCUT_PATH="+shortcutPath,
"GOSENTRY_TARGET_PATH="+executablePath,
"GOSENTRY_ARGUMENTS="+StartInTrayArgument,
"GOSENTRY_WORKING_DIRECTORY="+workingDirectory,
"GOSENTRY_ICON_PATH="+iconPath,
)
@@ -99,16 +103,27 @@ func createStartupShortcut(shortcutPath string, executablePath string, iconPath
return nil
}
func readShortcutTarget(shortcutPath string) (string, error) {
script := `$shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); [Console]::Out.Write($shortcut.TargetPath)`
func readShortcut(shortcutPath string) (string, string, error) {
script := `$shell = New-Object -ComObject WScript.Shell; $shortcut = $shell.CreateShortcut($env:GOSENTRY_SHORTCUT_PATH); [Console]::Out.Write($shortcut.TargetPath + [Environment]::NewLine + $shortcut.Arguments)`
command := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script)
command.Env = append(os.Environ(), "GOSENTRY_SHORTCUT_PATH="+shortcutPath)
configureHiddenWindow(command)
output, err := command.CombinedOutput()
if err != nil {
return "", fmt.Errorf("read startup shortcut: %w: %s", err, strings.TrimSpace(string(output)))
return "", "", fmt.Errorf("read startup shortcut: %w: %s", err, strings.TrimSpace(string(output)))
}
return strings.TrimSpace(string(output)), nil
lines := strings.SplitN(string(output), "\n", 2)
target := strings.TrimSpace(lines[0])
arguments := ""
if len(lines) > 1 {
arguments = strings.TrimSpace(lines[1])
}
return target, arguments, nil
}
func readShortcutTarget(shortcutPath string) (string, error) {
target, _, err := readShortcut(shortcutPath)
return target, err
}
func removeIfExists(path string) error {
+4 -1
View File
@@ -63,11 +63,14 @@ func TestCreateStartupShortcutHandlesSpaces(t *testing.T) {
t.Fatalf("create shortcut: %v", err)
}
actual, err := readShortcutTarget(shortcutPath)
actual, arguments, err := readShortcut(shortcutPath)
if err != nil {
t.Fatalf("read shortcut: %v", err)
}
if !sameWindowsPath(actual, targetPath) {
t.Fatalf("shortcut target mismatch: got %q want %q", actual, targetPath)
}
if arguments != StartInTrayArgument {
t.Fatalf("shortcut arguments mismatch: got %q want %q", arguments, StartInTrayArgument)
}
}
+5
View File
@@ -2,6 +2,11 @@ package core
import "time"
// StartInTrayArgument is written to the Windows Startup shortcut so autostart
// can keep the scheduler running without flashing the main window. Manual
// launches omit this flag and open the normal window.
const StartInTrayArgument = "--start-in-tray"
// Config is stored in pysentry.yaml next to the program. It contains only
// application-level choices: where to read jobs from, where to write logs, and
// how the desktop shell should behave.
+10 -4
View File
@@ -42,9 +42,9 @@ const singleInstanceShowCommand = "show"
type job = core.Job
type event = core.RunRecord
func Run() {
func Run(startInTray bool) {
started := time.Now()
instanceListener, primary := acquireSingleInstance()
instanceListener, primary := acquireSingleInstance(!startInTray)
if !primary {
return
}
@@ -62,6 +62,10 @@ func Run() {
w.Resize(fyne.NewSize(1120, 720))
w.SetContent(newMainView(w, started))
serveSingleInstance(instanceListener, w)
if startInTray {
a.Run()
return
}
w.ShowAndRun()
}
@@ -96,7 +100,7 @@ func configureSystemTray(a fyne.App, w fyne.Window) {
})
}
func acquireSingleInstance() (net.Listener, bool) {
func acquireSingleInstance(showExisting bool) (net.Listener, bool) {
listener, err := net.Listen("tcp", singleInstanceAddress)
if err == nil {
return listener, true
@@ -104,7 +108,9 @@ func acquireSingleInstance() (net.Listener, bool) {
connection, dialErr := net.DialTimeout("tcp", singleInstanceAddress, time.Second)
if dialErr == nil {
_, _ = io.WriteString(connection, singleInstanceShowCommand)
if showExisting {
_, _ = io.WriteString(connection, singleInstanceShowCommand)
}
_ = connection.Close()
return nil, false
}