From 86f273d930df52440641ef2397f0f7ebca648d7c Mon Sep 17 00:00:00 2001 From: Will Norris Date: Mon, 23 Dec 2024 13:38:09 -0800 Subject: [PATCH] cmd/systray: set app icon and title consistently Refactor code to set app icon and title as part of rebuild, rather than separately in eventLoop. This fixes several cases where they weren't getting updated properly. This change also makes use of the new exit node icons. Updates #1708 Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris --- cmd/systray/logo.go | 20 ++++++++----- cmd/systray/systray.go | 67 ++++++++++++++++++++++++------------------ 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/cmd/systray/logo.go b/cmd/systray/logo.go index 13fd4c564..de60bcdbd 100644 --- a/cmd/systray/logo.go +++ b/cmd/systray/logo.go @@ -136,6 +136,7 @@ type tsLogo struct { 1, 1, 1, 0, 1, 0, }, + // draw an arrow mask in the bottom right corner with a reasonably thick line width. dotMask: func(dc *gg.Context, borderUnits int, radius int) *image.Alpha { bu, r := float64(borderUnits), float64(radius) @@ -144,13 +145,14 @@ type tsLogo struct { x2 := x1 + (r * 5) mc := gg.NewContext(dc.Width(), dc.Height()) - mc.DrawLine(x1, y, x2, y) - mc.DrawLine(x2-(1.5*r), y-(1.5*r), x2, y) - mc.DrawLine(x2-(1.5*r), y+(1.5*r), x2, y) + mc.DrawLine(x1, y, x2, y) // arrow center line + mc.DrawLine(x2-(1.5*r), y-(1.5*r), x2, y) // top of arrow tip + mc.DrawLine(x2-(1.5*r), y+(1.5*r), x2, y) // bottom of arrow tip mc.SetLineWidth(r * 3) mc.Stroke() return mc.AsMask() }, + // draw an arrow in the bottom right corner over the masked area. overlay: func(dc *gg.Context, borderUnits int, radius int) { bu, r := float64(borderUnits), float64(radius) @@ -158,9 +160,9 @@ type tsLogo struct { y := r * (bu + 7) x2 := x1 + (r * 5) - dc.DrawLine(x1, y, x2, y) - dc.DrawLine(x2-(1.5*r), y-(1.5*r), x2, y) - dc.DrawLine(x2-(1.5*r), y+(1.5*r), x2, y) + dc.DrawLine(x1, y, x2, y) // arrow center line + dc.DrawLine(x2-(1.5*r), y-(1.5*r), x2, y) // top of arrow tip + dc.DrawLine(x2-(1.5*r), y+(1.5*r), x2, y) // bottom of arrow tip dc.SetColor(fg) dc.SetLineWidth(r) dc.Stroke() @@ -174,6 +176,7 @@ type tsLogo struct { 1, 1, 1, 0, 1, 0, }, + // Draw a square that hides the four dots in the bottom right corner, dotMask: func(dc *gg.Context, borderUnits int, radius int) *image.Alpha { bu, r := float64(borderUnits), float64(radius) x := r * (bu + 3) @@ -183,13 +186,14 @@ type tsLogo struct { mc.Fill() return mc.AsMask() }, + // draw a red "x" over the bottom right corner. overlay: func(dc *gg.Context, borderUnits int, radius int) { bu, r := float64(borderUnits), float64(radius) x1 := r * (bu + 4) x2 := x1 + (r * 3.5) - dc.DrawLine(x1, x1, x2, x2) - dc.DrawLine(x1, x2, x2, x1) + dc.DrawLine(x1, x1, x2, x2) // top-left to bottom-right stroke + dc.DrawLine(x1, x2, x2, x1) // bottom-left to top-right stroke dc.SetColor(red) dc.SetLineWidth(r) dc.Stroke() diff --git a/cmd/systray/systray.go b/cmd/systray/systray.go index 8a4ee08fd..0102b28a6 100644 --- a/cmd/systray/systray.go +++ b/cmd/systray/systray.go @@ -34,11 +34,8 @@ var ( localClient tailscale.LocalClient - chState chan ipn.State // tailscale state changes - - chRebuild chan struct{} // triggers a menu rebuild - - appIcon *os.File + rebuildCh chan struct{} // triggers a menu rebuild + appIcon *os.File // newMenuDelay is the amount of time to sleep after creating a new menu, // but before adding items to it. This works around a bug in some dbus implementations. @@ -112,8 +109,7 @@ func onReady() { appIcon, _ = os.CreateTemp("", "tailscale-systray.png") io.Copy(appIcon, connected.renderWithBorder(3)) - chState = make(chan ipn.State, 1) - chRebuild = make(chan struct{}, 1) + rebuildCh = make(chan struct{}, 1) menu := new(Menu) menu.rebuild(fetchState(ctx)) @@ -170,6 +166,34 @@ func (menu *Menu) rebuild(state state) { menu.disconnect.Hide() systray.AddSeparator() + // Set systray menu icon and title. + // Also adjust connect/disconnect menu items if needed. + switch menu.status.BackendState { + case ipn.Running.String(): + if state.status.ExitNodeStatus != nil && !state.status.ExitNodeStatus.ID.IsZero() { + if state.status.ExitNodeStatus.Online { + systray.SetTitle("Using exit node") + setAppIcon(exitNodeOnline) + } else { + systray.SetTitle("Exit node offline") + setAppIcon(exitNodeOffline) + } + } else { + systray.SetTitle(fmt.Sprintf("Connected to %s", state.status.CurrentTailnet.Name)) + setAppIcon(connected) + } + menu.connect.SetTitle("Connected") + menu.connect.Disable() + menu.disconnect.Show() + menu.disconnect.Enable() + case ipn.Starting.String(): + systray.SetTitle("Connecting") + setAppIcon(loading) + default: + systray.SetTitle("Disconnected") + setAppIcon(disconnected) + } + account := "Account" if pt := profileTitle(state.curProfile); pt != "" { account = pt @@ -268,27 +292,8 @@ func (menu *Menu) eventLoop(ctx context.Context) { select { case <-ctx.Done(): return - case <-chRebuild: + case <-rebuildCh: menu.rebuild(fetchState(ctx)) - case state := <-chState: - switch state { - case ipn.Running: - setAppIcon(loading) - menu.rebuild(fetchState(ctx)) - setAppIcon(connected) - menu.connect.SetTitle("Connected") - menu.connect.Disable() - 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() - case ipn.Starting: - setAppIcon(loading) - } case <-menu.connect.ClickedCh: _, err := localClient.EditPrefs(ctx, &ipn.MaskedPrefs{ Prefs: ipn.Prefs{ @@ -397,12 +402,16 @@ func watchIPNBusInner(ctx context.Context) error { if err != nil { return fmt.Errorf("ipnbus error: %w", err) } + var rebuild bool if n.State != nil { - chState <- *n.State log.Printf("new state: %v", n.State) + rebuild = true } if n.Prefs != nil { - chRebuild <- struct{}{} + rebuild = true + } + if rebuild { + rebuildCh <- struct{}{} } } }