Skip to content

Instantly share code, notes, and snippets.

@tarasyarema
Created January 7, 2026 02:25
Show Gist options
  • Select an option

  • Save tarasyarema/8579b12ff8b2a1ce0bf8964efb75f987 to your computer and use it in GitHub Desktop.

Select an option

Save tarasyarema/8579b12ff8b2a1ce0bf8964efb75f987 to your computer and use it in GitHub Desktop.
~/.claude/statusline.ts
#!/usr/bin/env bun
import { execSync } from 'child_process';
import { existsSync, readFileSync, statSync } from 'fs';
import { basename } from 'path';
interface StatusInput {
session_id: string;
transcript_path: string;
cwd: string;
model: {
id: string;
display_name: string;
};
workspace: {
current_dir: string;
project_dir: string;
};
version: string;
output_style: {
name: string;
};
context_window: {
total_input_tokens: number;
total_output_tokens: number;
context_window_size: number;
current_usage: {
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens: number;
cache_read_input_tokens: number;
} | null;
};
}
// ANSI color codes
const colors = {
reset: '\x1b[0m',
dim: '\x1b[2m',
bold: '\x1b[1m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
gray: '\x1b[90m',
brightGreen: '\x1b[92m',
brightYellow: '\x1b[93m',
brightBlue: '\x1b[94m',
brightMagenta: '\x1b[95m',
brightCyan: '\x1b[96m',
brightRed: '\x1b[91m',
};
const c = colors;
// Read stdin
const input: StatusInput = await Bun.stdin.json();
// Format number with k/M suffix
function formatTokens(n: number, decimals: number = 1): string {
if (n >= 1_000_000) {
return (n / 1_000_000).toFixed(decimals) + 'M';
} else if (n >= 1_000) {
return (n / 1_000).toFixed(decimals) + 'k';
}
return n.toString();
}
// Calculate context usage
let pctUsed = 0;
let currentTokens = 0;
const totalTokens = input.context_window.context_window_size;
let inputTokens = 0;
let outputTokens = 0;
let cachedTokens = 0;
if (input.context_window.current_usage) {
const usage = input.context_window.current_usage;
inputTokens = usage.input_tokens;
outputTokens = usage.output_tokens;
cachedTokens = usage.cache_creation_input_tokens + usage.cache_read_input_tokens;
currentTokens = inputTokens + cachedTokens;
pctUsed = Math.round((currentTokens / totalTokens) * 100);
}
const totalSessionTokens = inputTokens + outputTokens + cachedTokens;
// Calculate true session totals by reading transcript
interface SessionTotals {
input: number;
output: number;
cached: number;
}
function getSessionTotals(): SessionTotals {
const totals: SessionTotals = { input: 0, output: 0, cached: 0 };
try {
const transcriptPath = input.transcript_path;
if (existsSync(transcriptPath)) {
const content = readFileSync(transcriptPath, 'utf-8');
const lines = content.trim().split('\n');
for (const line of lines) {
try {
const entry = JSON.parse(line);
const usage = entry.message?.usage;
if (usage) {
totals.input += usage.input_tokens || 0;
totals.output += usage.output_tokens || 0;
totals.cached += (usage.cache_creation_input_tokens || 0) +
(usage.cache_read_input_tokens || 0);
}
} catch {
// Skip malformed lines
}
}
}
} catch {
// Ignore errors, return zeros
}
return totals;
}
const sessionTotals = getSessionTotals();
// Create colored progress bar (10 characters)
function createProgressBar(pct: number): string {
const filled = Math.round((pct / 100) * 10);
const empty = 10 - filled;
// Color based on usage level: green < 40%, yellow 40-60%, red > 60%
let barColor = c.brightGreen;
if (pct >= 40 && pct < 60) barColor = c.brightYellow;
if (pct >= 60) barColor = c.brightRed;
const pipes = barColor + '|'.repeat(filled) + c.reset;
const spaces = c.dim + '·'.repeat(empty) + c.reset;
return `${c.dim}[${c.reset}${pipes}${spaces}${c.dim}]${c.reset}`;
}
const contextBar = createProgressBar(pctUsed);
// Calculate session cost
const COST_PER_1M_INPUT = 3.00;
const COST_PER_1M_OUTPUT = 15.00;
const COST_PER_1M_CACHE_WRITE = 3.75;
const COST_PER_1M_CACHE_READ = 0.30;
const totalInput = input.context_window.total_input_tokens;
const totalOutput = input.context_window.total_output_tokens;
// Estimate cache tokens
let cacheWrite = 0;
let cacheRead = 0;
if (input.context_window.current_usage) {
const currentUsage = input.context_window.current_usage;
const currentTotal = currentUsage.input_tokens + currentUsage.cache_creation_input_tokens + currentUsage.cache_read_input_tokens;
if (currentTotal > 0) {
cacheWrite = Math.round(totalInput * (currentUsage.cache_creation_input_tokens / currentTotal));
cacheRead = Math.round(totalInput * (currentUsage.cache_read_input_tokens / currentTotal));
}
}
const regularInput = totalInput - cacheWrite - cacheRead;
const cost =
(regularInput / 1_000_000) * COST_PER_1M_INPUT +
(totalOutput / 1_000_000) * COST_PER_1M_OUTPUT +
(cacheWrite / 1_000_000) * COST_PER_1M_CACHE_WRITE +
(cacheRead / 1_000_000) * COST_PER_1M_CACHE_READ;
const costStr = `${c.green}$${cost.toFixed(4)}${c.reset}`;
// Calculate session time
function getSessionTime(): string {
try {
const transcriptPath = input.transcript_path;
if (existsSync(transcriptPath)) {
const content = readFileSync(transcriptPath, 'utf-8');
const lines = content.trim().split('\n');
if (lines.length > 0 && lines[0]) {
try {
const firstLine = JSON.parse(lines[0]);
if (firstLine.timestamp) {
const startTime = new Date(firstLine.timestamp).getTime();
if (!isNaN(startTime)) {
return formatDuration(Date.now() - startTime);
}
}
} catch {
// JSON parse failed, try stat
}
}
// Fallback: use file creation time
const stats = statSync(transcriptPath);
const startTime = stats.birthtime.getTime();
if (!isNaN(startTime)) {
return formatDuration(Date.now() - startTime);
}
}
} catch {
// Ignore errors
}
return '--:--';
}
function formatDuration(ms: number): string {
if (isNaN(ms) || ms < 0) return '--:--';
const hours = Math.floor(ms / 3600000);
const minutes = Math.floor((ms % 3600000) / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m ${seconds}s`;
}
}
const sessionTime = getSessionTime();
// Get project path display
function getProjectPath(): string {
const currentDir = input.workspace.current_dir;
const currentName = basename(currentDir);
// Get git root directory
let gitRoot: string | null = null;
try {
gitRoot = execSync('git rev-parse --show-toplevel 2>/dev/null', {
cwd: currentDir,
encoding: 'utf-8'
}).trim();
} catch {
// Not in a git repo
}
if (gitRoot) {
const rootName = basename(gitRoot);
// If at git root, just show root name
if (currentDir === gitRoot) {
return rootName;
}
// If in subdirectory, check depth
if (currentDir.startsWith(gitRoot + '/')) {
const relativePath = currentDir.slice(gitRoot.length + 1);
const depth = relativePath.split('/').length;
if (depth === 1) {
// Immediate subdirectory: root/dir
return `${rootName}/${c.brightCyan}${currentName}`;
} else {
// Deeper: root/../dir
return `${rootName}/${c.dim}../${c.reset}${c.brightCyan}${currentName}`;
}
}
}
// Fallback: just current directory name
return currentName;
}
const projectPath = `${c.brightCyan}${getProjectPath()}${c.reset}`;
// Get git branch
function getGitBranch(): string {
try {
const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
cwd: input.workspace.current_dir,
encoding: 'utf-8'
}).trim();
return branch;
} catch {
return '';
}
}
// Get git status
function getGitStatus(): string {
try {
const status = execSync('git status --porcelain 2>/dev/null', {
cwd: input.workspace.current_dir,
encoding: 'utf-8'
}).trim();
if (!status) {
return `${c.brightGreen}✓${c.reset}`;
}
const indicators: string[] = [];
const lines = status.split('\n');
let hasModified = false;
let hasAdded = false;
let hasDeleted = false;
let hasUntracked = false;
for (const line of lines) {
const code = line.substring(0, 2);
if (code.includes('M')) hasModified = true;
if (code.includes('A')) hasAdded = true;
if (code.includes('D')) hasDeleted = true;
if (code === '??') hasUntracked = true;
}
if (hasModified) indicators.push(`${c.yellow}●${c.reset}`);
if (hasAdded) indicators.push(`${c.green}+${c.reset}`);
if (hasDeleted) indicators.push(`${c.yellow}-${c.reset}`);
if (hasUntracked) indicators.push(`${c.gray}?${c.reset}`);
// Get line changes (+/-)
let additions = 0;
let deletions = 0;
try {
// Staged changes
const stagedDiff = execSync('git diff --cached --numstat 2>/dev/null', {
cwd: input.workspace.current_dir,
encoding: 'utf-8'
}).trim();
// Unstaged changes
const unstagedDiff = execSync('git diff --numstat 2>/dev/null', {
cwd: input.workspace.current_dir,
encoding: 'utf-8'
}).trim();
for (const diff of [stagedDiff, unstagedDiff]) {
if (diff) {
for (const line of diff.split('\n')) {
const [add, del] = line.split('\t');
if (add !== '-') additions += parseInt(add) || 0;
if (del !== '-') deletions += parseInt(del) || 0;
}
}
}
} catch {
// Ignore diff errors
}
if (additions > 0 || deletions > 0) {
const parts: string[] = [];
if (additions > 0) parts.push(`${c.green}+${additions}${c.reset}`);
if (deletions > 0) parts.push(`${c.brightRed}-${deletions}${c.reset}`);
indicators.push(parts.join(' '));
}
return indicators.join(' ');
} catch {
return '';
}
}
const gitBranch = getGitBranch();
const gitStatus = getGitStatus();
// Format model name
const modelName = `${c.magenta}${input.model.display_name}${c.reset}`;
// Format percentage with color
let pctColor = c.brightGreen;
if (pctUsed >= 40 && pctUsed < 60) pctColor = c.brightYellow;
if (pctUsed >= 60) pctColor = c.brightRed;
const pctStr = `${pctColor}${pctUsed}%${c.reset}`;
// Format token counts for progress bar (color coded based on usage)
const tokenInfo = `${c.dim}(${c.reset}${pctColor}${formatTokens(currentTokens)}${c.reset}${c.dim} / ${c.reset}${formatTokens(totalTokens)}${c.dim})${c.reset}`;
// Build line 1: project @ branch status [progress] pct (tokens) cost (time) model
let line1 = projectPath;
if (gitBranch) {
line1 += ` ${c.dim}@${c.reset} ${c.brightBlue}${gitBranch}${c.reset} ${gitStatus}`;
}
line1 += ` ${contextBar} ${pctStr} ${tokenInfo} ${costStr} ${c.dim}(${sessionTime})${c.reset} ${modelName}`;
// Build line 2: ↑ input (session) ↓ output (session) ↯ cached (session) Σ total (session)
const sessionTotalAll = sessionTotals.input + sessionTotals.output + sessionTotals.cached;
const fmt = (n: number) => formatTokens(n, 2);
const line2 = `${c.dim}↑${c.reset} ${c.cyan}${inputTokens.toLocaleString()}${c.reset} ${c.dim}(${fmt(sessionTotals.input)})${c.reset} ${c.dim}↓${c.reset} ${c.yellow}${outputTokens.toLocaleString()}${c.reset} ${c.dim}(${fmt(sessionTotals.output)})${c.reset} ${c.dim}↯${c.reset} ${c.blue}${cachedTokens.toLocaleString()}${c.reset} ${c.dim}(${fmt(sessionTotals.cached)})${c.reset} ${c.dim}Σ${c.reset} ${c.gray}${totalSessionTokens.toLocaleString()}${c.reset} ${c.dim}(${fmt(sessionTotalAll)})${c.reset}`;
console.log(line1);
console.log(line2);
console.log(''); // Extra spacing at bottom
@tarasyarema
Copy link
Author

Add to ~/.claude/settings.json

  "statusLine": {
    "type": "command",
    "command": "bun <path>/statusline.ts",
    "padding": 0
  },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment