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>
121 lines
2.5 KiB
Go
121 lines
2.5 KiB
Go
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, " | "))
|
|
}
|
|
}
|