diff --git a/cmd/systray/systray.go b/cmd/systray/systray.go index a3cd19c64..504ca5b8c 100644 --- a/cmd/systray/systray.go +++ b/cmd/systray/systray.go @@ -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.