Nightscout Tray
A cross-platform system tray application for people using CGM (continuous glucose monitoring). Built with Wails3, it runs natively on Windows, macOS, and Linux, showing the current glucose value directly in the system tray with color-coded urgency levels. Configurable polling, unit conversion, and delta trend arrows give a quick at-a-glance status without opening a browser.




nightscout-tray is a Wails3 app (Go backend, native webview frontend) that turns a continuous glucose monitor's Nightscout feed into a system tray icon. Every poll, it rasterizes a fresh 32×32 PNG on the fly with the gg 2D graphics library: a rounded rect colored by urgency status, the current glucose value as anchored text, and a direction arrow for the trend — recomputed each tick rather than swapped between a fixed set of pre-rendered icons. A separate prediction package implements an OpenAPS-style oref algorithm against recent glucose history to estimate where the value is heading, feeding configurable threshold notifications. The same binary builds natively for Windows, macOS, and Linux through Wails3's cross-platform webview bridge, with mobile builds (Android/iOS) cross-compiled through the project's own CI pipeline.
Architecture
graph TD
Nightscout[("Nightscout API")] -->|poll| Service["internal/app/service.go"]
Service --> Predictor["oref-style predictor
(internal/prediction)"]
Service --> IconGen["IconGenerator
(gg rasterizer)"]
Predictor -->|trend + forecast| Notifications["Threshold notifications"]
IconGen -->|fresh PNG per tick| Tray["System tray icon"]
Service --> TrayUnder the hood
The tray icon isn't a static asset — it's rasterized fresh on every poll: status color, the glucose value as text, and a trend arrow drawn onto a 32×32 canvas.
func (g *IconGenerator) GenerateIcon(text string, direction string, status *models.GlucoseStatus) []byte {
// Wails v3 systray uses PNG on all platforms
var width, height float64 = 32, 32
radius := width / 4
dc := gg.NewContext(int(width), int(height))
dc.SetRGBA(0, 0, 0, 0)
dc.Clear()
bgHex := getStatusColor(status)
r, ge, b := parseHexColor(bgHex)
dc.SetRGB255(int(r), int(ge), int(b))
dc.DrawRoundedRectangle(0, 0, width, height, radius)
dc.Fill()
brightness := (int(r)*299 + int(ge)*587 + int(b)*114) / 1000
if brightness > 128 {
dc.SetColor(color.Black)
} else {
dc.SetColor(color.White)
}
fontSize := height * 0.5
if err := loadFont(dc, fontSize); err == nil {
dc.DrawStringAnchored(text, width/2, height/2-2, 0.5, 0.5)
}
if direction != "" {
drawArrow(dc, width/2, height-5, height*0.3, direction)
}
var buf bytes.Buffer
png.Encode(&buf, dc.Image())
return buf.Bytes()
}