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{}{} } } }