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