Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 13 additions & 26 deletions cmd/goose/icons.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"log/slog"
)

// Icon variables are defined in platform-specific files:
// - icons_windows.go: uses .ico files.
// - icons_unix.go: uses .png files.
// Icon implementations are in platform-specific files:
// - icons_darwin.go: macOS (static PNG icons, counts shown in title)
// - icons_badge.go: Linux/BSD/Windows (dynamic circle badges with counts)

// IconType represents different icon states.
type IconType int
Expand All @@ -21,33 +21,20 @@ const (
IconLock // Authentication error
)

// getIcon returns the icon bytes for the given type.
func getIcon(iconType IconType) []byte {
switch iconType {
case IconGoose, IconBoth:
// For both, we'll use the goose icon as primary
return iconGoose
case IconPopper:
return iconPopper
case IconCockroach:
return iconCockroach
case IconWarning:
return iconWarning
case IconLock:
return iconLock
default:
return iconSmiling
}
}
// getIcon returns icon bytes for the given type and counts.
// Implementation is platform-specific:
// - macOS: returns static icons (counts displayed in title bar)
// - Linux/Windows: generates dynamic badges with embedded counts.
// Implemented in icons_darwin.go and icons_badge.go.

// setTrayIcon updates the system tray icon based on PR counts.
func (app *App) setTrayIcon(iconType IconType) {
iconBytes := getIcon(iconType)
// setTrayIcon updates the system tray icon.
func (app *App) setTrayIcon(iconType IconType, counts PRCounts) {
iconBytes := getIcon(iconType, counts)
if len(iconBytes) == 0 {
slog.Warn("Icon bytes are empty, skipping icon update", "type", iconType)
slog.Warn("icon bytes empty, skipping update", "type", iconType)
return
}

app.systrayInterface.SetIcon(iconBytes)
slog.Debug("[TRAY] Setting icon", "type", iconType)
slog.Debug("tray icon updated", "type", iconType, "incoming", counts.IncomingBlocked, "outgoing", counts.OutgoingBlocked)
}
71 changes: 71 additions & 0 deletions cmd/goose/icons_badge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//go:build (linux || freebsd || openbsd || netbsd || dragonfly || solaris || illumos || aix || windows) && !darwin

package main

import (
_ "embed"
"log/slog"
"sync"

"github.com/codeGROOVE-dev/goose/pkg/icon"
)

// Linux, BSD, and Windows use dynamic circle badges since they don't support title text.

//go:embed icons/smiling-face.png
var iconSmilingSource []byte

//go:embed icons/warning.png
var iconWarning []byte

//go:embed icons/lock.png
var iconLock []byte

var (
cache = icon.NewCache()

smiling []byte
smilingOnce sync.Once
)

func getIcon(iconType IconType, counts PRCounts) []byte {
// Static icons for error states
if iconType == IconWarning {
return iconWarning
}
if iconType == IconLock {
return iconLock
}

incoming := counts.IncomingBlocked
outgoing := counts.OutgoingBlocked

// Happy face when nothing is blocked
if incoming == 0 && outgoing == 0 {
smilingOnce.Do(func() {
scaled, err := icon.Scale(iconSmilingSource)
if err != nil {
slog.Error("failed to scale happy face icon", "error", err)
smiling = iconSmilingSource
return
}
smiling = scaled
})
return smiling
}

// Check cache
if cached, ok := cache.Get(incoming, outgoing); ok {
return cached
}

// Generate badge
badge, err := icon.Badge(incoming, outgoing)
if err != nil {
slog.Error("failed to generate badge", "error", err, "incoming", incoming, "outgoing", outgoing)
return smiling
}

cache.Put(incoming, outgoing, badge)
return badge
}
42 changes: 42 additions & 0 deletions cmd/goose/icons_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//go:build darwin

package main

import _ "embed"

// macOS displays counts in the title bar, so icons remain static.

//go:embed icons/goose.png
var iconGoose []byte

//go:embed icons/popper.png
var iconPopper []byte

//go:embed icons/smiling-face.png
var iconSmiling []byte

//go:embed icons/lock.png
var iconLock []byte

//go:embed icons/warning.png
var iconWarning []byte

//go:embed icons/cockroach.png
var iconCockroach []byte

func getIcon(iconType IconType, counts PRCounts) []byte {
switch iconType {
case IconGoose, IconBoth:
return iconGoose
case IconPopper:
return iconPopper
case IconCockroach:
return iconCockroach
case IconWarning:
return iconWarning
case IconLock:
return iconLock
default:
return iconSmiling
}
}
27 changes: 0 additions & 27 deletions cmd/goose/icons_unix.go

This file was deleted.

27 changes: 0 additions & 27 deletions cmd/goose/icons_windows.go

This file was deleted.

10 changes: 5 additions & 5 deletions cmd/goose/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ func (app *App) onReady(ctx context.Context) {
// Check if we have an auth error
if app.authError != "" {
systray.SetTitle("")
app.setTrayIcon(IconLock)
app.setTrayIcon(IconLock, PRCounts{})
systray.SetTooltip("Goose - Authentication Error")
// Create initial error menu
app.rebuildMenu(ctx)
Expand All @@ -476,7 +476,7 @@ func (app *App) onReady(ctx context.Context) {
}

systray.SetTitle("")
app.setTrayIcon(IconSmiling) // Start with smiling icon while loading
app.setTrayIcon(IconSmiling, PRCounts{}) // Start with smiling icon while loading

// Set tooltip based on whether we're using a custom user
tooltip := "Goose - Loading PRs..."
Expand All @@ -500,7 +500,7 @@ func (app *App) updateLoop(ctx context.Context) {

// Set error state in UI
systray.SetTitle("")
app.setTrayIcon(IconWarning)
app.setTrayIcon(IconWarning, PRCounts{})
systray.SetTooltip("Goose - Critical error")

// Update failure count
Expand Down Expand Up @@ -588,7 +588,7 @@ func (app *App) updatePRs(ctx context.Context) {
}

systray.SetTitle("")
app.setTrayIcon(iconType)
app.setTrayIcon(iconType, PRCounts{})

// Include time since last success and user info
timeSinceSuccess := "never"
Expand Down Expand Up @@ -769,7 +769,7 @@ func (app *App) updatePRsWithWait(ctx context.Context) {
}

systray.SetTitle("")
app.setTrayIcon(iconType)
app.setTrayIcon(iconType, PRCounts{})
systray.SetTooltip(tooltip)

// Create or update menu to show error state
Expand Down
2 changes: 1 addition & 1 deletion cmd/goose/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func (app *App) setTrayTitle() {
"outgoing_total", counts.OutgoingTotal,
"outgoing_blocked", counts.OutgoingBlocked)
app.systrayInterface.SetTitle(title)
app.setTrayIcon(iconType)
app.setTrayIcon(iconType, counts)
}

// addPRSection adds a section of PRs to the menu.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/gen2brain/beeep v0.11.1
github.com/godbus/dbus/v5 v5.1.0
github.com/google/go-github/v57 v57.0.0
golang.org/x/image v0.33.0
golang.org/x/oauth2 v0.33.0
)

Expand All @@ -27,4 +28,5 @@ require (
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG0
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NXpcvv2E4I7+ii6L5hZO2S6C3kw=
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
Expand All @@ -58,6 +60,8 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
Loading