Last active
September 2, 2026 22:05
-
-
Save hankbao/85f33e2782f0451590f7afcd97cd79fb to your computer and use it in GitHub Desktop.
Claude Code Statusline
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @if (@CodeSection == @Batch) @then | |
| @echo off | |
| rem Claude Code status line (bullet-train inspired, single-line layout). | |
| rem CMD/JScript hybrid port of the macOS bash version: | |
| rem https://gist.github.com/hankbao/85f33e2782f0451590f7afcd97cd79fb | |
| rem The batch header re-runs this same file through cscript's JScript | |
| rem engine (built into Windows), which parses the statusLine JSON payload | |
| rem from stdin and renders: | |
| rem time | dir | session | cost | rate limits | context remaining | model | effort | thinking | |
| rem The rate-limit group covers the 5h and 7d windows plus the per-model Fable | |
| rem weekly window, which is read from disk rather than stdin (see section 5b). | |
| rem | |
| rem Managed by the "statusline-setup" agent. Ask Claude to make further | |
| rem changes to this file rather than editing it by hand. | |
| setlocal | |
| rem Switch the console to UTF-8 so non-ASCII directory/session names in the | |
| rem JSON payload decode correctly, then restore the original codepage. | |
| set "_cp=" | |
| for /f "tokens=2 delims=:" %%a in ('chcp 2^>nul') do set "_cp=%%a" | |
| chcp 65001 >nul 2>nul | |
| cscript //nologo //e:jscript "%~f0" | |
| if defined _cp chcp %_cp% >nul 2>nul | |
| endlocal | |
| exit /b 0 | |
| @end | |
| // ---- JScript section (executed by cscript, skipped by cmd) ---- | |
| // ---- Helpers ---- | |
| // WSH JScript has no JSON.parse, so a guarded eval stands in for it. Both | |
| // inputs are local files written by Claude Code itself; well-formed JSON is | |
| // always a safe JS expression, and anything else throws into the catch. | |
| function parseJson(text) { | |
| if (!text) return null; | |
| if (text.charAt(0) === String.fromCharCode(65279)) text = text.substring(1); // UTF-8 BOM | |
| if (text.replace(/^\s+/, '').charAt(0) !== '{') return null; | |
| try { return eval('(' + text + ')'); } catch (e) { return null; } | |
| } | |
| // Null-safe nested lookup, so every segment below can degrade gracefully. | |
| function getFrom(obj, path) { | |
| var cur = obj; | |
| var parts = path.split('.'); | |
| for (var i = 0; i < parts.length; i++) { | |
| if (cur === null || cur === undefined || typeof cur !== 'object') return null; | |
| cur = cur[parts[i]]; | |
| } | |
| return (cur === undefined) ? null : cur; | |
| } | |
| // Read a UTF-8 file. ADODB.Stream is the only WSH reader that understands | |
| // UTF-8; FileSystemObject would decode via the ANSI codepage, where a CJK | |
| // byte pair can swallow a following backslash and corrupt the JSON. | |
| function readUtf8File(path) { | |
| try { | |
| var st = new ActiveXObject('ADODB.Stream'); | |
| st.Type = 2; // adTypeText | |
| st.Charset = 'utf-8'; | |
| st.Open(); | |
| st.LoadFromFile(path); | |
| var txt = st.ReadText(); | |
| st.Close(); | |
| return txt; | |
| } catch (e) { | |
| try { | |
| var fso = new ActiveXObject('Scripting.FileSystemObject'); | |
| var f = fso.OpenTextFile(path, 1); | |
| var t = f.ReadAll(); | |
| f.Close(); | |
| return t; | |
| } catch (e2) { return null; } | |
| } | |
| } | |
| // JScript's Date.parse predates ISO 8601, so parse resets_at by hand. | |
| // Returns whole epoch seconds, or null when the timestamp is unrecognisable. | |
| function parseIso8601Sec(s) { | |
| if (!s) return null; | |
| var m = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/ | |
| .exec(String(s)); | |
| if (!m) return null; | |
| var ms = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6], 0); | |
| var tz = m[7]; | |
| if (tz && tz !== 'Z') { | |
| // Unlike jq's fromdateiso8601 this reads the offset, so a non-UTC offset | |
| // yields a correct countdown instead of none. | |
| var sign = (tz.charAt(0) === '-') ? -1 : 1; | |
| var body = tz.substring(1).replace(':', ''); | |
| var mins = parseInt(body.substring(0, 2), 10) * 60 + parseInt(body.substring(2, 4), 10); | |
| ms -= sign * mins * 60000; | |
| } | |
| return Math.floor(ms / 1000); | |
| } | |
| // Coarse single-unit countdown: days, else hours, else minutes. | |
| function countdown(s) { | |
| if (s >= 86400) return Math.floor(s / 86400) + 'd'; | |
| if (s >= 3600) return Math.floor(s / 3600) + 'h'; | |
| return Math.floor(s / 60) + 'm'; | |
| } | |
| function isNum(v) { return v !== null && v !== '' && !isNaN(v); } | |
| function pad2(n) { return (n < 10 ? '0' : '') + n; } | |
| // ---- Payload from stdin ---- | |
| var raw = ''; | |
| try { raw = WScript.StdIn.ReadAll(); } catch (e) {} | |
| var data = parseJson(raw); | |
| function get(path) { return getFrom(data, path); } | |
| // ---- Dim ANSI palette (the status line is rendered dimmed by the terminal) ---- | |
| var RESET = '\x1b[0m'; | |
| var DIM = '\x1b[2m'; | |
| var C_TIME = '\x1b[2;37m'; // dim white | |
| var C_DIR = '\x1b[2;34m'; // dim blue | |
| var C_SESSION = '\x1b[2;35m'; // dim magenta | |
| var C_MODEL = '\x1b[2;32m'; // dim green | |
| var C_COST = '\x1b[2;33m'; // dim yellow | |
| var C_LIMITS = '\x1b[2;31m'; // dim red | |
| var C_EFFORT = '\x1b[2;36m'; // dim cyan | |
| var C_THINK = '\x1b[2;36m'; // dim cyan | |
| var C_CTX = '\x1b[2;32m'; // dim green | |
| var SEP = DIM + ' | ' + RESET; | |
| var segments = []; | |
| // ---- 1. Time (wall clock, not from the JSON payload) ---- | |
| var now = new Date(); | |
| segments.push(C_TIME + pad2(now.getHours()) + ':' + pad2(now.getMinutes()) + RESET); | |
| // ---- 2. Directory (basename of the current working directory) ---- | |
| var dirPath = get('workspace.current_dir'); | |
| if (!dirPath) dirPath = get('cwd'); | |
| if (dirPath) { | |
| var parts = String(dirPath).replace(/[\\\/]+$/, '').split(/[\\\/]/); | |
| var dirName = parts[parts.length - 1]; | |
| if (!dirName) dirName = String(dirPath); | |
| segments.push(C_DIR + dirName + RESET); | |
| } | |
| // ---- 3. Session (session_name, falling back to a short session_id prefix) ---- | |
| var sessionLabel = get('session_name'); | |
| if (!sessionLabel) { | |
| var sid = get('session_id'); | |
| if (sid) sessionLabel = String(sid).substring(0, 8); | |
| } | |
| if (sessionLabel) segments.push(C_SESSION + sessionLabel + RESET); | |
| // ---- 4. Session cost (USD) ---- | |
| var cost = get('cost.total_cost_usd'); | |
| if (isNum(cost)) segments.push(C_COST + '$' + Number(cost).toFixed(2) + RESET); | |
| // ---- 5. Rate limits (Claude.ai subscription usage, 5h / 7d windows) ---- | |
| var limitsParts = []; | |
| var fiveHour = get('rate_limits.five_hour.used_percentage'); | |
| if (isNum(fiveHour)) limitsParts.push('5h ' + Math.round(fiveHour) + '%'); | |
| var sevenDay = get('rate_limits.seven_day.used_percentage'); | |
| if (isNum(sevenDay)) limitsParts.push('7d ' + Math.round(sevenDay) + '%'); | |
| // ---- 5b. Fable weekly window (absent from the status line payload) ---- | |
| // Claude Code only ever emits five_hour / seven_day in .rate_limits, so the | |
| // per-model weekly bucket -- internally "weekly_scoped", labelled "Fable limit" | |
| // -- cannot be reached from stdin at all. It is however persisted verbatim in | |
| // ~/.claude.json under .cachedUsageUtilization, which is the cached | |
| // /api/oauth/usage response that opening /usage refreshes (at most every 5min). | |
| // Strictly read-only: Claude Code rewrites that file constantly. | |
| // | |
| // A "~" prefix marks a reading older than Claude Code's own 1h cache TTL, so a | |
| // stale percentage is never passed off as current -- the countdown stays exact. | |
| var cached = null; | |
| try { | |
| var homeDir = new ActiveXObject('WScript.Shell').ExpandEnvironmentStrings('%USERPROFILE%'); | |
| if (homeDir && homeDir.charAt(0) !== '%') { | |
| cached = getFrom(parseJson(readUtf8File(homeDir + '\\.claude.json')), | |
| 'cachedUsageUtilization'); | |
| } | |
| } catch (e) { cached = null; } | |
| if (cached) { | |
| var limits = getFrom(cached, 'utilization.limits'); | |
| var fable = null; | |
| if (limits && typeof limits.length === 'number') { | |
| for (var i = 0; i < limits.length; i++) { | |
| var dn = getFrom(limits[i], 'scope.model.display_name'); | |
| if (limits[i] && limits[i].kind === 'weekly_scoped' && | |
| String(dn === null ? '' : dn).toLowerCase() === 'fable') { | |
| fable = limits[i]; | |
| break; | |
| } | |
| } | |
| } | |
| if (fable && typeof fable.percent === 'number') { | |
| var nowMs = now.getTime(); | |
| var stale = (typeof cached.fetchedAtMs === 'number' && | |
| (nowMs - cached.fetchedAtMs) > 3600000) ? '~' : ''; | |
| var cdown = ''; | |
| var resetSec = parseIso8601Sec(fable.resets_at); | |
| if (resetSec !== null) { | |
| var d = Math.floor(resetSec - nowMs / 1000); | |
| if (d > 0) cdown = ' ' + countdown(d); | |
| } | |
| limitsParts.push('fable ' + stale + Math.floor(fable.percent) + '%' + cdown); | |
| } | |
| } | |
| if (limitsParts.length) segments.push(C_LIMITS + limitsParts.join(' | ') + RESET); | |
| // ---- 6. Context window remaining ---- | |
| var ctxRemaining = get('context_window.remaining_percentage'); | |
| if (isNum(ctxRemaining)) segments.push(C_CTX + 'ctx ' + Math.round(ctxRemaining) + '%' + RESET); | |
| // ---- 7. Model display name ---- | |
| var modelName = get('model.display_name'); | |
| if (modelName) segments.push(C_MODEL + modelName + RESET); | |
| // ---- 8. Effort level ---- | |
| var effort = get('effort.level'); | |
| if (effort) segments.push(C_EFFORT + 'eff:' + effort + RESET); | |
| // ---- 9. Thinking on/off ---- | |
| var thinking = get('thinking.enabled'); | |
| if (thinking === true) segments.push(C_THINK + 'think:on' + RESET); | |
| else if (thinking === false) segments.push(C_THINK + 'think:off' + RESET); | |
| var line = segments.join(SEP); | |
| if (line) WScript.StdOut.Write(line + '\n'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Claude Code status line (bullet-train inspired, single-line layout). | |
| # Windows/PowerShell port of the macOS bash version: | |
| # https://gist.github.com/hankbao/85f33e2782f0451590f7afcd97cd79fb | |
| # Reads the statusLine JSON payload from stdin and renders: | |
| # time | dir | session | cost | rate limits | context remaining | model | effort | thinking | |
| # The rate-limit group covers the 5h and 7d windows plus the per-model Fable | |
| # weekly window, which is read from disk rather than stdin (see section 5b). | |
| # | |
| # Managed by the "statusline-setup" agent. Ask Claude to make further | |
| # changes to this file rather than editing it by hand. | |
| $ErrorActionPreference = 'SilentlyContinue' | |
| # Read stdin as UTF-8 regardless of the console codepage, so non-ASCII | |
| # directory/session names survive the round trip. | |
| $reader = New-Object IO.StreamReader([Console]::OpenStandardInput(), [Text.Encoding]::UTF8) | |
| $raw = $reader.ReadToEnd() | |
| $json = $null | |
| try { $json = $raw | ConvertFrom-Json } catch {} | |
| # ---- Dim ANSI palette (the status line is rendered dimmed by the terminal) ---- | |
| $E = [char]27 | |
| $RESET = "$E[0m" | |
| $DIM = "$E[2m" | |
| $C_TIME = "$E[2;37m" # dim white | |
| $C_DIR = "$E[2;34m" # dim blue | |
| $C_SESSION = "$E[2;35m" # dim magenta | |
| $C_MODEL = "$E[2;32m" # dim green | |
| $C_COST = "$E[2;33m" # dim yellow | |
| $C_LIMITS = "$E[2;31m" # dim red | |
| $C_EFFORT = "$E[2;36m" # dim cyan | |
| $C_THINK = "$E[2;36m" # dim cyan | |
| $C_CTX = "$E[2;32m" # dim green | |
| $SEP = "${DIM} | ${RESET}" | |
| $inv = [Globalization.CultureInfo]::InvariantCulture | |
| # ConvertFrom-Json hands back Int32/Int64/Double/Decimal for JSON numbers; | |
| # this stands in for jq's `type == "number"` guard. | |
| function Test-JsonNumber($v) { | |
| return ($v -is [int] -or $v -is [long] -or $v -is [double] -or | |
| $v -is [decimal] -or $v -is [single] -or $v -is [short] -or $v -is [byte]) | |
| } | |
| # Coarse single-unit countdown: days, else hours, else minutes. | |
| function Format-Countdown($seconds) { | |
| if ($seconds -ge 86400) { return "$([Math]::Floor($seconds / 86400))d" } | |
| if ($seconds -ge 3600) { return "$([Math]::Floor($seconds / 3600))h" } | |
| return "$([Math]::Floor($seconds / 60))m" | |
| } | |
| $segments = @() | |
| # ---- 1. Time (wall clock, not from the JSON payload) ---- | |
| $segments += "${C_TIME}$(Get-Date -Format HH:mm)${RESET}" | |
| # ---- 2. Directory (basename of the current working directory) ---- | |
| $dirPath = $json.workspace.current_dir | |
| if (-not $dirPath) { $dirPath = $json.cwd } | |
| if ($dirPath) { | |
| $dirName = ("$dirPath" -replace '[\\/]+$', '' -split '[\\/]')[-1] | |
| if (-not $dirName) { $dirName = "$dirPath" } | |
| $segments += "${C_DIR}${dirName}${RESET}" | |
| } | |
| # ---- 3. Session (session_name, falling back to a short session_id prefix) ---- | |
| $sessionLabel = $json.session_name | |
| if (-not $sessionLabel) { | |
| $sid = $json.session_id | |
| if ($sid) { $sessionLabel = "$sid".Substring(0, [Math]::Min(8, "$sid".Length)) } | |
| } | |
| if ($sessionLabel) { $segments += "${C_SESSION}${sessionLabel}${RESET}" } | |
| # ---- 4. Session cost (USD) ---- | |
| $cost = $json.cost.total_cost_usd | |
| if ($null -ne $cost) { | |
| $segments += "${C_COST}`$$(([double]$cost).ToString('0.00', $inv))${RESET}" | |
| } | |
| # ---- 5. Rate limits (Claude.ai subscription usage, 5h / 7d windows) ---- | |
| $limitsParts = @() | |
| $fiveHour = $json.rate_limits.five_hour.used_percentage | |
| if ($null -ne $fiveHour) { $limitsParts += "5h $([Math]::Round([double]$fiveHour))%" } | |
| $sevenDay = $json.rate_limits.seven_day.used_percentage | |
| if ($null -ne $sevenDay) { $limitsParts += "7d $([Math]::Round([double]$sevenDay))%" } | |
| # ---- 5b. Fable weekly window (absent from the status line payload) ---- | |
| # Claude Code only ever emits five_hour / seven_day in .rate_limits, so the | |
| # per-model weekly bucket -- internally "weekly_scoped", labelled "Fable limit" | |
| # -- cannot be reached from stdin at all. It is however persisted verbatim in | |
| # ~/.claude.json under .cachedUsageUtilization, which is the cached | |
| # /api/oauth/usage response that opening /usage refreshes (at most every 5min). | |
| # Strictly read-only: Claude Code rewrites that file constantly, hence the | |
| # shared-write handle below (an exclusive read would fail intermittently). | |
| # | |
| # A "~" prefix marks a reading older than Claude Code's own 1h cache TTL, so a | |
| # stale percentage is never passed off as current -- the countdown stays exact. | |
| $cached = $null | |
| $homeDir = $HOME | |
| if (-not $homeDir) { $homeDir = $env:USERPROFILE } | |
| $claudeJson = Join-Path $homeDir '.claude.json' | |
| if ($homeDir -and (Test-Path -LiteralPath $claudeJson)) { | |
| try { | |
| $fs = [IO.File]::Open($claudeJson, [IO.FileMode]::Open, [IO.FileAccess]::Read, | |
| [IO.FileShare]::ReadWrite) | |
| try { | |
| $cachedRaw = (New-Object IO.StreamReader($fs, [Text.Encoding]::UTF8)).ReadToEnd() | |
| } finally { $fs.Dispose() } | |
| $cached = ($cachedRaw | ConvertFrom-Json).cachedUsageUtilization | |
| } catch { $cached = $null } | |
| } | |
| if ($cached) { | |
| $fable = $null | |
| foreach ($lim in @($cached.utilization.limits)) { | |
| if ($lim.kind -eq 'weekly_scoped' -and | |
| "$($lim.scope.model.display_name)".ToLowerInvariant() -eq 'fable') { | |
| $fable = $lim | |
| break | |
| } | |
| } | |
| if ($fable -and (Test-JsonNumber $fable.percent)) { | |
| $nowMs = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() | |
| $stale = '' | |
| if ((Test-JsonNumber $cached.fetchedAtMs) -and | |
| ($nowMs - [double]$cached.fetchedAtMs) -gt 3600000) { $stale = '~' } | |
| # resets_at looks like 2026-09-06T21:00:00.512+00:00. Unlike jq's | |
| # fromdateiso8601 the .NET parser reads the offset directly, so a non-UTC | |
| # offset yields a correct countdown instead of none. | |
| # NB: not $reset -- PowerShell variable names are case-insensitive, so that | |
| # would silently clobber the $RESET ANSI escape used by every segment. | |
| $cdown = '' | |
| $resetAt = [DateTimeOffset]::MinValue | |
| if ($fable.resets_at -and [DateTimeOffset]::TryParse( | |
| "$($fable.resets_at)", $inv, | |
| [Globalization.DateTimeStyles]::AssumeUniversal -bor | |
| [Globalization.DateTimeStyles]::AdjustToUniversal, | |
| [ref]$resetAt)) { | |
| $d = [Math]::Floor($resetAt.ToUnixTimeSeconds() - ($nowMs / 1000)) | |
| if ($d -gt 0) { $cdown = ' ' + (Format-Countdown $d) } | |
| } | |
| $limitsParts += "fable ${stale}$([Math]::Floor([double]$fable.percent))%${cdown}" | |
| } | |
| } | |
| if ($limitsParts.Count -gt 0) { | |
| $segments += "${C_LIMITS}$($limitsParts -join ' | ')${RESET}" | |
| } | |
| # ---- 6. Context window remaining ---- | |
| $ctxRemaining = $json.context_window.remaining_percentage | |
| if ($null -ne $ctxRemaining) { | |
| $segments += "${C_CTX}ctx $([Math]::Round([double]$ctxRemaining))%${RESET}" | |
| } | |
| # ---- 7. Model display name ---- | |
| $modelName = $json.model.display_name | |
| if ($modelName) { $segments += "${C_MODEL}${modelName}${RESET}" } | |
| # ---- 8. Effort level ---- | |
| $effort = $json.effort.level | |
| if ($effort) { $segments += "${C_EFFORT}eff:${effort}${RESET}" } | |
| # ---- 9. Thinking on/off ---- | |
| $thinking = $json.thinking.enabled | |
| if ($thinking -eq $true) { $segments += "${C_THINK}think:on${RESET}" } | |
| elseif ($thinking -eq $false) { $segments += "${C_THINK}think:off${RESET}" } | |
| $line = $segments -join $SEP | |
| if ($line) { | |
| # Write raw UTF-8 bytes so the output encoding never mangles the line. | |
| $bytes = [Text.Encoding]::UTF8.GetBytes($line + "`n") | |
| $stdout = [Console]::OpenStandardOutput() | |
| $stdout.Write($bytes, 0, $bytes.Length) | |
| $stdout.Flush() | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # | |
| # Claude Code status line (bullet-train inspired, single-line layout). | |
| # Reads the statusLine JSON payload from stdin and renders: | |
| # time | dir | session | cost | rate limits | context remaining | model | effort | thinking | |
| # The rate-limit group covers the 5h and 7d windows plus the per-model Fable | |
| # weekly window, which is read from disk rather than stdin (see section 6b). | |
| # | |
| # Managed by the "statusline-setup" agent. Ask Claude to make further | |
| # changes to this file rather than editing it by hand. | |
| input=$(cat) | |
| # jq helper: returns an empty string (never the literal "null") when a | |
| # field is missing or null, so every segment below can degrade gracefully. | |
| jget() { | |
| printf '%s' "$input" | jq -r "$1 // empty" 2>/dev/null | |
| } | |
| # ---- Dim ANSI palette (the status line is rendered dimmed by the terminal) ---- | |
| RESET=$'\033[0m' | |
| DIM=$'\033[2m' | |
| C_TIME=$'\033[2;37m' # dim white | |
| C_DIR=$'\033[2;34m' # dim blue | |
| C_SESSION=$'\033[2;35m' # dim magenta | |
| C_MODEL=$'\033[2;32m' # dim green | |
| C_COST=$'\033[2;33m' # dim yellow | |
| C_LIMITS=$'\033[2;31m' # dim red | |
| C_EFFORT=$'\033[2;36m' # dim cyan | |
| C_THINK=$'\033[2;36m' # dim cyan | |
| C_CTX=$'\033[2;32m' # dim green | |
| SEP="${DIM} | ${RESET}" | |
| # Join all non-empty segments with a dim separator. | |
| join() { | |
| local out="" | |
| for seg in "$@"; do | |
| [ -z "$seg" ] && continue | |
| if [ -z "$out" ]; then | |
| out="$seg" | |
| else | |
| out="${out}${SEP}${seg}" | |
| fi | |
| done | |
| printf '%s' "$out" | |
| } | |
| # ---- 1. Time (wall clock, not from the JSON payload) ---- | |
| seg_time="${C_TIME}$(date +%H:%M)${RESET}" | |
| # ---- 2. Directory (basename of the current working directory) ---- | |
| dir_path=$(jget '.workspace.current_dir') | |
| [ -z "$dir_path" ] && dir_path=$(jget '.cwd') | |
| seg_dir="" | |
| [ -n "$dir_path" ] && seg_dir="${C_DIR}$(basename "$dir_path")${RESET}" | |
| # ---- 3. Session (session_name, falling back to a short session_id prefix) ---- | |
| session_label=$(jget '.session_name') | |
| if [ -z "$session_label" ]; then | |
| sid=$(jget '.session_id') | |
| [ -n "$sid" ] && session_label="${sid:0:8}" | |
| fi | |
| seg_session="" | |
| [ -n "$session_label" ] && seg_session="${C_SESSION}${session_label}${RESET}" | |
| # ---- 4. Model display name ---- | |
| model_name=$(jget '.model.display_name') | |
| seg_model="" | |
| [ -n "$model_name" ] && seg_model="${C_MODEL}${model_name}${RESET}" | |
| # ---- 5. Session cost (USD) ---- | |
| cost=$(jget '.cost.total_cost_usd') | |
| seg_cost="" | |
| if [ -n "$cost" ]; then | |
| cost_fmt=$(printf '$%.2f' "$cost" 2>/dev/null) | |
| [ -n "$cost_fmt" ] && seg_cost="${C_COST}${cost_fmt}${RESET}" | |
| fi | |
| # ---- 6. Rate limits (Claude.ai subscription usage, 5h / 7d windows) ---- | |
| five_hour=$(jget '.rate_limits.five_hour.used_percentage') | |
| seven_day=$(jget '.rate_limits.seven_day.used_percentage') | |
| limits_str="" | |
| if [ -n "$five_hour" ]; then | |
| five_fmt=$(printf '%.0f' "$five_hour" 2>/dev/null) | |
| [ -n "$five_fmt" ] && limits_str="5h ${five_fmt}%" | |
| fi | |
| if [ -n "$seven_day" ]; then | |
| seven_fmt=$(printf '%.0f' "$seven_day" 2>/dev/null) | |
| if [ -n "$seven_fmt" ]; then | |
| if [ -n "$limits_str" ]; then | |
| limits_str="${limits_str} | 7d ${seven_fmt}%" | |
| else | |
| limits_str="7d ${seven_fmt}%" | |
| fi | |
| fi | |
| fi | |
| # ---- 6b. Fable weekly window (absent from the status line payload) ---- | |
| # Claude Code only ever emits five_hour / seven_day in .rate_limits, so the | |
| # per-model weekly bucket -- internally "seven_day_overage_included", labelled | |
| # "Fable limit" -- cannot be reached from stdin at all. It is however persisted | |
| # verbatim in ~/.claude.json under .cachedUsageUtilization, which is the cached | |
| # /api/oauth/usage response that opening /usage refreshes (at most every 5min). | |
| # Strictly read-only: Claude Code rewrites that file constantly. | |
| # | |
| # percent is already 0-100. resets_at looks like 2026-09-06T21:00:00.512+00:00, | |
| # which fromdateiso8601 rejects, hence the two subs; a non-UTC offset would fall | |
| # through to "no countdown" rather than render a wrong one. A "~" prefix marks a | |
| # reading older than Claude Code's own 1h cache TTL, so a stale percentage is | |
| # never passed off as current -- the countdown stays exact either way. | |
| fable_str="" | |
| if [ -r "$HOME/.claude.json" ]; then | |
| fable_str=$(jq -r ' | |
| def cd($s): | |
| if $s >= 86400 then "\($s/86400|floor)d" | |
| elif $s >= 3600 then "\($s/3600|floor)h" | |
| else "\($s/60|floor)m" end; | |
| (.cachedUsageUtilization // {}) as $c | |
| | ($c.utilization.limits // [] | |
| | map(select(.kind == "weekly_scoped" | |
| and ((.scope.model.display_name // "") | ascii_downcase) == "fable")) | |
| | first) as $f | |
| | if $f == null or ($f.percent | type) != "number" then empty | |
| else | |
| (if ($c.fetchedAtMs | type) == "number" and (now * 1000 - $c.fetchedAtMs) > 3600000 | |
| then "~" else "" end) as $stale | |
| | (try ($f.resets_at | sub("\\.[0-9]+"; "") | sub("\\+00:00$"; "Z") | fromdateiso8601) | |
| catch null) as $reset | |
| | (if $reset == null then "" | |
| else (($reset - now) | floor) as $d | |
| | if $d > 0 then " " + cd($d) else "" end | |
| end) as $cdown | |
| | "fable \($stale)\($f.percent | floor)%\($cdown)" | |
| end' "$HOME/.claude.json" 2>/dev/null) | |
| fi | |
| if [ -n "$fable_str" ]; then | |
| if [ -n "$limits_str" ]; then | |
| limits_str="${limits_str} | ${fable_str}" | |
| else | |
| limits_str="$fable_str" | |
| fi | |
| fi | |
| seg_limits="" | |
| [ -n "$limits_str" ] && seg_limits="${C_LIMITS}${limits_str}${RESET}" | |
| # ---- 7. Effort level ---- | |
| effort=$(jget '.effort.level') | |
| seg_effort="" | |
| [ -n "$effort" ] && seg_effort="${C_EFFORT}eff:${effort}${RESET}" | |
| # ---- 8. Thinking on/off ---- | |
| thinking=$(jget '.thinking.enabled') | |
| seg_think="" | |
| if [ "$thinking" = "true" ]; then | |
| seg_think="${C_THINK}think:on${RESET}" | |
| elif [ "$thinking" = "false" ]; then | |
| seg_think="${C_THINK}think:off${RESET}" | |
| fi | |
| # ---- 9. Context window remaining ---- | |
| ctx_remaining=$(jget '.context_window.remaining_percentage') | |
| seg_ctx="" | |
| if [ -n "$ctx_remaining" ]; then | |
| ctx_fmt=$(printf '%.0f' "$ctx_remaining" 2>/dev/null) | |
| [ -n "$ctx_fmt" ] && seg_ctx="${C_CTX}ctx ${ctx_fmt}%${RESET}" | |
| fi | |
| line=$(join "$seg_time" "$seg_dir" "$seg_session" "$seg_cost" "$seg_limits" "$seg_ctx" "$seg_model" "$seg_effort" "$seg_think") | |
| [ -n "$line" ] && printf '%s\n' "$line" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment