Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 8 additions & 69 deletions cmd/statusline.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import (
"strings"

"github.com/spf13/cobra"

"github.com/RandomCodeSpace/ctm/internal/claude"
)

func init() {
Expand All @@ -24,17 +22,15 @@ func init() {
// we print a three-line display on stdout. Hidden because it's an
// internal hook, not a user-facing command.
//
// Output layout (3 lines):
// Output layout (2 lines):
//
// Line 1: <model> · <project> (project shown as a plain path)
// Line 2: c 25% (437k) w 40% h 10% (context % + tokens + rate limits)
// Line 3: ↑ 117k ↓ 434k (cumulative session input / output)
// Line 2: ctx 25% w 40% h 10% (context % + rate limits)
//
// Cache_read (⚡) was dropped from the status because its magnitude is
// already captured in the context-tokens parenthesis and Claude Code's
// own focus-mode overlay duplicates the information. Weekly / 5-hour
// rate limits share line 2 with context because they're all
// percentages; tokens share line 3 because both are cumulative ints.
// Cache_read (⚡) was dropped because its magnitude is already captured
// in the context-tokens parenthesis and Claude Code's own focus-mode
// overlay duplicates the information. Weekly / 5-hour rate limits
// share line 2 with context because they're all percentages.
var statuslineCmd = &cobra.Command{
Use: "statusline",
Short: "Internal statusLine renderer — reads JSON on stdin (hidden)",
Expand Down Expand Up @@ -138,9 +134,6 @@ const (
cMagenta = "\x1b[1;38;5;220m" // weekly bar
cYellow = "\x1b[1;38;5;208m" // 5-hour bar
cHdrModel = "\x1b[1;97m"
cTokIn = "\x1b[1;38;5;33m"
cTokOut = "\x1b[1;38;5;37m"
cDimGray = "\x1b[90m"
)

func renderStatusline(in *statuslineInput) string {
Expand All @@ -153,9 +146,6 @@ func renderStatusline(in *statuslineInput) string {
if mid != "" {
lines = append(lines, mid)
}
if s := buildTokenLine(in); s != "" {
lines = append(lines, s)
}
return strings.Join(lines, "\n")
}

Expand Down Expand Up @@ -201,66 +191,15 @@ func buildHeader(in *statuslineInput) string {
}
}

// readEffortLevel is a cmd-package-local wrapper so other renderers can
// pick up the current effort level without duplicating the path dance.
// Silent on every error path — effort is a nice-to-have, not critical.
func readEffortLevel() string {
p, err := claude.SettingsJSONPath()
if err != nil {
return ""
}
return claude.ReadEffortLevel(p)
}

// buildContextLine builds the `c <pct>% (<tokens>)` segment of line 2.
// The context-window-used percentage is the primary signal; the
// parenthesised token sum (input + cache_creation + cache_read, per
// Claude Code's input-only formula) is a secondary concrete number.
// buildContextLine builds the `ctx <pct>%` segment of line 2.
// Returns "" when used_percentage is absent.
func buildContextLine(in *statuslineInput) string {
used := in.ContextWindow.UsedPercentage
if used == nil {
return ""
}
usedPct := int(math.Round(*used))
entry := fmt.Sprintf("%sc %d%%%s", cCyan, usedPct, cReset)
if ctx := contextTokens(in); ctx > 0 {
entry += fmt.Sprintf(" %s(%s)%s", cDimGray, fmtTokens(ctx), cReset)
}
return entry
}

// buildTokenLine renders the cumulative session token totals: `↑ <input>`
// and `↓ <output>`. cache_read (⚡) used to live here too; it was dropped
// from the statusline — the token magnitude is already visible as the
// parenthesised number on the context line and Claude Code's focus-mode
// overlay renders its own cache indicator.
func buildTokenLine(in *statuslineInput) string {
var parts []string
add := func(glyph rune, color string, n *int64) {
if n == nil || *n <= 0 {
return
}
parts = append(parts, fmt.Sprintf("%s%c%s %s%s%s",
color, glyph, cReset, cDimGray, fmtTokens(*n), cReset))
}
add('↑', cTokIn, in.ContextWindow.TotalInputTokens)
add('↓', cTokOut, in.ContextWindow.TotalOutputTokens)
line := strings.Join(parts, " ")

// Tack the current effort level onto the last line. Sourced from
// ~/.claude/settings.json via readEffortLevel — not in Claude
// Code's statusLine payload. Dim-gray so it reads as secondary
// info next to the token counts. Only appended when at least one
// token is present so a truly empty payload doesn't render a
// lone "· xhigh" orphan.
if line == "" {
return ""
}
if effort := readEffortLevel(); effort != "" {
line += fmt.Sprintf(" %s%s%s", cDimGray, effort, cReset)
}
return line
return fmt.Sprintf("%sctx %d%%%s", cCyan, usedPct, cReset)
}

// buildRateLimitLine renders `w <pct>%` and `h <pct>%` for weekly and
Expand Down
62 changes: 8 additions & 54 deletions cmd/statusline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,28 @@ func TestRenderStatuslineFullPayload(t *testing.T) {

out := renderStatusline(in)
lines := strings.Split(out, "\n")
if len(lines) != 3 {
t.Fatalf("expected 3 lines, got %d:\n%s", len(lines), out)
if len(lines) != 2 {
t.Fatalf("expected 2 lines, got %d:\n%s", len(lines), out)
}
// Line 0 — Header: full model name (minus the redundant "Claude "
// prefix) plus the project tail.
if !strings.Contains(lines[0], "Sonnet 4.5 (1M)") {
t.Errorf("header missing full model name: %q", lines[0])
}
if !strings.Contains(lines[0], "ctm-statusline-fake") {
t.Errorf("header missing project tail: %q", lines[0])
}
// Line 1 — Context + rate limits share one line now: c / w / h.
for _, want := range []string{"c", "25%", "w", "40%", "h", "10%"} {
for _, want := range []string{"ctx", "25%", "w", "40%", "h", "10%"} {
if !strings.Contains(lines[1], want) {
t.Errorf("line 2 missing %q: %q", want, lines[1])
}
}
// ⚡ should NOT appear anywhere — cache was dropped as a separate
// statusline entry. (The cache_read value may still contribute to
// the parenthesised context-tokens sum, which is expected.)
if strings.Contains(out, "⚡") {
t.Errorf("cache glyph ⚡ should have been removed:\n%s", out)
if strings.Contains(lines[1], "(") || strings.Contains(lines[1], ")") {
t.Errorf("line 2 should not contain token-count parens: %q", lines[1])
}
// Line 2 — Tokens: input + output only.
for _, want := range []string{"↑", "↓", "12.3k", "6.8k"} {
if !strings.Contains(lines[2], want) {
t.Errorf("token line missing %q: %q", want, lines[2])
for _, banned := range []string{"⚡", "↑", "↓"} {
if strings.Contains(out, banned) {
t.Errorf("dropped glyph %q should not appear:\n%s", banned, out)
}
}
// No bar runes should appear anywhere.
for _, bar := range []string{"━", "─"} {
if strings.Contains(out, bar) {
t.Errorf("unexpected bar rune %q in output:\n%s", bar, out)
Expand Down Expand Up @@ -122,44 +114,6 @@ func TestContextTokens_AllNilReturnsZero(t *testing.T) {
}
}

func TestRenderStatuslineShowsContextTokens(t *testing.T) {
in := &statuslineInput{}
in.Model.DisplayName = "Claude Sonnet 4.5"
in.ContextWindow.UsedPercentage = floatPtr(42)
in.ContextWindow.CurrentUsage.InputTokens = intPtr(12000)
in.ContextWindow.CurrentUsage.CacheCreationInputTokens = intPtr(8000)
in.ContextWindow.CurrentUsage.CacheReadInputTokens = intPtr(417270)
// Sum = 437 270 → formats as "437.3k"

out := renderStatusline(in)
lines := strings.Split(out, "\n")
if len(lines) < 2 {
t.Fatalf("expected at least 2 lines, got:\n%s", out)
}
if !strings.Contains(lines[1], "437.3k") {
t.Errorf("line 2 missing context token count: %q", lines[1])
}
if !strings.Contains(lines[1], "42%") {
t.Errorf("line 2 missing context %%: %q", lines[1])
}
}

func TestRenderStatuslineOmitsContextTokensWhenZero(t *testing.T) {
in := &statuslineInput{}
in.Model.DisplayName = "Claude Sonnet 4.5"
in.ContextWindow.UsedPercentage = floatPtr(0)
// current_usage absent — contextTokens = 0

out := renderStatusline(in)
lines := strings.Split(out, "\n")
if len(lines) < 2 {
t.Fatalf("expected at least 2 lines, got:\n%s", out)
}
if strings.Contains(lines[1], "(") {
t.Errorf("line 2 unexpectedly includes a token-count paren: %q", lines[1])
}
}

func TestFmtTokens(t *testing.T) {
cases := map[int64]string{
0: "0",
Expand Down
Loading