Skip to content

Instantly share code, notes, and snippets.

@andrienko
Created July 13, 2026 16:52
Show Gist options
  • Select an option

  • Save andrienko/9b8b831b1808185e77eb46eb153143fb to your computer and use it in GitHub Desktop.

Select an option

Save andrienko/9b8b831b1808185e77eb46eb153143fb to your computer and use it in GitHub Desktop.
Status line for claude
#!/usr/bin/env node
// Claude Code statusline.
// Renders: MODEL | ctx% (size) | 5h% (5h) | 7d% (7d) | cwd
// Reads the statusLine JSON payload on stdin, prints one line on stdout.
import os from 'node:os';
const config = {
// Threshold colors for percentages. Values interpolate between these.
pct: {
green: [78, 201, 78],
yellow: [214, 200, 74],
red: [212, 90, 74],
// Below greenMax: solid green. greenMax..yellowStart: lerp green->yellow.
// yellowStart..yellowMax: solid yellow. yellowMax..redStart: lerp yellow->red.
// At/above redStart: solid red.
greenMax: 40,
yellowStart: 60,
yellowMax: 70,
redStart: 90,
},
model: {
haiku: [225, 138, 138], // pale red
sonnet: [92, 152, 240], // blue
opus: [232, 232, 232], // white
fallback: [200, 200, 200],
},
separator: [92, 92, 92], // darkgrey
muted: [140, 140, 140], // grey: parens and cwd
cwdMaxLen: 30, // abbreviate path segments past this length
};
const esc = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
const reset = '\x1b[0m';
const paint = (rgb, text) => `${esc(rgb[0], rgb[1], rgb[2])}${text}${reset}`;
const lerp = (a, b, t) => Math.round(a + (b - a) * t);
const lerpRgb = (a, b, t) => [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)];
const pctColor = (p) => {
const c = config.pct;
if (p <= c.greenMax) return c.green;
if (p < c.yellowStart) return lerpRgb(c.green, c.yellow, (p - c.greenMax) / (c.yellowStart - c.greenMax));
if (p <= c.yellowMax) return c.yellow;
if (p < c.redStart) return lerpRgb(c.yellow, c.red, (p - c.yellowMax) / (c.redStart - c.yellowMax));
return c.red;
};
// HSL -> RGB, h in [0,1], s/l in [0,1].
const hslToRgb = (h, s, l) => {
if (s === 0) { const v = Math.round(l * 255); return [v, v, v]; }
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
return [
Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
Math.round(hue2rgb(p, q, h) * 255),
Math.round(hue2rgb(p, q, h - 1 / 3) * 255),
];
};
// Fable: each letter a different color across the hue wheel.
const rainbow = (text) => {
const chars = [...text];
return chars
.map((ch, i) => (ch === ' ' ? ch : paint(hslToRgb(chars.length > 1 ? i / chars.length : 0, 1, 0.5), ch)))
.join('');
};
const detectFamily = (name, id) => {
const s = `${name} ${id}`.toLowerCase();
if (s.includes('haiku')) return 'haiku';
if (s.includes('sonnet')) return 'sonnet';
if (s.includes('opus')) return 'opus';
if (s.includes('fable')) return 'fable';
return null;
};
const renderModel = (model) => {
const name = model?.display_name || 'Model';
const family = detectFamily(name, model?.id || '');
// Short label: family name if recognized, else first token of display name.
const label = family ? family[0].toUpperCase() + family.slice(1) : name.split(/\s+/)[0];
if (family === 'fable') return rainbow(label);
return paint(config.model[family] || config.model.fallback, label);
};
// 1000000 -> "1kk", 200000 -> "200k", 8000 -> "8k", else raw.
const fmtSize = (n) => {
if (!Number.isFinite(n) || n <= 0) return '?';
if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(n % 1_000_000 ? 1 : 0)}kk`;
if (n >= 1000) return `${+(n / 1000).toFixed(n % 1000 ? 1 : 0)}k`;
return `${n}`;
};
// Rate-limit reset: unix epoch seconds -> "13/17" (day 13, 17:00), local time.
// Minutes appended only when non-zero: "13/17:30". Absent/invalid -> null.
const fmtReset = (epoch) => {
if (!Number.isFinite(epoch) || epoch <= 0) return null;
const dt = new Date(epoch * 1000);
const day = dt.getDate();
const hh = dt.getHours();
const mm = dt.getMinutes();
return mm ? `${day}/${hh}:${String(mm).padStart(2, '0')}` : `${day}/${hh}`;
};
const shortenCwd = (cwd) => {
if (!cwd) return '?';
const home = os.homedir();
let p = cwd;
const sep = p.includes('\\') ? '\\' : '/';
if (home && (p === home || p.startsWith(home + sep))) p = '~' + p.slice(home.length);
if (p.length <= config.cwdMaxLen) return p;
// Abbreviate every segment except the last to its first char.
const parts = p.split(/[\\/]/);
const last = parts.length - 1;
return parts
.map((seg, i) => {
if (i === last || seg === '' || seg === '~') return seg;
return seg.startsWith('.') ? seg.slice(0, 2) : seg[0];
})
.join(sep);
};
// A percentage cell: "NN%" colored by threshold, then " (label)" muted.
// Missing value (rate limit / usage not yet reported) -> null, so the cell is
// dropped from the line entirely rather than rendered as a dash.
const pctCell = (value, label) => {
if (value === undefined || value === null || Number.isNaN(value)) return null;
const p = Math.round(value);
return paint(pctColor(p), `${p}%`) + paint(config.muted, ` (${label})`);
};
const readStdin = () =>
new Promise((resolve) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (c) => (data += c));
process.stdin.on('end', () => resolve(data));
process.stdin.on('error', () => resolve(data));
});
const main = async () => {
let d = {};
try {
d = JSON.parse((await readStdin()).replace(/^\uFEFF/, '') || '{}');
} catch {
d = {};
}
const ctx = d.context_window || {};
const rl = d.rate_limits || {};
const cwd = d.workspace?.current_dir || d.cwd;
const sep = paint(config.separator, ' | ');
const sevenDay = rl.seven_day || {};
const cells = [
renderModel(d.model),
pctCell(ctx.used_percentage, fmtSize(ctx.context_window_size)),
pctCell(rl.five_hour?.used_percentage, '5h'),
pctCell(sevenDay.used_percentage, fmtReset(sevenDay.resets_at) || '7d'),
paint(config.muted, shortenCwd(cwd)),
].filter((c) => c != null);
process.stdout.write(cells.join(sep));
};
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment