Skip to content

Instantly share code, notes, and snippets.

@MetalArend
Last active March 31, 2026 15:59
Show Gist options
  • Select an option

  • Save MetalArend/90788ef3ce05236aba635f5f46fcbc49 to your computer and use it in GitHub Desktop.

Select an option

Save MetalArend/90788ef3ce05236aba635f5f46fcbc49 to your computer and use it in GitHub Desktop.
Claude Statusline with limits

Claude Code Status Line

A custom status line script for Claude Code that displays directory, git branch, model, current task, context window usage, session stats, and rate limits in the terminal.

Prerequisites

  • Node.js (available as node on $PATH)
  • git (for the git segment)

Installation

  1. Place the script at ~/.claude/statusline.js.

  2. Add the statusLine hook to ~/.claude/settings.json:

    {
      "statusLine": {
        "type": "command",
        "command": "node /home/YOUR_USERNAME/.claude/statusline.js"
      }
    }

    Replace YOUR_USERNAME with your actual username, or use the full absolute path.

    Note: Use an absolute path — the ~ shorthand does not expand in this config value.

What Each Segment Shows

~/projects/myapp | main* ↑2 | Claude Sonnet 4.6 | plan | Writing tests +2
⏱ 12m | ████░ 74% (80%) | 85% cache · $0.023/hr · $0.042 · 1.2k tpm | +156 -23 | 5h ███░░ 60% (!45m/↺2h) · 7d █░░░░ 20% (↺5d)
Segment Description
~/projects/myapp Working directory ($HOME replaced with ~); truncated if truncatePath is enabled
main* ↑2 Git branch name; * if uncommitted changes; ↑N/↓N for commits ahead/behind upstream
Claude Sonnet 4.6 Active model name; hidden before the first API response
plan Permission mode — only shown when non-default (bypass in red, auto-edit in yellow, plan in cyan)
Writing tests +2 In-progress task label from the Claude Code task list; +N shows pending task count
⏱ 12m Session elapsed time
████░ 74% (80%) Context window bar; usable% (total%)
85% cache Cache hit efficiency from the current context; green ≥ 80%, yellow ≥ 50%, grey below
$0.023/hr Cost burn rate per hour
$0.042 Cumulative session cost
1.2k tpm Tokens per minute (input + output combined)
+156 -23 Lines added (green) and removed (red) this session
5h ███░░ 60% (!45m/↺2h) 5-hour rate limit bar; ! predicted exhaustion time, time until reset
7d █░░░░ 20% (↺5d) 7-day rate limit bar

Context window bar

The bar shows two percentages: usable% (total%).

  • usable% — percentage of the usable context (excluding the auto-compact buffer) that has been used; this is what the bar visualises
  • total% — raw percentage of the full context window used (shown in grey, in parentheses)

The greyed-out blocks at the right of the bar represent the auto-compact buffer zone (read from CLAUDE_AUTOCOMPACT_PCT_OVERRIDE, defaulting to 20%). The bar color transitions green → yellow → orange → red per barColorThresholds, and starts blinking above contextBlinkThreshold.

Rate limit time display

The time shown in parentheses after each rate limit bar is:

  • reset time only — if usage is 0% or 100%, or exhaustion time cannot be computed
  • ! exhaustion + reset — if the limit will be exhausted before it resets:
    • Yellow if the gap between exhaustion and reset is less than exhaustionWarningPercent of the period
    • Red if the gap is larger

With useRelativeTime: false, times are shown as absolute dates (Thu 27/03 14:00) instead of relative durations.

Configuration

All options live in the config object at the top of statusline.js. They can also be overridden via settings.json without editing the script — see Overriding via settings.json below.

barBlockCount

Default: 5

Width of all progress bars, in block characters.

barColorThresholds

Default: [50, 65, 80]

Three percentage cutoffs [low, mid, high] that control bar and text color:

Usage Color
< 50% Green
50–64% Yellow
65–79% Orange
≥ 80% Red (or blinking red for context bar above contextBlinkThreshold)

contextBlinkThreshold

Default: 95

When context total usage exceeds this percentage, the context bar switches to blinking red.

exhaustionWarningPercent

Default: 20

When the predicted rate-limit exhaustion time is within this percentage of the period before the reset, the exhaustion label turns yellow (rather than red).

groupSeparator

Default: ' · '

String placed between segment names within a group array.

labels

Default:

{
    elapsedTime:         '⏱',
    gitDirty:            '*',
    gitAhead:            '↑',
    gitBehind:           '↓',
    rateLimitsReset:     '↺',
    rateLimitsExhaustion:'!',
    rateLimits5h:        '5h',
    rateLimits7d:        '7d',
}

Short strings and icons used in the output. When overriding via settings.json, only the keys you specify are changed (see merge behaviour below).

maxPathLength

Default: 40

Maximum characters for the directory path before truncation (only applied when truncatePath is true). Truncation uses a centre-ellipsis strategy.

maxTaskLength

Default: 40

Maximum characters for the task label before truncation. Same centre-ellipsis strategy.

segmentOrder

Default:

[
    'directory',
    'git',
    'model',
    'permissionMode',
    'task',
    '\n',
    'elapsedTime',
    'context',
    ['cache', 'burnRate', 'totalCost', 'tpm'],
    'linesChanged',
    ['rateLimits5h', 'rateLimits7d'],
]

Controls which segments appear and in what order. Rules:

  • Each entry is a segment name string, a group array, or '\n'.
  • A '\n' entry starts a new output line.
  • An array of names (e.g. ['cache', 'burnRate', 'totalCost', 'tpm']) groups those segments together, joining them with groupSeparator (·) instead of segmentSeparator (|).
  • Segments whose data is unavailable are automatically hidden; empty groups are omitted.

Available segment names:

Name Description
burnRate Cost burn rate ($/hr)
cache Cache hit efficiency %
context Context window bar with usable% and total%
directory Current working directory path
elapsedTime Session elapsed time
git Branch name, dirty indicator, ahead/behind counts
linesChanged Lines added/removed this session
model Active Claude model name
permissionMode Active permission mode (hidden when default)
rateLimits5h 5-hour rate limit bar and reset/exhaustion time
rateLimits7d 7-day rate limit bar and reset/exhaustion time
task In-progress task label and pending task count
totalCost Cumulative session cost in USD
tpm Tokens per minute

segmentSeparator

Default: ' | '

String placed between top-level segments (and between groups) on the same line.

theme

Default:

{
    reset:    '\x1b[0m',
    bold:     '\x1b[1m',
    dim:      '\x1b[2m',
    path:     '\x1b[34m',      // blue
    git:      '\x1b[35m',      // magenta
    accent:   '\x1b[36m',      // cyan
    good:     '\x1b[32m',      // green
    warning:  '\x1b[33m',      // yellow
    caution:  '\x1b[38;5;208m',// orange
    critical: '\x1b[31m',      // red
    alert:    '\x1b[5;31m',    // blinking red
    secondary:'\x1b[90m',      // grey
}

ANSI escape codes for each color role. When overriding via settings.json, only the keys you specify are changed — the rest keep their defaults (see merge behaviour below).

truncatePath

Default: true

When true, directory paths longer than maxPathLength are truncated with a centre ellipsis ().

useRelativeTime

Default: true

When true, rate limit times display as relative durations (↺2h30m). When false, they display as absolute timestamps (Thu 27/03 14:00).


Overriding via settings.json

You can override any config value by adding a statusLine object to ~/.claude/settings.json alongside the existing type/command keys:

{
  "statusLine": {
    "type": "command",
    "command": "node /home/YOUR_USERNAME/.claude/statusline.js",
    "segmentOrder": [
      "directory", "git", "model",
      "\n",
      "elapsedTime", "context", ["burnRate", "totalCost"]
    ],
    "segmentSeparator": "  ",
    "groupSeparator": " / ",
    "barBlockCount": 8,
    "maxPathLength": 50,
    "maxTaskLength": 30,
    "barColorThresholds": [40, 60, 80],
    "contextBlinkThreshold": 90,
    "exhaustionWarningPercent": 15,
    "truncatePath": false,
    "useRelativeTime": false,
    "theme": {
      "path": "\u001b[33m"
    },
    "labels": {
      "elapsedTime": "🕐",
      "rateLimits5h": "5H",
      "rateLimits7d": "7D"
    }
  }
}

Overwrite behaviour for top-level keys

The following top-level keys replace the default entirely when provided:

segmentOrder, segmentSeparator, groupSeparator, barBlockCount, maxPathLength, maxTaskLength, barColorThresholds, contextBlinkThreshold, exhaustionWarningPercent, truncatePath, useRelativeTime

Type-checking is enforced: a wrong type is silently ignored and the default is kept.

Merge behaviour for theme and labels

theme and labels are not replaced wholesale. Instead, only the keys you provide are updated; all other keys keep their defaults. This means you can change a single icon or color without restating the entire object.

Only existing keys are accepted — unknown keys are silently ignored.


Files

Path Purpose
~/.claude/statusline.js The status line script
~/.claude/settings.json Claude Code settings where statusLine is configured and overrides are placed
{
"statusLine": {
"type": "command",
"command": "node /home/mammi/.claude/statusline.js"
}
}
#!/usr/bin/env node
// Claude Code status line script
const fs = require('fs');
const path = require('path');
const os = require('os');
const {execFileSync} = require('child_process');
// --- Config defaults ---
// All keys can be overridden via the statusLine object in ~/.claude/settings.json.
//
// Segment names (alphabetical):
// burnRate — cost burn rate ($/hr)
// cache — cache hit efficiency %
// context — context window bar with usable% and total%
// directory — current working directory path
// elapsedTime — session elapsed time
// git — branch name, dirty indicator, ahead/behind counts
// linesChanged — lines added/removed this session
// model — active Claude model name
// permissionMode — active permission mode (hidden when default)
// rateLimits5h — 5-hour rate limit bar and reset/exhaustion time
// rateLimits7d — 7-day rate limit bar and reset/exhaustion time
// task — in-progress task label and pending task count
// totalCost — cumulative session cost in USD
// tpm — tokens per minute
//
// Use "\n" as a segmentOrder entry to start a new line.
// Wrap names in an array to group them with groupSeparator: ["burnRate", "totalCost", "tpm"]
const config = {
segmentOrder: [
'directory',
'git',
'model',
'permissionMode',
'task',
'\n',
'elapsedTime',
'context',
['cache', 'burnRate', 'totalCost', 'tpm'],
'linesChanged',
['rateLimits5h', 'rateLimits7d'],
],
segmentSeparator: ' | ',
groupSeparator: ' · ',
barBlockCount: 5,
maxPathLength: 40,
maxTaskLength: 40,
barColorThresholds: [50, 65, 80],
contextBlinkThreshold: 95,
exhaustionWarningPercent: 20,
truncatePath: true,
useRelativeTime: true,
theme: {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
path: '\x1b[34m',
git: '\x1b[35m',
accent: '\x1b[36m',
good: '\x1b[32m',
warning: '\x1b[33m',
caution: '\x1b[38;5;208m',
critical: '\x1b[31m',
alert: '\x1b[5;31m',
secondary: '\x1b[90m',
},
labels: {
elapsedTime: '⏱',
gitDirty: '*',
gitAhead: '↑',
gitBehind: '↓',
rateLimitsReset: '↺',
rateLimitsExhaustion: '!',
rateLimits5h: '5h',
rateLimits7d: '7d',
},
};
const claudeConfigDirectory = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
// Load overrides from settings.json
const configTypes = {
segmentOrder: 'array',
segmentSeparator: 'string',
groupSeparator: 'string',
barBlockCount: 'number',
maxPathLength: 'number',
maxTaskLength: 'number',
barColorThresholds: 'array',
contextBlinkThreshold: 'number',
exhaustionWarningPercent: 'number',
truncatePath: 'boolean',
useRelativeTime: 'boolean',
};
try {
const userStatusLine = JSON.parse(fs.readFileSync(path.join(claudeConfigDirectory, 'settings.json'), 'utf8')).statusLine || {};
for (const [key, type] of Object.entries(configTypes)) {
const value = userStatusLine[key];
if (value === undefined) continue;
if (type === 'array' ? Array.isArray(value) : typeof value === type) config[key] = value;
}
for (const key of ['labels', 'theme']) {
const sub = userStatusLine[key];
if (sub && typeof sub === 'object' && !Array.isArray(sub)) {
for (const [k, v] of Object.entries(sub)) {
if (typeof v === 'string' && k in config[key]) config[key][k] = v;
}
}
}
} catch (_) {
}
// --- Helpers ---
function truncate(text, maxLength) {
if (text.length <= maxLength) return text;
const leftLength = Math.ceil((maxLength - 1) / 2);
const rightLength = Math.floor((maxLength - 1) / 2);
return text.slice(0, leftLength) + '…' + text.slice(text.length - rightLength);
}
function barColor(percentage, enableBlink = false) {
const [lowThreshold, midThreshold, highThreshold] = config.barColorThresholds;
if (percentage < lowThreshold) return config.theme.good;
if (percentage < midThreshold) return config.theme.warning;
if (percentage < highThreshold) return config.theme.caution;
return enableBlink ? config.theme.alert : config.theme.critical;
}
function makeBar(percentage, blockCount, fillColor, reservedBlockCount = 0) {
const clampedReserved = Math.min(reservedBlockCount, blockCount);
const usableBlockCount = blockCount - clampedReserved;
const filledBlockCount = Math.min(usableBlockCount, Math.round(percentage * usableBlockCount / 100));
return fillColor + '█'.repeat(filledBlockCount) + '░'.repeat(usableBlockCount - filledBlockCount) + config.theme.secondary + '░'.repeat(clampedReserved) + config.theme.reset;
}
function formatAbsolute(timestamp) {
const date = new Date(timestamp * 1000);
const weekday = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date.getDay()];
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${weekday} ${day}/${month} ${hours}:${minutes}`;
}
function formatRelative(seconds) {
if (seconds <= 0) return '0m';
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) return hours > 0 ? `${days}d${hours}h` : `${days}d`;
return hours > 0 ? (minutes > 0 ? `${hours}h${minutes}m` : `${hours}h`) : `${minutes}m`;
}
function formatLimit(usedPercentage, resetsAtTimestamp, periodSeconds, periodLabel, nowTimestamp) {
const percentage = Math.round(usedPercentage);
const fillColor = barColor(percentage);
const progressBar = makeBar(percentage, config.barBlockCount, fillColor);
if (resetsAtTimestamp == null) return `${periodLabel} ${progressBar} ${fillColor}${percentage}%${config.theme.reset}`;
const resetLabel = config.useRelativeTime
? formatRelative(resetsAtTimestamp - nowTimestamp)
: formatAbsolute(resetsAtTimestamp);
if (usedPercentage <= 0 || usedPercentage >= 100) {
return `${periodLabel} ${progressBar} ${fillColor}${percentage}%${config.theme.reset} (${config.labels.rateLimitsReset}${resetLabel})`;
}
const elapsedSeconds = nowTimestamp - (resetsAtTimestamp - periodSeconds);
if (elapsedSeconds <= 0) {
return `${periodLabel} ${progressBar} ${fillColor}${percentage}%${config.theme.reset} (${config.labels.rateLimitsReset}${resetLabel})`;
}
const exhaustionTimestamp = (resetsAtTimestamp - periodSeconds) + (elapsedSeconds * 100 / usedPercentage);
const timeUntilExhaustion = exhaustionTimestamp - nowTimestamp;
if (exhaustionTimestamp < resetsAtTimestamp) {
const gapPercent = (resetsAtTimestamp - exhaustionTimestamp) / periodSeconds * 100;
const warningColor = gapPercent < config.exhaustionWarningPercent ? config.theme.warning : config.theme.critical;
const exhaustionLabel = config.useRelativeTime
? formatRelative(timeUntilExhaustion)
: formatAbsolute(exhaustionTimestamp);
return `${periodLabel} ${progressBar} ${fillColor}${percentage}%${config.theme.reset} (${warningColor}${config.labels.rateLimitsExhaustion}${exhaustionLabel}${config.theme.reset}/${config.labels.rateLimitsReset}${resetLabel})`;
} else {
return `${periodLabel} ${progressBar} ${fillColor}${percentage}%${config.theme.reset} (${config.labels.rateLimitsReset}${resetLabel})`;
}
}
// --- Main ---
let stdinBuffer = '';
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => stdinBuffer += chunk);
process.stdin.on('end', () => {
clearTimeout(stdinTimeout);
let input;
try {
input = JSON.parse(stdinBuffer);
} catch (_) {
process.exit(0);
}
try {
const activeSegments = new Set(config.segmentOrder.flat().filter(s => s !== '\n'));
const segments = {};
// directory + git (share currentDirectory read)
if (activeSegments.has('directory') || activeSegments.has('git')) {
const currentDirectory = input.workspace?.current_dir || input.cwd || '';
if (activeSegments.has('directory') && currentDirectory) {
const homeDirectory = process.env.HOME || '';
const shortenedPath = homeDirectory && currentDirectory.startsWith(homeDirectory)
? '~' + currentDirectory.slice(homeDirectory.length)
: currentDirectory;
const displayedPath = config.truncatePath ? truncate(shortenedPath, config.maxPathLength) : shortenedPath;
segments.directory = config.theme.path + displayedPath + config.theme.reset;
}
if (activeSegments.has('git') && currentDirectory) {
try {
execFileSync('git', ['-C', currentDirectory, 'rev-parse', '--git-dir'], {stdio: 'ignore'});
let branch = '';
try {
branch = execFileSync(
'git', ['-C', currentDirectory, '--no-optional-locks', 'symbolic-ref', '--short', 'HEAD'],
{stdio: ['ignore', 'pipe', 'ignore']}
).toString().trim();
} catch (_) {
}
if (branch) {
let dirtyIndicator = '';
try {
execFileSync('git', ['-C', currentDirectory, '--no-optional-locks', 'diff', '--quiet'], {stdio: 'ignore'});
} catch (_) {
dirtyIndicator = config.labels.gitDirty;
}
if (!dirtyIndicator) {
try {
execFileSync('git', ['-C', currentDirectory, '--no-optional-locks', 'diff', '--cached', '--quiet'], {stdio: 'ignore'});
} catch (_) {
dirtyIndicator = config.labels.gitDirty;
}
}
let aheadBehindIndicator = '';
try {
const aheadBehindCounts = execFileSync(
'git', ['-C', currentDirectory, '--no-optional-locks', 'rev-list', '--left-right', '--count', 'HEAD...@{upstream}'],
{stdio: ['ignore', 'pipe', 'ignore']}
).toString().trim().split('\t');
const ahead = parseInt(aheadBehindCounts[0], 10) || 0;
const behind = parseInt(aheadBehindCounts[1], 10) || 0;
if (ahead) aheadBehindIndicator += config.labels.gitAhead + ahead;
if (behind) aheadBehindIndicator += config.labels.gitBehind + behind;
} catch (_) {
}
segments.git = config.theme.git + branch + dirtyIndicator + (aheadBehindIndicator ? ' ' + aheadBehindIndicator : '') + config.theme.reset;
}
} catch (_) {
}
}
}
// model
if (activeSegments.has('model')) {
const modelName = input.model?.display_name;
if (modelName) {
segments.model = config.theme.accent + config.theme.bold + modelName + config.theme.reset;
}
}
// permissionMode
if (activeSegments.has('permissionMode')) {
const permissionMode = input.permissionMode || '';
if (permissionMode && permissionMode !== 'default') {
const permissionModeLabels = {
bypassPermissions: config.theme.critical + 'bypass',
acceptEdits: config.theme.warning + 'auto-edit',
plan: config.theme.accent + 'plan'
};
segments.permissionMode = (permissionModeLabels[permissionMode] || config.theme.warning + permissionMode) + config.theme.reset;
}
}
// task
if (activeSegments.has('task')) {
const sessionId = input.session_id || '';
if (sessionId) {
try {
const tasksDirectory = path.join(claudeConfigDirectory, 'tasks', sessionId);
const taskFiles = fs.readdirSync(tasksDirectory).filter(f => f.endsWith('.json'));
let inProgressTaskLabel = '';
let pendingTaskCount = 0;
for (const taskFile of taskFiles) {
const task = JSON.parse(fs.readFileSync(path.join(tasksDirectory, taskFile), 'utf8'));
if (task.status === 'in_progress') inProgressTaskLabel = task.activeForm || task.subject || '';
else if (task.status === 'pending') pendingTaskCount++;
}
if (inProgressTaskLabel || pendingTaskCount > 0) {
const pendingCountSuffix = pendingTaskCount > 0 ? config.theme.dim + ' +' + pendingTaskCount + config.theme.reset : '';
segments.task = config.theme.bold + truncate(inProgressTaskLabel, config.maxTaskLength) + config.theme.reset + pendingCountSuffix;
}
} catch (_) {
}
}
}
// elapsedTime
if (activeSegments.has('elapsedTime')) {
const durationMilliseconds = input.cost?.total_duration_ms;
if (durationMilliseconds > 0) {
const totalMinutes = Math.floor(durationMilliseconds / 60000);
const elapsedFormatted = totalMinutes < 60
? `${totalMinutes}m`
: `${Math.floor(totalMinutes / 60)}h${totalMinutes % 60 > 0 ? (totalMinutes % 60) + 'm' : ''}`;
segments.elapsedTime = config.labels.elapsedTime + ' ' + elapsedFormatted;
}
}
// context
if (activeSegments.has('context')) {
const remainingPercentage = input.context_window?.remaining_percentage;
if (remainingPercentage != null) {
const autoCompactPercentage = parseFloat(input.env?.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE ?? process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE ?? 20);
const usedPercentage = Math.round(100 - remainingPercentage);
const usableUsedPercentage = Math.min(100, Math.round(usedPercentage / (100 - autoCompactPercentage) * 100));
const autoCompactBlockCount = Math.max(0, Math.min(config.barBlockCount, Math.round(autoCompactPercentage * config.barBlockCount / 100)));
const contextColor = barColor(usedPercentage, usedPercentage >= config.contextBlinkThreshold);
const contextBar = makeBar(usableUsedPercentage, config.barBlockCount, contextColor, autoCompactBlockCount);
segments.context = contextBar + ' ' + contextColor + usableUsedPercentage + '%' + config.theme.reset + ' ' + config.theme.secondary + '(' + usedPercentage + '%)' + config.theme.reset;
}
}
// cache
if (activeSegments.has('cache')) {
const currentUsage = input.context_window?.current_usage;
if (currentUsage) {
const freshInputTokens = currentUsage.input_tokens ?? 0;
const cacheWriteTokens = currentUsage.cache_creation_input_tokens ?? 0;
const cacheReadTokens = currentUsage.cache_read_input_tokens ?? 0;
const totalInputTokens = freshInputTokens + cacheWriteTokens + cacheReadTokens;
if (totalInputTokens > 0) {
const cacheEfficiencyPercent = Math.round(cacheReadTokens / totalInputTokens * 100);
const cacheColor = cacheEfficiencyPercent >= 80 ? config.theme.good : cacheEfficiencyPercent >= 50 ? config.theme.warning : config.theme.secondary;
segments.cache = cacheColor + cacheEfficiencyPercent + '% cache' + config.theme.reset;
}
}
}
// burnRate, totalCost, tpm (share cost/duration reads)
if (activeSegments.has('burnRate') || activeSegments.has('totalCost') || activeSegments.has('tpm')) {
const totalCostUsd = input.cost?.total_cost_usd;
const totalDurationMs = input.cost?.total_duration_ms;
if (activeSegments.has('burnRate') && totalCostUsd != null && totalDurationMs > 0) {
const burnRatePerHour = totalCostUsd * 3600000 / totalDurationMs;
segments.burnRate = (burnRatePerHour < 0.01 ? '$' + burnRatePerHour.toFixed(3) : '$' + burnRatePerHour.toFixed(2)) + '/hr';
}
if (activeSegments.has('totalCost') && totalCostUsd != null) {
segments.totalCost = totalCostUsd < 0.01 ? '$' + totalCostUsd.toFixed(3) : '$' + totalCostUsd.toFixed(2);
}
if (activeSegments.has('tpm') && totalDurationMs > 0) {
const totalInputTokens = input.context_window?.total_input_tokens ?? 0;
const totalOutputTokens = input.context_window?.total_output_tokens ?? 0;
const totalTokens = totalInputTokens + totalOutputTokens;
if (totalTokens > 0) {
const tokensPerMinute = totalTokens * 60000 / totalDurationMs;
segments.tpm = (tokensPerMinute >= 1000
? (tokensPerMinute / 1000).toFixed(1) + 'k'
: Math.round(tokensPerMinute).toString()) + ' tpm';
}
}
}
// linesChanged
if (activeSegments.has('linesChanged')) {
const linesAdded = input.cost?.total_lines_added ?? 0;
const linesRemoved = input.cost?.total_lines_removed ?? 0;
if (linesAdded > 0 || linesRemoved > 0) {
const addedPart = linesAdded > 0 ? config.theme.good + '+' + linesAdded + config.theme.reset : '';
const removedPart = linesRemoved > 0 ? config.theme.critical + '-' + linesRemoved + config.theme.reset : '';
segments.linesChanged = addedPart + (linesAdded > 0 && linesRemoved > 0 ? ' ' : '') + removedPart;
}
}
// rateLimits5h
if (activeSegments.has('rateLimits5h')) {
const fiveHourUsedPercentage = input.rate_limits?.five_hour?.used_percentage ?? null;
const fiveHourResetsAt = input.rate_limits?.five_hour?.resets_at ?? null;
if (fiveHourUsedPercentage != null) {
segments.rateLimits5h = formatLimit(fiveHourUsedPercentage, fiveHourResetsAt, 18000, config.labels.rateLimits5h, Math.floor(Date.now() / 1000));
}
}
// rateLimits7d
if (activeSegments.has('rateLimits7d')) {
const sevenDayUsedPercentage = input.rate_limits?.seven_day?.used_percentage ?? null;
const sevenDayResetsAt = input.rate_limits?.seven_day?.resets_at ?? null;
if (sevenDayUsedPercentage != null) {
segments.rateLimits7d = formatLimit(sevenDayUsedPercentage, sevenDayResetsAt, 604800, config.labels.rateLimits7d, Math.floor(Date.now() / 1000));
}
}
// Assemble output from segmentOrder
const outputLines = [];
let currentLineParts = [];
for (const item of config.segmentOrder) {
if (item === '\n') {
outputLines.push(currentLineParts.join(config.segmentSeparator));
currentLineParts = [];
} else if (Array.isArray(item)) {
const groupParts = item.map(name => segments[name]).filter(Boolean);
if (groupParts.length > 0) currentLineParts.push(groupParts.join(config.groupSeparator));
} else {
if (segments[item]) currentLineParts.push(segments[item]);
}
}
outputLines.push(currentLineParts.join(config.segmentSeparator));
process.stdout.write(outputLines.join('\n') + '\n');
} catch (_) {
process.exit(0);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment