Skip to content

Commit 980b4e1

Browse files
committed
Fix tray icon not appearing after reboot on macOS
The tray icon would fail to appear when the app auto-started at login because launchd was launching it before WindowServer was ready. - Add LimitLoadToSessionType=Aqua to launchd plist to ensure the app only starts when a GUI session is available - Add WaitForGUI() fallback that waits for WindowServer before initializing systray Users with existing auto-start will need to run `nfc-agent uninstall` then `nfc-agent install` to get the updated plist.
1 parent 739714b commit 980b4e1

File tree

4 files changed

+53
-0
lines changed

4 files changed

+53
-0
lines changed

internal/service/service_darwin.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const (
2424
</array>
2525
<key>RunAtLoad</key>
2626
<true/>
27+
<key>LimitLoadToSessionType</key>
28+
<string>Aqua</string>
29+
<key>ProcessType</key>
30+
<string>Interactive</string>
2731
<key>KeepAlive</key>
2832
<dict>
2933
<key>SuccessfulExit</key>

internal/tray/gui_darwin.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//go:build darwin
2+
3+
package tray
4+
5+
import (
6+
"log"
7+
"os/exec"
8+
"time"
9+
)
10+
11+
// WaitForGUI waits for the GUI/WindowServer to be ready.
12+
// Uses pgrep to check for WindowServer process.
13+
// Returns true if GUI is ready, false if timed out.
14+
func WaitForGUI(timeout time.Duration, retryInterval time.Duration) bool {
15+
deadline := time.Now().Add(timeout)
16+
17+
for time.Now().Before(deadline) {
18+
// Check if WindowServer process exists
19+
cmd := exec.Command("pgrep", "-x", "WindowServer")
20+
if err := cmd.Run(); err == nil {
21+
// WindowServer is running, give it a moment to be fully ready
22+
time.Sleep(500 * time.Millisecond)
23+
log.Println("WindowServer is ready")
24+
return true
25+
}
26+
log.Printf("Waiting for WindowServer... (%.0fs remaining)", time.Until(deadline).Seconds())
27+
time.Sleep(retryInterval)
28+
}
29+
30+
log.Println("Timed out waiting for WindowServer")
31+
return false
32+
}

internal/tray/gui_other.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build !darwin
2+
3+
package tray
4+
5+
import "time"
6+
7+
// WaitForGUI is a no-op on non-macOS platforms.
8+
// Returns true immediately as no GUI wait is needed.
9+
func WaitForGUI(timeout time.Duration, retryInterval time.Duration) bool {
10+
return true
11+
}

internal/tray/tray.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os/exec"
99
"runtime"
1010
"sync"
11+
"time"
1112

1213
"github.com/SimplyPrint/nfc-agent/internal/api"
1314
"github.com/SimplyPrint/nfc-agent/internal/core"
@@ -47,6 +48,11 @@ func (t *TrayApp) Run() {
4748
// RunWithServer runs the tray on the main thread and starts the server in a goroutine.
4849
// This function BLOCKS - it must be called from the main goroutine on macOS.
4950
func (t *TrayApp) RunWithServer(serverStart func()) {
51+
// Wait for GUI/WindowServer to be ready (handles macOS startup race condition)
52+
if !WaitForGUI(30*time.Second, 1*time.Second) {
53+
log.Println("Warning: GUI may not be ready, systray initialization may fail")
54+
}
55+
5056
systray.Run(func() {
5157
t.onReady()
5258
if serverStart != nil {

0 commit comments

Comments
 (0)