cmd/systray: add account switcher

Updates #1708

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
Andrew Lytvynov 2024-12-10 13:54:31 -08:00 committed by Will Norris
parent ff5b4bae99
commit 6ae0287a57

View File

@ -49,6 +49,8 @@ type Menu struct {
more *systray.MenuItem
quit *systray.MenuItem
accountsCh chan ipn.ProfileID
eventCancel func() // cancel eventLoop
}
@ -64,15 +66,32 @@ func onReady() {
chState = make(chan ipn.State, 1)
menu := new(Menu)
menu.rebuild(fetchState(ctx))
go watchIPNBus(ctx)
}
type state struct {
status *ipnstate.Status
curProfile ipn.LoginProfile
allProfiles []ipn.LoginProfile
}
func fetchState(ctx context.Context) state {
status, err := localClient.Status(ctx)
if err != nil {
log.Print(err)
}
menu := new(Menu)
menu.rebuild(status)
go watchIPNBus(ctx)
curProfile, allProfiles, err := localClient.ProfileStatus(ctx)
if err != nil {
log.Print(err)
}
return state{
status: status,
curProfile: curProfile,
allProfiles: allProfiles,
}
}
// rebuild the systray menu based on the current Tailscale state.
@ -80,14 +99,17 @@ func onReady() {
// We currently rebuild the entire menu because it is not easy to update the existing menu.
// You cannot iterate over the items in a menu, nor can you remove some items like separators.
// So for now we rebuild the whole thing, and can optimize this later if needed.
func (menu *Menu) rebuild(status *ipnstate.Status) {
func (menu *Menu) rebuild(state state) {
menu.mu.Lock()
defer menu.mu.Unlock()
if menu.eventCancel != nil {
menu.eventCancel()
}
menu.status = status
ctx := context.Background()
ctx, menu.eventCancel = context.WithCancel(ctx)
menu.status = state.status
systray.ResetMenu()
menu.connect = systray.AddMenuItem("Connect", "")
@ -95,8 +117,46 @@ func (menu *Menu) rebuild(status *ipnstate.Status) {
menu.disconnect.Hide()
systray.AddSeparator()
if status != nil && status.Self != nil {
title := fmt.Sprintf("This Device: %s (%s)", status.Self.HostName, status.Self.TailscaleIPs[0])
account := "Account"
if state.curProfile.Name != "" {
account += fmt.Sprintf(" (%s)", state.curProfile.Name)
}
accounts := systray.AddMenuItem(account, "")
// 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.
//
// On waybar with libdbusmenu-gtk, this manifests as the following warning:
// (waybar:153009): LIBDBUSMENU-GTK-WARNING **: 18:07:11.551: Children but no menu, someone's been naughty with their 'children-display' property: 'submenu'
time.Sleep(100 * time.Millisecond)
// 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.
if profile.ID == state.curProfile.ID {
title = "* " + title
}
item := accounts.AddSubMenuItem(title, "")
go func(profile ipn.LoginProfile) {
for {
select {
case <-ctx.Done():
return
case <-item.ClickedCh:
select {
case <-ctx.Done():
return
case menu.accountsCh <- profile.ID:
}
}
}
}(profile)
}
if state.status != nil && state.status.Self != nil {
title := fmt.Sprintf("This Device: %s (%s)", state.status.Self.HostName, state.status.Self.TailscaleIPs[0])
menu.self = systray.AddMenuItem(title, "")
}
systray.AddSeparator()
@ -107,8 +167,6 @@ func (menu *Menu) rebuild(status *ipnstate.Status) {
menu.quit = systray.AddMenuItem("Quit", "Quit the app")
menu.quit.Enable()
ctx := context.Background()
ctx, menu.eventCancel = context.WithCancel(ctx)
go menu.eventLoop(ctx)
}
@ -124,11 +182,7 @@ func (menu *Menu) eventLoop(ctx context.Context) {
switch state {
case ipn.Running:
setAppIcon(loading)
status, err := localClient.Status(ctx)
if err != nil {
log.Printf("error getting tailscale status: %v", err)
}
menu.rebuild(status)
menu.rebuild(fetchState(ctx))
setAppIcon(connected)
menu.connect.SetTitle("Connected")
menu.connect.Disable()
@ -172,6 +226,11 @@ func (menu *Menu) eventLoop(ctx context.Context) {
case <-menu.more.ClickedCh:
webbrowser.Open("http://100.100.100.100/")
case id := <-menu.accountsCh:
if err := localClient.SwitchProfile(ctx, id); err != nil {
log.Printf("failed switching to profile ID %v: %v", id, err)
}
case <-menu.quit.ClickedCh:
systray.Quit()
}