186 lines
4.6 KiB
Go
186 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
|
|
"github.com/getlantern/systray"
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
)
|
|
|
|
// Precomputed tray icons
|
|
var (
|
|
iconGray = makeVpnIcon(130, 130, 130)
|
|
iconGreen = makeVpnIcon(76, 175, 80)
|
|
iconYellow = makeVpnIcon(253, 216, 53)
|
|
iconRed = makeVpnIcon(229, 57, 53)
|
|
)
|
|
|
|
func makeVpnIcon(r, g, b uint8) []byte {
|
|
const size = 16
|
|
img := image.NewNRGBA(image.Rect(0, 0, size, size))
|
|
bg := color.NRGBA{R: r, G: g, B: b, A: 255}
|
|
|
|
type pt struct{ x, y int }
|
|
skip := make(map[pt]bool)
|
|
|
|
// Make a horizontal pill-shaped rectangle
|
|
// Skip rows 0, 1, 14, 15
|
|
for x := 0; x < size; x++ {
|
|
skip[pt{x, 0}] = true; skip[pt{x, 1}] = true
|
|
skip[pt{x, 14}] = true; skip[pt{x, 15}] = true
|
|
}
|
|
// Round corners of the remaining rectangle (rows 2-13)
|
|
// y=2 and y=13 skip x=0,1, 14,15
|
|
skip[pt{0, 2}] = true; skip[pt{1, 2}] = true; skip[pt{14, 2}] = true; skip[pt{15, 2}] = true
|
|
skip[pt{0, 13}] = true; skip[pt{1, 13}] = true; skip[pt{14, 13}] = true; skip[pt{15, 13}] = true
|
|
// y=3 and y=12 skip x=0, 15
|
|
skip[pt{0, 3}] = true; skip[pt{15, 3}] = true
|
|
skip[pt{0, 12}] = true; skip[pt{15, 12}] = true
|
|
|
|
// Draw rounded background
|
|
for y := 0; y < size; y++ {
|
|
for x := 0; x < size; x++ {
|
|
if !skip[pt{x, y}] {
|
|
img.SetNRGBA(x, y, bg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw "VPN" text in white using pixel font
|
|
white := color.NRGBA{255, 255, 255, 255}
|
|
// V (x: 2-4, y: 5-9)
|
|
img.SetNRGBA(2, 5, white); img.SetNRGBA(4, 5, white)
|
|
img.SetNRGBA(2, 6, white); img.SetNRGBA(4, 6, white)
|
|
img.SetNRGBA(2, 7, white); img.SetNRGBA(4, 7, white)
|
|
img.SetNRGBA(3, 8, white); img.SetNRGBA(3, 9, white)
|
|
|
|
// P (x: 6-8, y: 5-9)
|
|
for y := 5; y <= 9; y++ { img.SetNRGBA(6, y, white) }
|
|
img.SetNRGBA(7, 5, white); img.SetNRGBA(8, 5, white)
|
|
img.SetNRGBA(8, 6, white)
|
|
img.SetNRGBA(7, 7, white); img.SetNRGBA(8, 7, white)
|
|
|
|
// N (x: 10-13, y: 5-9)
|
|
for y := 5; y <= 9; y++ { img.SetNRGBA(10, y, white); img.SetNRGBA(13, y, white) }
|
|
img.SetNRGBA(11, 6, white)
|
|
img.SetNRGBA(11, 7, white); img.SetNRGBA(12, 7, white)
|
|
img.SetNRGBA(12, 8, white)
|
|
|
|
var buf bytes.Buffer
|
|
_ = png.Encode(&buf, img)
|
|
|
|
// Wrap PNG in ICO format
|
|
pngData := buf.Bytes()
|
|
var ico bytes.Buffer
|
|
ico.Write([]byte{0, 0, 1, 0, 1, 0}) // ICONDIR
|
|
ico.Write([]byte{16, 16, 0, 0, 1, 0, 32, 0}) // ICONDIRENTRY (16x16, 32bpp)
|
|
sizeBytes := len(pngData)
|
|
ico.WriteByte(byte(sizeBytes & 0xFF))
|
|
ico.WriteByte(byte((sizeBytes >> 8) & 0xFF))
|
|
ico.WriteByte(byte((sizeBytes >> 16) & 0xFF))
|
|
ico.WriteByte(byte((sizeBytes >> 24) & 0xFF))
|
|
ico.Write([]byte{22, 0, 0, 0}) // Image offset
|
|
ico.Write(pngData)
|
|
|
|
return ico.Bytes()
|
|
}
|
|
|
|
func (a *App) setupTray() {
|
|
go systray.Run(a.onTrayReady, func() {})
|
|
}
|
|
|
|
func (a *App) onTrayReady() {
|
|
systray.SetIcon(iconGray)
|
|
systray.SetTooltip("kettuRay — Disconnected")
|
|
|
|
mToggle := systray.AddMenuItem("Connect", "Toggle VPN")
|
|
systray.AddSeparator()
|
|
mShow := systray.AddMenuItem("Show", "Show window")
|
|
systray.AddSeparator()
|
|
mQuit := systray.AddMenuItem("Exit", "Exit kettuRay")
|
|
|
|
// Store menu item reference for text updates
|
|
a.mu.Lock()
|
|
a.trayToggleItem = mToggle
|
|
a.mu.Unlock()
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-mToggle.ClickedCh:
|
|
a.toggleVPN()
|
|
case <-mShow.ClickedCh:
|
|
runtime.WindowShow(a.ctx)
|
|
case <-mQuit.ClickedCh:
|
|
runtime.Quit(a.ctx)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *App) updateTrayState(state string) {
|
|
switch state {
|
|
case "Connected":
|
|
systray.SetIcon(iconGreen)
|
|
systray.SetTooltip("kettuRay — Connected")
|
|
a.mu.Lock()
|
|
if a.trayToggleItem != nil {
|
|
a.trayToggleItem.SetTitle("Disconnect")
|
|
}
|
|
a.mu.Unlock()
|
|
case "Connecting":
|
|
systray.SetIcon(iconYellow)
|
|
systray.SetTooltip("kettuRay — Connecting...")
|
|
a.mu.Lock()
|
|
if a.trayToggleItem != nil {
|
|
a.trayToggleItem.SetTitle("Connecting...")
|
|
}
|
|
a.mu.Unlock()
|
|
case "Disconnecting":
|
|
systray.SetIcon(iconYellow)
|
|
systray.SetTooltip("kettuRay — Disconnecting...")
|
|
case "Error":
|
|
systray.SetIcon(iconRed)
|
|
systray.SetTooltip("kettuRay — Error")
|
|
a.mu.Lock()
|
|
if a.trayToggleItem != nil {
|
|
a.trayToggleItem.SetTitle("Connect")
|
|
}
|
|
a.mu.Unlock()
|
|
default: // Disconnected
|
|
systray.SetIcon(iconGray)
|
|
systray.SetTooltip("kettuRay — Disconnected")
|
|
a.mu.Lock()
|
|
if a.trayToggleItem != nil {
|
|
a.trayToggleItem.SetTitle("Connect")
|
|
}
|
|
a.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func (a *App) toggleVPN() {
|
|
a.mu.Lock()
|
|
state := a.manager.State
|
|
id := a.selectedConfigID
|
|
a.mu.Unlock()
|
|
|
|
switch state {
|
|
case 2: // Connected
|
|
go a.manager.Disconnect()
|
|
case 1: // Connecting
|
|
go a.manager.Disconnect()
|
|
default:
|
|
if id != "" {
|
|
go a.connectByID(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// HideWindow hides the app window (minimize to tray).
|
|
func (a *App) HideWindow() {
|
|
runtime.WindowHide(a.ctx)
|
|
}
|