mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-08 09:14:40 +00:00
cmd/systray: rebuild menu on pref change, assorted other fixes
- rebuild menu when prefs change outside of systray, such as setting an exit node - refactor onClick handler code - compare lowercase country name, the same as macOS and Windows (now sorts Ukraine before USA) - fix "connected / disconnected" menu items on stopped status - prevent nil pointer on "This Device" menu item Updates #1708 Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
This commit is contained in:
parent
76ca1adc64
commit
3837b6cebc
@ -7,7 +7,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -30,12 +29,15 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/stringsx"
|
||||
)
|
||||
|
||||
var (
|
||||
localClient tailscale.LocalClient
|
||||
chState chan ipn.State // tailscale state changes
|
||||
|
||||
chRebuild chan struct{} // triggers a menu rebuild
|
||||
|
||||
appIcon *os.File
|
||||
|
||||
// newMenuDelay is the amount of time to sleep after creating a new menu,
|
||||
@ -111,6 +113,7 @@ func onReady() {
|
||||
io.Copy(appIcon, connected.renderWithBorder(3))
|
||||
|
||||
chState = make(chan ipn.State, 1)
|
||||
chRebuild = make(chan struct{}, 1)
|
||||
|
||||
menu := new(Menu)
|
||||
menu.rebuild(fetchState(ctx))
|
||||
@ -146,6 +149,10 @@ func fetchState(ctx context.Context) state {
|
||||
// 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(state state) {
|
||||
if state.status == nil {
|
||||
return
|
||||
}
|
||||
|
||||
menu.mu.Lock()
|
||||
defer menu.mu.Unlock()
|
||||
|
||||
@ -181,25 +188,20 @@ func (menu *Menu) rebuild(state state) {
|
||||
item = accounts.AddSubMenuItem(title, "")
|
||||
}
|
||||
setRemoteIcon(item, profile.UserProfile.ProfilePicURL)
|
||||
go func(profile ipn.LoginProfile) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-item.ClickedCh:
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case menu.accountsCh <- profile.ID:
|
||||
}
|
||||
}
|
||||
onClick(ctx, item, func(ctx context.Context) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case menu.accountsCh <- profile.ID:
|
||||
}
|
||||
}(profile)
|
||||
})
|
||||
}
|
||||
|
||||
if state.status != nil && state.status.Self != nil {
|
||||
if state.status != nil && state.status.Self != nil && len(state.status.Self.TailscaleIPs) > 0 {
|
||||
title := fmt.Sprintf("This Device: %s (%s)", state.status.Self.HostName, state.status.Self.TailscaleIPs[0])
|
||||
menu.self = systray.AddMenuItem(title, "")
|
||||
} else {
|
||||
menu.self = systray.AddMenuItem("This Device: not connected", "")
|
||||
menu.self.Disable()
|
||||
}
|
||||
systray.AddSeparator()
|
||||
|
||||
@ -266,6 +268,8 @@ func (menu *Menu) eventLoop(ctx context.Context) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-chRebuild:
|
||||
menu.rebuild(fetchState(ctx))
|
||||
case state := <-chState:
|
||||
switch state {
|
||||
case ipn.Running:
|
||||
@ -277,10 +281,11 @@ func (menu *Menu) eventLoop(ctx context.Context) {
|
||||
menu.disconnect.Show()
|
||||
menu.disconnect.Enable()
|
||||
case ipn.NoState, ipn.Stopped:
|
||||
setAppIcon(disconnected)
|
||||
menu.rebuild(fetchState(ctx))
|
||||
menu.connect.SetTitle("Connect")
|
||||
menu.connect.Enable()
|
||||
menu.disconnect.Hide()
|
||||
setAppIcon(disconnected)
|
||||
case ipn.Starting:
|
||||
setAppIcon(loading)
|
||||
}
|
||||
@ -337,7 +342,6 @@ func (menu *Menu) eventLoop(ctx context.Context) {
|
||||
log.Printf("failed setting exit node: %v", err)
|
||||
}
|
||||
}
|
||||
menu.rebuild(fetchState(ctx))
|
||||
|
||||
case <-menu.quit.ClickedCh:
|
||||
systray.Quit()
|
||||
@ -345,6 +349,20 @@ func (menu *Menu) eventLoop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// onClick registers a click handler for a menu item.
|
||||
func onClick(ctx context.Context, item *systray.MenuItem, fn func(ctx context.Context)) {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-item.ClickedCh:
|
||||
fn(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// watchIPNBus subscribes to the tailscale event bus and sends state updates to chState.
|
||||
// This method does not return.
|
||||
func watchIPNBus(ctx context.Context) {
|
||||
@ -383,6 +401,9 @@ func watchIPNBusInner(ctx context.Context) error {
|
||||
chState <- *n.State
|
||||
log.Printf("new state: %v", n.State)
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
chRebuild <- struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -425,25 +446,17 @@ func (menu *Menu) rebuildExitNodeMenu(ctx context.Context) {
|
||||
time.Sleep(newMenuDelay)
|
||||
|
||||
// register a click handler for a menu item to set nodeID as the exit node.
|
||||
onClick := func(item *systray.MenuItem, nodeID tailcfg.StableNodeID) {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-item.ClickedCh:
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case menu.exitNodeCh <- nodeID:
|
||||
}
|
||||
}
|
||||
setExitNodeOnClick := func(item *systray.MenuItem, nodeID tailcfg.StableNodeID) {
|
||||
onClick(ctx, item, func(ctx context.Context) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case menu.exitNodeCh <- nodeID:
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
noExitNodeMenu := menu.exitNodes.AddSubMenuItemCheckbox("None", "", status.ExitNodeStatus == nil)
|
||||
onClick(noExitNodeMenu, "")
|
||||
setExitNodeOnClick(noExitNodeMenu, "")
|
||||
|
||||
// Show recommended exit node if available.
|
||||
if status.Self.CapMap.Contains(tailcfg.NodeAttrSuggestExitNodeUI) {
|
||||
@ -458,7 +471,7 @@ func (menu *Menu) rebuildExitNodeMenu(ctx context.Context) {
|
||||
}
|
||||
menu.exitNodes.AddSeparator()
|
||||
rm := menu.exitNodes.AddSubMenuItemCheckbox(title, "", false)
|
||||
onClick(rm, sugg.ID)
|
||||
setExitNodeOnClick(rm, sugg.ID)
|
||||
if status.ExitNodeStatus != nil && sugg.ID == status.ExitNodeStatus.ID {
|
||||
rm.Check()
|
||||
}
|
||||
@ -490,7 +503,7 @@ func (menu *Menu) rebuildExitNodeMenu(ctx context.Context) {
|
||||
if status.ExitNodeStatus != nil && ps.ID == status.ExitNodeStatus.ID {
|
||||
sm.Check()
|
||||
}
|
||||
onClick(sm, ps.ID)
|
||||
setExitNodeOnClick(sm, ps.ID)
|
||||
}
|
||||
}
|
||||
|
||||
@ -510,7 +523,7 @@ func (menu *Menu) rebuildExitNodeMenu(ctx context.Context) {
|
||||
|
||||
// single-city country, no submenu
|
||||
if len(country.cities) == 1 || hideMullvadCities {
|
||||
onClick(countryMenu, country.best.ID)
|
||||
setExitNodeOnClick(countryMenu, country.best.ID)
|
||||
if status.ExitNodeStatus != nil {
|
||||
for _, city := range country.cities {
|
||||
for _, ps := range city.peers {
|
||||
@ -527,12 +540,12 @@ func (menu *Menu) rebuildExitNodeMenu(ctx context.Context) {
|
||||
// multi-city country, build submenu with "best available" option and cities.
|
||||
time.Sleep(newMenuDelay)
|
||||
bm := countryMenu.AddSubMenuItemCheckbox("Best Available", "", false)
|
||||
onClick(bm, country.best.ID)
|
||||
setExitNodeOnClick(bm, country.best.ID)
|
||||
countryMenu.AddSeparator()
|
||||
|
||||
for _, city := range country.sortedCities() {
|
||||
cityMenu := countryMenu.AddSubMenuItemCheckbox(city.name, "", false)
|
||||
onClick(cityMenu, city.best.ID)
|
||||
setExitNodeOnClick(cityMenu, city.best.ID)
|
||||
if status.ExitNodeStatus != nil {
|
||||
for _, ps := range city.peers {
|
||||
if status.ExitNodeStatus.ID == ps.ID {
|
||||
@ -558,7 +571,7 @@ type mullvadPeers struct {
|
||||
func (mp mullvadPeers) sortedCountries() []*mvCountry {
|
||||
countries := slices.Collect(maps.Values(mp.countries))
|
||||
slices.SortFunc(countries, func(a, b *mvCountry) int {
|
||||
return cmp.Compare(a.name, b.name)
|
||||
return stringsx.CompareFold(a.name, b.name)
|
||||
})
|
||||
return countries
|
||||
}
|
||||
@ -574,7 +587,7 @@ type mvCountry struct {
|
||||
func (mc *mvCountry) sortedCities() []*mvCity {
|
||||
cities := slices.Collect(maps.Values(mc.cities))
|
||||
slices.SortFunc(cities, func(a, b *mvCity) int {
|
||||
return cmp.Compare(a.name, b.name)
|
||||
return stringsx.CompareFold(a.name, b.name)
|
||||
})
|
||||
return cities
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user