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

55
cmd/fetcher/main.go Normal file
View File

@@ -0,0 +1,55 @@
package main
import (
"fmt"
"os"
"git.davoryn.de/calic/claude-statusline/internal/config"
"git.davoryn.de/calic/claude-statusline/internal/fetcher"
)
func main() {
sessionKey := config.GetSessionKey()
if sessionKey == "" {
fmt.Fprintln(os.Stderr, "error: no session key (set CLAUDE_SESSION_KEY or write to "+config.SessionKeyPath()+")")
os.Exit(1)
}
cfg := config.Load()
data, orgID, err := fetcher.FetchUsage(sessionKey, cfg.OrgID)
if err != nil {
if data != nil {
// Write error state to cache so statusline can display it
_ = fetcher.WriteCache(data)
}
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if err := fetcher.WriteCache(data); err != nil {
fmt.Fprintf(os.Stderr, "error writing cache: %v\n", err)
os.Exit(1)
}
// Persist discovered org ID
if orgID != "" && orgID != cfg.OrgID {
cfg.OrgID = orgID
_ = config.Save(cfg)
}
parsed := fetcher.ParseUsage(data)
if parsed.FiveHourPct > 0 {
fmt.Printf("5h: %d%%", parsed.FiveHourPct)
if parsed.FiveHourResetsIn != "" {
fmt.Printf(" (resets in %s)", parsed.FiveHourResetsIn)
}
fmt.Println()
}
if parsed.SevenDayPct > 0 {
fmt.Printf("7d: %d%%", parsed.SevenDayPct)
if parsed.SevenDayResetsIn != "" {
fmt.Printf(" (resets in %s)", parsed.SevenDayResetsIn)
}
fmt.Println()
}
}

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, " | "))
}
}

9
cmd/widget/main.go Normal file
View File

@@ -0,0 +1,9 @@
package main
import (
"git.davoryn.de/calic/claude-statusline/internal/tray"
)
func main() {
tray.Run()
}