Rewrite in Go: static binaries, zero runtime dependencies
Some checks failed
Release / build (push) Failing after 21s

Replace Node.js + Python codebase with three Go binaries:
- claude-statusline: CLI status bar for Claude Code
- claude-fetcher: standalone cron job for API usage
- claude-widget: system tray icon (fyne-io/systray + fogleman/gg)

All CGO-free for trivial cross-compilation. Add nfpm .deb packaging
with autostart and cron. CI pipeline produces Linux + Windows binaries,
.deb, .tar.gz, and .zip release assets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-02-26 15:27:10 +00:00
parent 59afabd65a
commit 7f17a40b7c
33 changed files with 1275 additions and 1512 deletions

120
cmd/statusline/main.go Normal file
View File

@@ -0,0 +1,120 @@
package main
import (
"encoding/json"
"fmt"
"math"
"os"
"strconv"
"strings"
"time"
"git.davoryn.de/calic/claude-statusline/internal/fetcher"
)
const (
filledBlock = "\u2593"
lightBlock = "\u2591"
)
// stdinData is the JSON structure from Claude Code's stdin.
type stdinData struct {
ContextWindow struct {
UsedPercentage float64 `json:"used_percentage"`
} `json:"context_window"`
}
func bar(pct float64, width int) string {
pct = math.Max(0, math.Min(100, pct))
filled := int(math.Floor(pct / (100.0 / float64(width))))
return strings.Repeat(filledBlock, filled) + strings.Repeat(lightBlock, width-filled)
}
func getContextPart(data *stdinData) string {
if data == nil {
return ""
}
pct := data.ContextWindow.UsedPercentage
return fmt.Sprintf("Context %s %d%%", bar(pct, 10), int(pct))
}
func formatMinutes(isoStr string) string {
if isoStr == "" {
return ""
}
t, err := time.Parse(time.RFC3339, isoStr)
if err != nil {
return ""
}
mins := int(math.Max(0, time.Until(t).Minutes()))
return fmt.Sprintf("%dM", mins)
}
func getUsagePart(data *fetcher.CacheData) string {
if data == nil {
return ""
}
if data.Error != "" {
switch data.Error {
case "auth_expired":
return "Token: session expired"
case "no_session_key":
return ""
default:
return "Token: error"
}
}
var parts []string
if data.FiveHour != nil && data.FiveHour.Utilization > 0 {
pct := data.FiveHour.Utilization
s := fmt.Sprintf("Token %s %d%%", bar(pct, 10), int(math.Round(pct)))
if mins := formatMinutes(data.FiveHour.ResetsAt); mins != "" {
s += " " + mins
}
parts = append(parts, s)
}
if data.SevenDay != nil && data.SevenDay.Utilization > 20 {
parts = append(parts, fmt.Sprintf("7d %d%%", int(math.Round(data.SevenDay.Utilization))))
}
return strings.Join(parts, " | ")
}
func main() {
// Read stdin (context window data from Claude Code)
var input stdinData
var hasInput bool
dec := json.NewDecoder(os.Stdin)
if err := dec.Decode(&input); err == nil {
hasInput = true
}
// Read cache
maxAge := 900 * time.Second
if v := os.Getenv("CLAUDE_USAGE_MAX_AGE"); v != "" {
if secs, err := strconv.Atoi(v); err == nil {
maxAge = time.Duration(secs) * time.Second
}
}
cache := fetcher.ReadCacheIfFresh(maxAge)
// Build output
var parts []string
if hasInput {
if ctx := getContextPart(&input); ctx != "" {
parts = append(parts, ctx)
}
}
if usage := getUsagePart(cache); usage != "" {
parts = append(parts, usage)
}
if len(parts) > 0 {
fmt.Println(strings.Join(parts, " | "))
}
}