Skip to content

Commit 3f7afa9

Browse files
committed
improve Linux compatibility
1 parent 9971e2d commit 3f7afa9

File tree

10 files changed

+321
-133
lines changed

10 files changed

+321
-133
lines changed

cmd/goose/icons.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package main
2+
3+
import (
4+
_ "embed"
5+
"log/slog"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
// Embed icon files at compile time for better distribution
11+
//
12+
//go:embed icons/goose.png
13+
var iconGoose []byte
14+
15+
//go:embed icons/popper.png
16+
var iconPopper []byte
17+
18+
//go:embed icons/smiling-face.png
19+
var iconSmiling []byte
20+
21+
//go:embed icons/lock.png
22+
var iconLock []byte
23+
24+
//go:embed icons/warning.png
25+
var iconWarning []byte
26+
27+
// IconType represents different icon states
28+
type IconType int
29+
30+
const (
31+
IconSmiling IconType = iota // No blocked PRs
32+
IconGoose // Incoming PRs blocked
33+
IconPopper // Outgoing PRs blocked
34+
IconBoth // Both incoming and outgoing blocked
35+
IconWarning // General error/warning
36+
IconLock // Authentication error
37+
)
38+
39+
// getIcon returns the icon bytes for the given type
40+
func getIcon(iconType IconType) []byte {
41+
switch iconType {
42+
case IconGoose:
43+
return iconGoose
44+
case IconPopper:
45+
return iconPopper
46+
case IconSmiling:
47+
return iconSmiling
48+
case IconWarning:
49+
return iconWarning
50+
case IconLock:
51+
return iconLock
52+
case IconBoth:
53+
// For both, we'll use the goose icon as primary
54+
return iconGoose
55+
default:
56+
return iconSmiling
57+
}
58+
}
59+
60+
// loadIconFromFile loads an icon from the filesystem (fallback if embed fails)
61+
func loadIconFromFile(filename string) []byte {
62+
iconPath := filepath.Join("icons", filename)
63+
data, err := os.ReadFile(iconPath)
64+
if err != nil {
65+
slog.Warn("Failed to load icon file", "path", iconPath, "error", err)
66+
return nil
67+
}
68+
return data
69+
}
70+
71+
// setTrayIcon updates the system tray icon based on PR counts
72+
func (app *App) setTrayIcon(iconType IconType) {
73+
iconBytes := getIcon(iconType)
74+
if iconBytes == nil || len(iconBytes) == 0 {
75+
slog.Warn("Icon bytes are empty, skipping icon update", "type", iconType)
76+
return
77+
}
78+
79+
app.systrayInterface.SetIcon(iconBytes)
80+
slog.Debug("[TRAY] Setting icon", "type", iconType)
81+
}

cmd/goose/icons/goose.png

16 KB
Loading

cmd/goose/icons/lock.png

9.6 KB
Loading

cmd/goose/icons/popper.png

30.8 KB
Loading

cmd/goose/icons/smiling-face.png

50.2 KB
Loading

cmd/goose/icons/warning.png

9.72 KB
Loading

cmd/goose/main.go

Lines changed: 100 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"log/slog"
1212
"os"
1313
"path/filepath"
14+
"runtime"
1415
"slices"
1516
"strings"
1617
"sync"
@@ -192,7 +193,7 @@ func main() {
192193
Level: slog.LevelInfo,
193194
}
194195
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, opts)))
195-
slog.Info("Starting GitHub PR Monitor", "version", version, "commit", commit, "date", date)
196+
slog.Info("Starting Goose", "version", version, "commit", commit, "date", date)
196197
slog.Info("Configuration", "update_interval", updateInterval, "max_retries", maxRetries, "max_delay", maxRetryDelay)
197198
slog.Info("Browser auto-open configuration", "startup_delay", browserOpenDelay, "max_per_minute", maxBrowserOpensMinute, "max_per_day", maxBrowserOpensDay)
198199

@@ -283,23 +284,87 @@ func main() {
283284
func (app *App) onReady(ctx context.Context) {
284285
slog.Info("System tray ready")
285286

287+
// On Linux, immediately build a minimal menu to ensure it's visible
288+
if runtime.GOOS == "linux" {
289+
slog.Info("[LINUX] Building initial minimal menu")
290+
app.systrayInterface.ResetMenu()
291+
placeholderItem := app.systrayInterface.AddMenuItem("Loading...", "Goose is starting up")
292+
if placeholderItem != nil {
293+
placeholderItem.Disable()
294+
}
295+
app.systrayInterface.AddSeparator()
296+
quitItem := app.systrayInterface.AddMenuItem("Quit", "Quit Goose")
297+
if quitItem != nil {
298+
quitItem.Click(func() {
299+
slog.Info("Quit clicked")
300+
systray.Quit()
301+
})
302+
}
303+
}
304+
286305
// Set up click handlers first (needed for both success and error states)
287306
systray.SetOnClick(func(menu systray.IMenu) {
288307
slog.Debug("Icon clicked")
289308

290-
// Check if we can perform a forced refresh (rate limited to every 10 seconds)
309+
// Check if we're in auth error state and should retry
291310
app.mu.RLock()
292-
timeSinceLastSearch := time.Since(app.lastSearchAttempt)
311+
hasAuthError := app.authError != ""
293312
app.mu.RUnlock()
294313

295-
if timeSinceLastSearch >= minUpdateInterval {
296-
slog.Info("[CLICK] Forcing search refresh", "lastSearchAgo", timeSinceLastSearch)
314+
if hasAuthError {
315+
slog.Info("[CLICK] Auth error detected, attempting to re-authenticate")
297316
go func() {
298-
app.updatePRs(ctx)
317+
// Try to reinitialize clients which will attempt to get token via gh auth token
318+
if err := app.initClients(ctx); err != nil {
319+
slog.Warn("[CLICK] Re-authentication failed", "error", err)
320+
app.mu.Lock()
321+
app.authError = err.Error()
322+
app.mu.Unlock()
323+
} else {
324+
// Success! Clear auth error and reload user
325+
slog.Info("[CLICK] Re-authentication successful")
326+
app.mu.Lock()
327+
app.authError = ""
328+
app.mu.Unlock()
329+
330+
// Load current user
331+
loadCurrentUser(ctx, app)
332+
333+
// Update tooltip
334+
tooltip := "Goose - Loading PRs..."
335+
if app.targetUser != "" {
336+
tooltip = fmt.Sprintf("Goose - Loading PRs... (@%s)", app.targetUser)
337+
}
338+
systray.SetTooltip(tooltip)
339+
340+
// Rebuild menu to remove error state
341+
app.rebuildMenu(ctx)
342+
343+
// Start update loop if not already running
344+
if !app.menuInitialized {
345+
app.menuInitialized = true
346+
go app.updateLoop(ctx)
347+
} else {
348+
// Just do a single update to refresh data
349+
go app.updatePRs(ctx)
350+
}
351+
}
299352
}()
300353
} else {
301-
remainingTime := minUpdateInterval - timeSinceLastSearch
302-
slog.Debug("[CLICK] Rate limited", "lastSearchAgo", timeSinceLastSearch, "remaining", remainingTime)
354+
// Normal operation - check if we can perform a forced refresh
355+
app.mu.RLock()
356+
timeSinceLastSearch := time.Since(app.lastSearchAttempt)
357+
app.mu.RUnlock()
358+
359+
if timeSinceLastSearch >= minUpdateInterval {
360+
slog.Info("[CLICK] Forcing search refresh", "lastSearchAgo", timeSinceLastSearch)
361+
go func() {
362+
app.updatePRs(ctx)
363+
}()
364+
} else {
365+
remainingTime := minUpdateInterval - timeSinceLastSearch
366+
slog.Debug("[CLICK] Rate limited", "lastSearchAgo", timeSinceLastSearch, "remaining", remainingTime)
367+
}
303368
}
304369

305370
if menu != nil {
@@ -320,21 +385,23 @@ func (app *App) onReady(ctx context.Context) {
320385

321386
// Check if we have an auth error
322387
if app.authError != "" {
323-
systray.SetTitle("⚠️")
324-
systray.SetTooltip("GitHub PR Monitor - Authentication Error")
388+
systray.SetTitle("")
389+
app.setTrayIcon(IconLock)
390+
systray.SetTooltip("Goose - Authentication Error")
325391
// Create initial error menu
326392
app.rebuildMenu(ctx)
327393
// Clean old cache on startup
328394
app.cleanupOldCache()
329395
return
330396
}
331397

332-
systray.SetTitle("Loading PRs...")
398+
systray.SetTitle("")
399+
app.setTrayIcon(IconSmiling) // Start with smiling icon while loading
333400

334401
// Set tooltip based on whether we're using a custom user
335-
tooltip := "GitHub PR Monitor"
402+
tooltip := "Goose - Loading PRs..."
336403
if app.targetUser != "" {
337-
tooltip = fmt.Sprintf("GitHub PR Monitor - @%s", app.targetUser)
404+
tooltip = fmt.Sprintf("Goose - Loading PRs... (@%s)", app.targetUser)
338405
}
339406
systray.SetTooltip(tooltip)
340407

@@ -352,8 +419,9 @@ func (app *App) updateLoop(ctx context.Context) {
352419
slog.Error("PANIC in update loop", "panic", r)
353420

354421
// Set error state in UI
355-
systray.SetTitle("💥")
356-
systray.SetTooltip("GitHub PR Monitor - Critical error")
422+
systray.SetTitle("")
423+
app.setTrayIcon(IconWarning)
424+
systray.SetTooltip("Goose - Critical error")
357425

358426
// Update failure count
359427
app.mu.Lock()
@@ -406,23 +474,19 @@ func (app *App) updatePRs(ctx context.Context) {
406474
app.mu.Unlock()
407475

408476
// Progressive degradation based on failure count
409-
var title, tooltip string
477+
var tooltip string
478+
var iconType IconType
410479
switch {
411-
case failureCount == 1:
412-
title = "⚠️"
413-
tooltip = "GitHub PR Monitor - Temporary error, retrying..."
414480
case failureCount <= minorFailureThreshold:
415-
title = "⚠️"
416-
tooltip = fmt.Sprintf("GitHub PR Monitor - %d consecutive failures", failureCount)
417-
case failureCount <= majorFailureThreshold:
418-
title = "❌"
419-
tooltip = "GitHub PR Monitor - Multiple failures, check connection"
481+
iconType = IconWarning
482+
tooltip = fmt.Sprintf("Goose - %d consecutive failures", failureCount)
420483
default:
421-
title = "💀"
422-
tooltip = "GitHub PR Monitor - Service degraded, check authentication"
484+
iconType = IconWarning
485+
tooltip = "Goose - Connection failures, check network/auth"
423486
}
424487

425-
systray.SetTitle(title)
488+
systray.SetTitle("")
489+
app.setTrayIcon(iconType)
426490

427491
// Include time since last success and user info
428492
timeSinceSuccess := "never"
@@ -553,23 +617,19 @@ func (app *App) updatePRsWithWait(ctx context.Context) {
553617
app.mu.Unlock()
554618

555619
// Progressive degradation based on failure count
556-
var title, tooltip string
620+
var tooltip string
621+
var iconType IconType
557622
switch {
558-
case failureCount == 1:
559-
title = "⚠️"
560-
tooltip = "GitHub PR Monitor - Temporary error, retrying..."
561623
case failureCount <= minorFailureThreshold:
562-
title = "⚠️"
563-
tooltip = fmt.Sprintf("GitHub PR Monitor - %d consecutive failures", failureCount)
564-
case failureCount <= majorFailureThreshold:
565-
title = "❌"
566-
tooltip = "GitHub PR Monitor - Multiple failures, check connection"
624+
iconType = IconWarning
625+
tooltip = fmt.Sprintf("Goose - %d consecutive failures", failureCount)
567626
default:
568-
title = "💀"
569-
tooltip = "GitHub PR Monitor - Service degraded, check authentication"
627+
iconType = IconWarning
628+
tooltip = "Goose - Connection failures, check network/auth"
570629
}
571630

572-
systray.SetTitle(title)
631+
systray.SetTitle("")
632+
app.setTrayIcon(iconType)
573633
systray.SetTooltip(tooltip)
574634

575635
// Create or update menu to show error state
@@ -676,7 +736,7 @@ func (app *App) tryAutoOpenPR(ctx context.Context, pr PR, autoBrowserEnabled boo
676736
slog.Warn("Auto-open strict validation failed", "url", sanitizeForLog(pr.URL), "error", err)
677737
return
678738
}
679-
if err := openURL(ctx, pr.URL, pr.ActionKind); err != nil {
739+
if err := openURL(ctx, pr.URL); err != nil {
680740
slog.Error("[BROWSER] Failed to auto-open PR", "url", pr.URL, "error", err)
681741
} else {
682742
app.browserRateLimiter.RecordOpen(pr.URL)

0 commit comments

Comments
 (0)