@ -12,6 +12,7 @@ import (
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
@ -118,10 +119,11 @@ func (menu *Menu) rebuild(state state) {
systray . AddSeparator ( )
account := "Account"
if state. curProfile . Name != "" {
account += fmt . Sprintf ( " (%s)" , state . curProfile . Name )
if pt := profileTitle ( state . curProfile ) ; pt != "" {
account = pt
}
accounts := systray . AddMenuItem ( account , "" )
setRemoteIcon ( accounts , state . curProfile . UserProfile . ProfilePicURL )
// The dbus message about this menu item must propagate to the receiving
// end before we attach any submenu items. Otherwise the receiver may not
// yet record the parent menu item and error out.
@ -132,13 +134,14 @@ func (menu *Menu) rebuild(state state) {
// Aggregate all clicks into a shared channel.
menu . accountsCh = make ( chan ipn . ProfileID )
for _ , profile := range state . allProfiles {
title := fmt . Sprintf ( "%s (%s)" , profile . Name , profile . NetworkProfile . DomainName )
// Note: we could use AddSubMenuItemCheckbox instead of this formatting
// hack, but checkboxes don't work across all desktops unfortunately.
title := profileTitle ( profile )
var item * systray . MenuItem
if profile . ID == state . curProfile . ID {
title = "* " + title
item = accounts . AddSubMenuItemCheckbox ( title , "" , true )
} else {
item = accounts . AddSubMenuItem ( title , "" )
}
item := accounts . AddSubMenuItem ( title , "" )
setRemoteIcon ( item , profile . UserProfile . ProfilePicURL )
go func ( profile ipn . LoginProfile ) {
for {
select {
@ -170,6 +173,44 @@ func (menu *Menu) rebuild(state state) {
go menu . eventLoop ( ctx )
}
// profileTitle returns the title string for a profile menu item.
func profileTitle ( profile ipn . LoginProfile ) string {
title := profile . Name
if profile . NetworkProfile . DomainName != "" {
title += "\n" + profile . NetworkProfile . DomainName
}
return title
}
var (
cacheMu sync . Mutex
httpCache = map [ string ] [ ] byte { } // URL => response body
)
// setRemoteIcon sets the icon for menu to the specified remote image.
// Remote images are fetched as needed and cached.
func setRemoteIcon ( menu * systray . MenuItem , urlStr string ) {
if menu == nil || urlStr == "" {
return
}
cacheMu . Lock ( )
b , ok := httpCache [ urlStr ]
if ! ok {
resp , err := http . Get ( urlStr )
if err == nil && resp . StatusCode == http . StatusOK {
b , _ = io . ReadAll ( resp . Body )
httpCache [ urlStr ] = b
resp . Body . Close ( )
}
}
cacheMu . Unlock ( )
if len ( b ) > 0 {
menu . SetIcon ( b )
}
}
// eventLoop is the main event loop for handling click events on menu items
// and responding to Tailscale state changes.
// This method does not return until ctx.Done is closed.