Skip to content

Commit 92fa9c2

Browse files
authored
feat(tray): add color icons for Windows (#1)
1 parent dae964e commit 92fa9c2

6 files changed

Lines changed: 129 additions & 7 deletions

File tree

assets/status/green-circle.ico

723 Bytes
Binary file not shown.

assets/status/locked.ico

619 Bytes
Binary file not shown.

assets/status/pause.ico

574 Bytes
Binary file not shown.

assets/status/red-circle.ico

702 Bytes
Binary file not shown.

internal/tray/managers.go

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ package tray
55
import (
66
"context"
77
"fmt"
8+
"os"
9+
"path/filepath"
10+
"runtime"
811
"sort"
912
"strings"
1013
"sync"
@@ -21,6 +24,63 @@ const (
2124
textDisable = "Disable"
2225
)
2326

27+
// Icon file paths relative to executable
28+
const (
29+
iconConnected = "assets/status/green-circle.ico"
30+
iconDisconnected = "assets/status/red-circle.ico"
31+
iconPaused = "assets/status/pause.ico"
32+
iconLocked = "assets/status/locked.ico"
33+
)
34+
35+
// Icon cache to avoid loading the same icon multiple times
36+
var (
37+
iconCache = make(map[string][]byte)
38+
iconCacheMu sync.RWMutex
39+
useIconsCache = runtime.GOOS == osWindows // Icons work best on Windows
40+
)
41+
42+
// loadIcon loads an icon from file and caches it
43+
func loadIcon(iconPath string) []byte {
44+
if !useIconsCache {
45+
return nil
46+
}
47+
48+
// Check cache first
49+
iconCacheMu.RLock()
50+
if data, ok := iconCache[iconPath]; ok {
51+
iconCacheMu.RUnlock()
52+
return data
53+
}
54+
iconCacheMu.RUnlock()
55+
56+
// Get executable directory
57+
exePath, err := os.Executable()
58+
if err != nil {
59+
return nil
60+
}
61+
exeDir := filepath.Dir(exePath)
62+
63+
// Build full path
64+
fullPath := filepath.Join(exeDir, iconPath)
65+
66+
// Load icon file
67+
data, err := os.ReadFile(fullPath)
68+
if err != nil {
69+
// Try relative to current directory as fallback
70+
data, err = os.ReadFile(iconPath)
71+
if err != nil {
72+
return nil
73+
}
74+
}
75+
76+
// Cache the icon
77+
iconCacheMu.Lock()
78+
iconCache[iconPath] = data
79+
iconCacheMu.Unlock()
80+
81+
return data
82+
}
83+
2484
// ServerStateManager manages server state synchronization between storage, config, and menu
2585
type ServerStateManager struct {
2686
server ServerInterface
@@ -339,8 +399,14 @@ func (m *MenuManager) UpdateUpstreamServersMenu(servers []map[string]interface{}
339399
for _, serverName := range currentServerNames {
340400
serverData := currentServerMap[serverName]
341401
m.logger.Info("Creating menu item for server", zap.String("server", serverName))
342-
status, tooltip := m.getServerStatusDisplay(serverData)
402+
status, tooltip, iconData := m.getServerStatusDisplay(serverData)
343403
serverMenuItem := m.upstreamServersMenu.AddSubMenuItem(status, tooltip)
404+
405+
// Set icon if available (Windows)
406+
if iconData != nil {
407+
serverMenuItem.SetIcon(iconData)
408+
}
409+
344410
m.serverMenuItems[serverName] = serverMenuItem
345411

346412
// Create its action submenus
@@ -356,9 +422,15 @@ func (m *MenuManager) UpdateUpstreamServersMenu(servers []map[string]interface{}
356422

357423
serverData := currentServerMap[serverName]
358424
// Server exists, update its display and ensure it's visible
359-
status, tooltip := m.getServerStatusDisplay(serverData)
425+
status, tooltip, iconData := m.getServerStatusDisplay(serverData)
360426
menuItem.SetTitle(status)
361427
menuItem.SetTooltip(tooltip)
428+
429+
// Update icon if available (Windows)
430+
if iconData != nil {
431+
menuItem.SetIcon(iconData)
432+
}
433+
362434
m.updateServerActionMenus(serverName, serverData) // Update sub-menu items too
363435
menuItem.Show()
364436
}
@@ -410,8 +482,10 @@ func (m *MenuManager) UpdateQuarantineMenu(quarantinedServers []map[string]inter
410482
if m.quarantineInfoEmpty == nil || m.quarantineInfoHelp == nil {
411483
m.quarantineInfoEmpty = m.quarantineMenu.AddSubMenuItem("(No servers quarantined)", "No servers are currently quarantined")
412484
m.quarantineInfoHelp = m.quarantineMenu.AddSubMenuItem("Click to unquarantine", "Click on a quarantined server to remove it from quarantine")
485+
m.quarantineInfoEmpty.Disable()
486+
m.quarantineInfoHelp.Disable()
413487
// Add empty separator for visual separation
414-
m.quarantineMenu.AddSubMenuItem("", "")
488+
m.quarantineMenu.AddSeparator()
415489
}
416490

417491
// --- Update Info Item Visibility ---
@@ -466,8 +540,16 @@ func (m *MenuManager) UpdateQuarantineMenu(quarantinedServers []map[string]inter
466540
continue
467541
}
468542

543+
// On Windows, use icon instead of emoji
544+
var displayText string
545+
if runtime.GOOS == osWindows {
546+
displayText = serverName
547+
} else {
548+
displayText = fmt.Sprintf("🔒 %s", serverName)
549+
}
550+
469551
quarantineMenuItem := m.quarantineMenu.AddSubMenuItem(
470-
fmt.Sprintf("🔒 %s", serverName),
552+
displayText,
471553
fmt.Sprintf("Click to unquarantine %s", serverName),
472554
)
473555

@@ -476,6 +558,14 @@ func (m *MenuManager) UpdateQuarantineMenu(quarantinedServers []map[string]inter
476558
continue
477559
}
478560

561+
// Set icon for Windows
562+
if runtime.GOOS == osWindows {
563+
iconData := loadIcon(iconLocked)
564+
if iconData != nil {
565+
quarantineMenuItem.SetIcon(iconData)
566+
}
567+
}
568+
479569
m.quarantineMenuItems[serverName] = quarantineMenuItem
480570

481571
// Set up the one-time click handler
@@ -531,8 +621,8 @@ func (m *MenuManager) ForceRefresh() {
531621
// The new Hide/Show logic should be used instead.
532622
}
533623

534-
// getServerStatusDisplay returns display text and tooltip for a server
535-
func (m *MenuManager) getServerStatusDisplay(server map[string]interface{}) (displayText, tooltip string) {
624+
// getServerStatusDisplay returns display text, tooltip, and icon data for a server
625+
func (m *MenuManager) getServerStatusDisplay(server map[string]interface{}) (displayText, tooltip string, iconData []byte) {
536626
serverName, _ := server["name"].(string)
537627
enabled, _ := server["enabled"].(bool)
538628
connected, _ := server["connected"].(bool)
@@ -541,22 +631,35 @@ func (m *MenuManager) getServerStatusDisplay(server map[string]interface{}) (dis
541631

542632
var statusIcon string
543633
var statusText string
634+
var iconPath string
544635

545636
if quarantined {
546637
statusIcon = "🔒"
547638
statusText = "quarantined"
639+
iconPath = iconLocked
548640
} else if !enabled {
549641
statusIcon = "⏸️"
550642
statusText = "disabled"
643+
iconPath = iconPaused
551644
} else if connected {
552645
statusIcon = "🟢"
553646
statusText = fmt.Sprintf("connected (%d tools)", toolCount)
647+
iconPath = iconConnected
554648
} else {
555649
statusIcon = "🔴"
556650
statusText = "disconnected"
651+
iconPath = iconDisconnected
652+
}
653+
654+
// On Windows, use icons instead of emoji for better visual appearance
655+
if runtime.GOOS == osWindows {
656+
displayText = serverName
657+
iconData = loadIcon(iconPath)
658+
} else {
659+
// On other platforms, keep using emoji
660+
displayText = fmt.Sprintf("%s %s", statusIcon, serverName)
557661
}
558662

559-
displayText = fmt.Sprintf("%s %s", statusIcon, serverName)
560663
tooltip = fmt.Sprintf("%s - %s", serverName, statusText)
561664

562665
return

scripts/run-unit-tests.ps1

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,25 @@ if (!(Get-Command go -ErrorAction SilentlyContinue)) {
1818

1919
$goVersion = go version
2020
Write-Host "Go version: $goVersion" -ForegroundColor Yellow
21+
22+
# Check if CGO is available (required for race detection)
23+
if ($Race) {
24+
$env:CGO_ENABLED = "1"
25+
$cgoTest = go env CGO_ENABLED
26+
if ($cgoTest -eq "0") {
27+
Write-Host "Warning: CGO is not available. Race detection will be disabled." -ForegroundColor Yellow
28+
Write-Host "To enable race detection, install MinGW-w64: https://www.mingw-w64.org/" -ForegroundColor Yellow
29+
$Race = $false
30+
} else {
31+
# Check if gcc is available
32+
if (!(Get-Command gcc -ErrorAction SilentlyContinue)) {
33+
Write-Host "Warning: gcc not found. Race detection will be disabled." -ForegroundColor Yellow
34+
Write-Host "To enable race detection, install MinGW-w64 and add it to PATH" -ForegroundColor Yellow
35+
$Race = $false
36+
}
37+
}
38+
}
39+
2140
Write-Host "Test timeout: $Timeout" -ForegroundColor Yellow
2241
Write-Host "Race detection: $Race" -ForegroundColor Yellow
2342
Write-Host "Coverage: $Cover" -ForegroundColor Yellow

0 commit comments

Comments
 (0)