@@ -5,6 +5,9 @@ package tray
55import (
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
2585type 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
0 commit comments