Files
claude-statusline/internal/renderer/renderer.go
Axel Meyer 47165ce02c
All checks were successful
Release / build (push) Successful in 1m45s
Remove standalone fetcher, add setup tool with install/uninstall workflow
Drop claude-fetcher binary and cron job — the widget's built-in
BackgroundFetcher is the sole fetcher now. Add cmd/setup with cross-platform
install and uninstall (--uninstall): kills widget, removes binaries + autostart,
cleans Claude Code statusline setting, optionally removes config dir.

Also includes: browser-based login (chromedp), ICO wrapper for Windows tray
icon, and reduced icon size (64px).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:11:08 +01:00

179 lines
4.8 KiB
Go

package renderer
import (
"bytes"
"encoding/binary"
"image"
"image/color"
"image/png"
"math"
"runtime"
"github.com/fogleman/gg"
)
const iconSize = 64
// Claude orange for the starburst logo.
var claudeOrange = color.RGBA{224, 123, 83, 255}
// arcColors maps usage percentage thresholds to colors.
var arcColors = []struct {
threshold int
color color.RGBA
}{
{10, color.RGBA{76, 175, 80, 255}}, // green
{20, color.RGBA{67, 160, 71, 255}}, // darker green
{30, color.RGBA{124, 179, 66, 255}}, // light green
{40, color.RGBA{192, 202, 51, 255}}, // lime
{50, color.RGBA{253, 216, 53, 255}}, // yellow
{60, color.RGBA{255, 193, 7, 255}}, // amber
{70, color.RGBA{255, 179, 0, 255}}, // darker amber
{80, color.RGBA{255, 152, 0, 255}}, // orange
{90, color.RGBA{255, 87, 34, 255}}, // deep orange
{100, color.RGBA{244, 67, 54, 255}}, // red
}
func getArcColor(pct int) color.RGBA {
for _, ac := range arcColors {
if pct <= ac.threshold {
return ac.color
}
}
return arcColors[len(arcColors)-1].color
}
// drawStarburst draws the 8-petal Claude logo.
func drawStarburst(dc *gg.Context) {
cx := float64(iconSize) / 2
cy := float64(iconSize) / 2
petalLen := float64(iconSize) * 0.38
petalWidth := float64(iconSize) * 0.10
centerRadius := float64(iconSize) * 0.04
dc.SetColor(claudeOrange)
for i := 0; i < 8; i++ {
angle := float64(i) * (2 * math.Pi / 8)
// Tip of the petal
tipX := cx + petalLen*math.Cos(angle)
tipY := cy + petalLen*math.Sin(angle)
// Base points (perpendicular to angle)
perpAngle := angle + math.Pi/2
baseX1 := cx + petalWidth*math.Cos(perpAngle)
baseY1 := cy + petalWidth*math.Sin(perpAngle)
baseX2 := cx - petalWidth*math.Cos(perpAngle)
baseY2 := cy - petalWidth*math.Sin(perpAngle)
// Inner point (slightly behind center for petal shape)
innerX := cx - petalWidth*0.5*math.Cos(angle)
innerY := cy - petalWidth*0.5*math.Sin(angle)
dc.MoveTo(innerX, innerY)
dc.LineTo(baseX1, baseY1)
dc.LineTo(tipX, tipY)
dc.LineTo(baseX2, baseY2)
dc.ClosePath()
dc.Fill()
}
// Center dot
dc.DrawCircle(cx, cy, centerRadius)
dc.Fill()
}
// drawArc draws a circular progress arc.
func drawArc(dc *gg.Context, pct int) {
if pct <= 0 {
return
}
if pct > 100 {
pct = 100
}
cx := float64(iconSize) / 2
cy := float64(iconSize) / 2
radius := float64(iconSize)/2 - 4 // inset from edge
arcWidth := 7.0
startAngle := -math.Pi / 2 // 12 o'clock
endAngle := startAngle + (float64(pct)/100)*2*math.Pi
dc.SetColor(getArcColor(pct))
dc.SetLineWidth(arcWidth)
dc.SetLineCap(gg.LineCapButt)
dc.DrawArc(cx, cy, radius, startAngle, endAngle)
dc.Stroke()
}
// RenderIcon generates a 256x256 PNG icon with starburst and usage arc.
func RenderIcon(pct int) image.Image {
dc := gg.NewContext(iconSize, iconSize)
drawStarburst(dc)
drawArc(dc, pct)
return dc.Image()
}
// RenderIconPNG generates the icon as PNG bytes.
func RenderIconPNG(pct int) ([]byte, error) {
img := RenderIcon(pct)
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// RenderIconForTray returns icon bytes suitable for systray.SetIcon:
// ICO (PNG-compressed) on Windows, raw PNG on other platforms.
func RenderIconForTray(pct int) ([]byte, error) {
pngData, err := RenderIconPNG(pct)
if err != nil {
return nil, err
}
if runtime.GOOS != "windows" {
return pngData, nil
}
return wrapPNGInICO(pngData, iconSize, iconSize), nil
}
// wrapPNGInICO wraps raw PNG bytes in a minimal ICO container.
// Windows Vista+ supports PNG-compressed ICO entries.
func wrapPNGInICO(pngData []byte, width, height int) []byte {
const headerSize = 6
const entrySize = 16
imageOffset := headerSize + entrySize
buf := new(bytes.Buffer)
// ICONDIR header
binary.Write(buf, binary.LittleEndian, uint16(0)) // Reserved
binary.Write(buf, binary.LittleEndian, uint16(1)) // Type: 1 = icon
binary.Write(buf, binary.LittleEndian, uint16(1)) // Count: 1 image
// ICONDIRENTRY
w := byte(width)
if width >= 256 {
w = 0 // 0 means 256
}
h := byte(height)
if height >= 256 {
h = 0
}
buf.WriteByte(w) // Width
buf.WriteByte(h) // Height
buf.WriteByte(0) // ColorCount (0 = no palette)
buf.WriteByte(0) // Reserved
binary.Write(buf, binary.LittleEndian, uint16(1)) // Planes
binary.Write(buf, binary.LittleEndian, uint16(32)) // BitCount
binary.Write(buf, binary.LittleEndian, uint32(len(pngData))) // BytesInRes
binary.Write(buf, binary.LittleEndian, uint32(imageOffset)) // ImageOffset
// PNG image data
buf.Write(pngData)
return buf.Bytes()
}