mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 19:45:35 +00:00
3c66ee3f57
This adds a systray app for linux, similar to the apps for macOS and windows. There are already a number of community-developed systray apps, but most of them are either long abandoned, are built for a specific desktop environment, or simply wrap the tailscale CLI. This uses fyne.io/systray (a fork of github.com/getlantern/systray) which uses newer D-Bus specifications to render the tray icon and menu. This results in a pretty broad support for modern desktop environments. This initial commit lacks a number of features like profile switching, device listing, and exit node selection. This is really focused on the application structure, the interaction with LocalAPI, and some system integration pieces like the app icon, notifications, and the clipboard. Updates #1708 Signed-off-by: Will Norris <will@tailscale.com>
221 lines
3.7 KiB
Go
221 lines
3.7 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build cgo || !darwin
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"image/color"
|
|
"image/png"
|
|
"sync"
|
|
"time"
|
|
|
|
"fyne.io/systray"
|
|
"github.com/fogleman/gg"
|
|
)
|
|
|
|
// tsLogo represents the state of the 3x3 dot grid in the Tailscale logo.
|
|
// A 0 represents a gray dot, any other value is a white dot.
|
|
type tsLogo [9]byte
|
|
|
|
var (
|
|
// disconnected is all gray dots
|
|
disconnected = tsLogo{
|
|
0, 0, 0,
|
|
0, 0, 0,
|
|
0, 0, 0,
|
|
}
|
|
|
|
// connected is the normal Tailscale logo
|
|
connected = tsLogo{
|
|
0, 0, 0,
|
|
1, 1, 1,
|
|
0, 1, 0,
|
|
}
|
|
|
|
// loading is a special tsLogo value that is not meant to be rendered directly,
|
|
// but indicates that the loading animation should be shown.
|
|
loading = tsLogo{'l', 'o', 'a', 'd', 'i', 'n', 'g'}
|
|
|
|
// loadingIcons are shown in sequence as an animated loading icon.
|
|
loadingLogos = []tsLogo{
|
|
{
|
|
0, 1, 1,
|
|
1, 0, 1,
|
|
0, 0, 1,
|
|
},
|
|
{
|
|
0, 1, 1,
|
|
0, 0, 1,
|
|
0, 1, 0,
|
|
},
|
|
{
|
|
0, 1, 1,
|
|
0, 0, 0,
|
|
0, 0, 1,
|
|
},
|
|
{
|
|
0, 0, 1,
|
|
0, 1, 0,
|
|
0, 0, 0,
|
|
},
|
|
{
|
|
0, 1, 0,
|
|
0, 0, 0,
|
|
0, 0, 0,
|
|
},
|
|
{
|
|
0, 0, 0,
|
|
0, 0, 1,
|
|
0, 0, 0,
|
|
},
|
|
{
|
|
0, 0, 0,
|
|
0, 0, 0,
|
|
0, 0, 0,
|
|
},
|
|
{
|
|
0, 0, 1,
|
|
0, 0, 0,
|
|
0, 0, 0,
|
|
},
|
|
{
|
|
0, 0, 0,
|
|
0, 0, 0,
|
|
1, 0, 0,
|
|
},
|
|
{
|
|
0, 0, 0,
|
|
0, 0, 0,
|
|
1, 1, 0,
|
|
},
|
|
{
|
|
0, 0, 0,
|
|
1, 0, 0,
|
|
1, 1, 0,
|
|
},
|
|
{
|
|
0, 0, 0,
|
|
1, 1, 0,
|
|
0, 1, 0,
|
|
},
|
|
{
|
|
0, 0, 0,
|
|
1, 1, 0,
|
|
0, 1, 1,
|
|
},
|
|
{
|
|
0, 0, 0,
|
|
1, 1, 1,
|
|
0, 0, 1,
|
|
},
|
|
{
|
|
0, 1, 0,
|
|
0, 1, 1,
|
|
1, 0, 1,
|
|
},
|
|
}
|
|
)
|
|
|
|
var (
|
|
black = color.NRGBA{0, 0, 0, 255}
|
|
white = color.NRGBA{255, 255, 255, 255}
|
|
gray = color.NRGBA{255, 255, 255, 102}
|
|
)
|
|
|
|
// render returns a PNG image of the logo.
|
|
func (logo tsLogo) render() *bytes.Buffer {
|
|
const radius = 25
|
|
const borderUnits = 1
|
|
dim := radius * (8 + borderUnits*2)
|
|
|
|
dc := gg.NewContext(dim, dim)
|
|
dc.DrawRectangle(0, 0, float64(dim), float64(dim))
|
|
dc.SetColor(black)
|
|
dc.Fill()
|
|
|
|
for y := 0; y < 3; y++ {
|
|
for x := 0; x < 3; x++ {
|
|
px := (borderUnits + 1 + 3*x) * radius
|
|
py := (borderUnits + 1 + 3*y) * radius
|
|
col := white
|
|
if logo[y*3+x] == 0 {
|
|
col = gray
|
|
}
|
|
dc.DrawCircle(float64(px), float64(py), radius)
|
|
dc.SetColor(col)
|
|
dc.Fill()
|
|
}
|
|
}
|
|
|
|
b := bytes.NewBuffer(nil)
|
|
png.Encode(b, dc.Image())
|
|
return b
|
|
}
|
|
|
|
// setAppIcon renders logo and sets it as the systray icon.
|
|
func setAppIcon(icon tsLogo) {
|
|
if icon == loading {
|
|
startLoadingAnimation()
|
|
} else {
|
|
stopLoadingAnimation()
|
|
systray.SetIcon(icon.render().Bytes())
|
|
}
|
|
}
|
|
|
|
var (
|
|
loadingMu sync.Mutex // protects loadingCancel
|
|
|
|
// loadingCancel stops the loading animation in the systray icon.
|
|
// This is nil if the animation is not currently active.
|
|
loadingCancel func()
|
|
)
|
|
|
|
// startLoadingAnimation starts the animated loading icon in the system tray.
|
|
// The animation continues until [stopLoadingAnimation] is called.
|
|
// If the loading animation is already active, this func does nothing.
|
|
func startLoadingAnimation() {
|
|
loadingMu.Lock()
|
|
defer loadingMu.Unlock()
|
|
|
|
if loadingCancel != nil {
|
|
// loading icon already displayed
|
|
return
|
|
}
|
|
|
|
ctx := context.Background()
|
|
ctx, loadingCancel = context.WithCancel(ctx)
|
|
|
|
go func() {
|
|
t := time.NewTicker(500 * time.Millisecond)
|
|
var i int
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
systray.SetIcon(loadingLogos[i].render().Bytes())
|
|
i++
|
|
if i >= len(loadingLogos) {
|
|
i = 0
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// stopLoadingAnimation stops the animated loading icon in the system tray.
|
|
// If the loading animation is not currently active, this func does nothing.
|
|
func stopLoadingAnimation() {
|
|
loadingMu.Lock()
|
|
defer loadingMu.Unlock()
|
|
|
|
if loadingCancel != nil {
|
|
loadingCancel()
|
|
loadingCancel = nil
|
|
}
|
|
}
|