|
#!/bin/bash |
|
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
|
# Claude Code โ status line (three rows) |
|
# |
|
# Row 1: ๐ฅ๏ธ/๐/๐ hostname | ๐ path | ๐ฟ git branch, upstream, ahead/behind, working-tree state | ๐ PR |
|
# Row 2: ๐ค model โฎ ๐ช effort | ๐ง context โฎ ๐ง memory | โป๏ธ autocompact | ๐ battery |
|
# Row 3: โฐ clock โฎ โฒ๏ธ 5h reset โฎ ๐
7d reset date | โฒ๏ธ 5h quota โฎ ๐ฅ burn | ๐
7d quota โฎ ๐ฅ burn โฎ per-model |
|
# (โฒ๏ธ = the 5-hour window, ๐
= the 7-day window โ each shown as a reset time and as usage%) |
|
# Row 4: โจ๏ธ static keybinding hints, e.g. "Control+U: Clear | Hold Space: Voice" (SHOW_HINTS / HINTS_TEXT) |
|
# |
|
# Dependencies: /bin/bash 3.2 (macOS stock), jq, git; curl only for the |
|
# per-model quotas segment. |
|
# Data sources: |
|
# 1. stdin JSON from Claude Code (always fresh, no network); effort level |
|
# prefers the native stdin field and falls back to settings.json. |
|
# 2. https://api.anthropic.com/api/oauth/usage โ UNDOCUMENTED community- |
|
# discovered endpoint: per-model weekly quotas, plus account-fresh 5h/7d |
|
# percentages so another session's burn is never understated here. |
|
# Refreshed in the background (never blocks a render), cached, mkdir-locked. |
|
# 3. usage-history.log โ shared sample log powering sliding-window burn |
|
# rates (true current pace across sessions); prunes itself past 8 days. |
|
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
|
|
|
MEM_LIMIT=400000 # ๐ง memory budget (tokens) |
|
SETTINGS_FILE="$HOME/.claude/settings.json" |
|
|
|
# Toggles and paths (env-overridable: "SHOW_PR=0 ~/.claude/statusline.sh") |
|
: "${SHOW_PR:=1}" # ๐ PR number + review state on row 1 |
|
: "${SHOW_WEEK_DATE:=1}" # ๐
7-day reset date next to the โฒ๏ธ 5h reset |
|
: "${SHOW_WEEKLY_MODELS:=1}" # per-model 7-day quotas appended to ๐ง memory on row 2 |
|
: "${SHOW_PIPE_STYLE:=1}" # bold bright-blue | separators; muted-blue โฎ sub-separators |
|
: "${PIPE_FG:=39}" # 256-color foreground for the | glyph (bright blue) |
|
: "${PIPE_BG:=0}" # 256-color background for the | glyph (black) |
|
: "${SUBSEP_FG:=189}" # 256-color fg for the โฎ glyph (near-white, faint blue); "none" = leave the terminal default (exact font match) |
|
: "${SHOW_HINTS:=1}" # 4th row: static keybinding hints |
|
# HINTS_TEXT keyboard shortcuts follow the Google developer-docs style: spell |
|
# out modifier keys (Control, not Ctrl/^), join with +, uppercase letter keys, |
|
# and spell out confusing chars (comma, hyphen, period, plus). |
|
: "${HINTS_TEXT:=โจ๏ธ Control+U: Clear | Hold Space: Voice}" |
|
# usage-API refresh + account-fresh quota merge. Defaults off inside a Docker |
|
# Sandbox: there, .credentials.json holds a proxy-managed sentinel (a short, |
|
# format-shaped placeholder) rather than a usable token โ the real credential is |
|
# injected host-side by the sandbox proxy and never enters the VM โ so the API |
|
# can never authenticate and the refresh is a doomed curl every REFRESH_INTERVAL. |
|
# Nothing is lost: stdin still carries real 5h/7d percentages, and |
|
# weekly_models_part renders from whatever the host left in USAGE_CACHE (mount |
|
# it into the sandbox to keep that row alive). An explicit env value still wins. |
|
if [ -n "$IS_SANDBOX" ]; then |
|
: "${USE_USAGE_API:=0}" |
|
else |
|
: "${USE_USAGE_API:=1}" |
|
fi |
|
: "${REFRESH_INTERVAL:=60}" # seconds between usage-API refreshes |
|
: "${USAGE_CACHE:=$HOME/.claude/usage-cache.json}" |
|
: "${HISTORY_FILE:=$HOME/.claude/usage-history.log}" |
|
CREDENTIALS_FILE="$HOME/.claude/.credentials.json" |
|
KEYCHAIN_SERVICE="Claude Code-credentials" # macOS Keychain entry holding the OAuth blob |
|
LOCK_DIR="$HOME/.claude/statusline-refresh.lockdir" |
|
|
|
# โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
|
|
|
fmt_dur() { |
|
local s=$1 |
|
local d=$((s / 86400)) h=$(((s % 86400) / 3600)) m=$(((s % 3600) / 60)) out="" |
|
[ "$d" -gt 0 ] && out="${d}d " |
|
[ "$h" -gt 0 ] && out="${out}${h}h " |
|
[ "$m" -gt 0 ] && out="${out}${m}m" |
|
out="${out% }" |
|
[ -z "$out" ] && out="<1m" |
|
echo "$out" |
|
} |
|
|
|
# Control bytes in untrusted strings (branch/model names) could carry terminal |
|
# escape sequences โ strip them before display. |
|
sanitize() { |
|
printf '%s' "$1" | tr -d '[:cntrl:]' |
|
} |
|
|
|
# ๐ข/โ ๏ธ/๐จ for a percentage at the standard 50/80 thresholds. |
|
usage_dot() { |
|
if [ "$1" -ge 80 ]; then echo "๐จ" |
|
elif [ "$1" -ge 50 ]; then echo "โ ๏ธ" |
|
else echo "๐ข" |
|
fi |
|
} |
|
|
|
# ISO 8601 โ epoch. BSD date first (macOS), GNU fallback. The usage API |
|
# returns +00:00 offsets, so parsing the stripped core as UTC is correct; |
|
# other offsets are left to the GNU path. |
|
iso_to_epoch() { |
|
local iso="$1" core |
|
core="${iso%%.*}" |
|
core="${core%%+*}" |
|
core="${core%Z}" |
|
date -juf "%Y-%m-%dT%H:%M:%S" "$core" +%s 2>/dev/null && return |
|
date -d "$iso" +%s 2>/dev/null |
|
} |
|
|
|
# merge_quota <stdin_pct> <stdin_reset> <cache_pct> <cache_reset> |
|
# Echoes "pct|reset". stdin reflects this session's last API response; the |
|
# cache can be fresher when another session is doing the burning. A cache |
|
# from a newer window wins outright; the same window (resets within 120s) |
|
# takes the higher percentage โ usage within a window only grows. |
|
merge_quota() { |
|
local sp="$1" sr="$2" cp="$3" cr="$4" d spi cpi |
|
if [ -z "$cp" ] || [ -z "$cr" ]; then echo "${sp}|${sr}"; return; fi |
|
if [ -z "$sp" ] || [ -z "$sr" ]; then echo "${cp}|${cr}"; return; fi |
|
d=$((cr - sr)) |
|
if [ "$d" -gt 120 ]; then |
|
echo "${cp}|${cr}" |
|
elif [ "$d" -lt -120 ]; then |
|
echo "${sp}|${sr}" |
|
else |
|
spi=$(printf '%.0f' "$sp" 2>/dev/null) |
|
cpi=$(printf '%.0f' "$cp" 2>/dev/null) |
|
if [ "${cpi:-0}" -gt "${spi:-0}" ]; then |
|
echo "${cp}|${sr}" |
|
else |
|
echo "${sp}|${sr}" |
|
fi |
|
fi |
|
} |
|
|
|
# quota_segment <emoji> <raw_pct> <reset_epoch> <window> <min_burn_elapsed> |
|
# <burn_unit> <burn_suffix> <burn_warn> <burn_crit> <min_data_label> |
|
# <hist_lookback> <hist_min_span> <hist_pct_col> <hist_reset_col> |
|
# Renders "emoji pct% (time-left) dot [โฎ ๐ฅ rate (eta) dot]". The burn rate |
|
# prefers a sliding window over the shared sample history โ the true current |
|
# pace, counting every session โ and falls back to the whole-window average |
|
# until enough history spans hist_min_span. A reset moment already in the |
|
# past means the window rolled over, so the stale % is really 0. |
|
quota_segment() { |
|
local emoji="$1" raw="$2" reset="$3" window="$4" min_elapsed="$5" |
|
local unit="$6" suffix="$7" warn="$8" crit="$9" min_label="${10}" |
|
local lb="${11}" min_span="${12}" pcol="${13}" rcol="${14}" |
|
local pct left="--" diff=0 dot burn="" elapsed rate eta efmt edot |
|
local sample ts0 p0 span dpct |
|
pct=$(printf '%.0f' "$raw" 2>/dev/null) || pct=0 |
|
if [ -n "$reset" ]; then |
|
diff=$((reset - NOW)) |
|
if [ "$diff" -le 0 ]; then diff=0; pct=0; fi |
|
left=$(fmt_dur $diff) |
|
fi |
|
dot=$(usage_dot "$pct") |
|
|
|
# Sliding-window burn: oldest history sample within the lookback that |
|
# belongs to the same quota window (reset within 120s). |
|
if [ -n "$lb" ] && [ -n "$reset" ] && [ -f "$HISTORY_FILE" ] \ |
|
&& [ "$pct" -gt 0 ] && [ "$pct" -lt 100 ]; then |
|
sample=$(awk -v now="$NOW" -v lb="$lb" -v pc="$pcol" -v rc="$rcol" -v rs="$reset" \ |
|
'$1+0 >= now-lb && $pc != "-" && $rc != "-" && ($rc-rs) <= 120 && ($rc-rs) >= -120 { print $1, $pc; exit }' \ |
|
"$HISTORY_FILE" 2>/dev/null) |
|
if [ -n "$sample" ]; then |
|
ts0=${sample%% *} |
|
p0=${sample##* } |
|
span=$((NOW - ts0)) |
|
dpct=$((pct - p0)) |
|
if [ "$span" -ge "$min_span" ] && [ "$dpct" -ge 0 ]; then |
|
rate=$((dpct * unit / span)) |
|
if [ "$dpct" -gt 0 ]; then |
|
efmt=$(fmt_dur $(((100 - pct) * span / dpct))) |
|
else |
|
efmt="--" |
|
fi |
|
if [ "$rate" -ge "$crit" ]; then edot="๐จ" |
|
elif [ "$rate" -ge "$warn" ]; then edot="โ ๏ธ" |
|
else edot="๐ข" |
|
fi |
|
burn=" โฎ ๐ฅ ${rate}${suffix} (${efmt}) ${edot}" |
|
fi |
|
fi |
|
fi |
|
|
|
# Fallback: average since the window opened (the original behavior). |
|
elapsed=$((window - diff)) |
|
if [ -z "$burn" ] && [ "$pct" -gt 0 ] && [ "$pct" -lt 100 ] && [ "$elapsed" -gt "$min_elapsed" ]; then |
|
rate=$((pct * unit / elapsed)) |
|
eta=$(((100 - pct) * elapsed / pct)) |
|
efmt=$(fmt_dur $eta) |
|
if [ "$rate" -ge "$crit" ]; then edot="๐จ" |
|
elif [ "$rate" -ge "$warn" ]; then edot="โ ๏ธ" |
|
else edot="๐ข" |
|
fi |
|
burn=" โฎ ๐ฅ ${rate}${suffix} (${efmt}) ${edot}" |
|
elif [ -z "$burn" ] && [ "$pct" -gt 0 ] && [ "$pct" -lt 100 ] && [ "$elapsed" -le "$min_elapsed" ]; then |
|
burn=" โฎ ๐ฅ ${min_label} โณ" |
|
fi |
|
echo "${emoji} ${pct}% (${left}) ${dot}${burn}" |
|
} |
|
|
|
# โโ Per-model weekly quotas (OAuth usage API) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
|
|
|
file_mtime() { |
|
local m |
|
# GNU/BusyBox FIRST, and the order is load-bearing: on those `stat -f` means |
|
# FILESYSTEM stat, which prints "Block size: 4096 ..." to stdout and exits 0 |
|
# โ so a BSD-first `||` chain never falls through and that text becomes the |
|
# timestamp, leaking into the rendered bar. macOS has no `stat -c`, so it |
|
# fails cleanly and takes the BSD branch. |
|
m=$(stat -c %Y "$1" 2>/dev/null) || m=$(stat -f %m "$1" 2>/dev/null) |
|
case "${m:-}" in ''|*[!0-9]*) echo 0 ;; *) echo "$m" ;; esac |
|
} |
|
|
|
cache_age() { |
|
[ -f "$USAGE_CACHE" ] || { echo 999999; return; } |
|
local age=$((NOW - $(file_mtime "$USAGE_CACHE"))) |
|
[ "$age" -lt 0 ] && age=0 |
|
echo "$age" |
|
} |
|
|
|
# macOS: OAuth blob lives in the Keychain; Linux/WSL: plaintext credentials file. |
|
get_oauth_token() { |
|
local token="" |
|
if command -v security >/dev/null 2>&1; then |
|
token=$(security find-generic-password -s "$KEYCHAIN_SERVICE" -w 2>/dev/null \ |
|
| jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) |
|
fi |
|
if [ -z "$token" ] && [ -f "$CREDENTIALS_FILE" ]; then |
|
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$CREDENTIALS_FILE" 2>/dev/null) |
|
fi |
|
printf '%s' "$token" |
|
} |
|
|
|
refresh_usage() { |
|
local token resp tmp |
|
token=$(get_oauth_token) |
|
[ -z "$token" ] && return 1 |
|
resp=$(curl -s --max-time 3 "https://api.anthropic.com/api/oauth/usage" \ |
|
-H "Authorization: Bearer $token" \ |
|
-H "anthropic-beta: oauth-2025-04-20" \ |
|
-H "Content-Type: application/json" 2>/dev/null) |
|
printf '%s' "$resp" | jq -e '.five_hour.utilization' >/dev/null 2>&1 || return 1 |
|
tmp=$(mktemp "${USAGE_CACHE}.XXXXXX") || return 1 |
|
if printf '%s' "$resp" > "$tmp"; then |
|
mv -f "$tmp" "$USAGE_CACHE" |
|
else |
|
rm -f "$tmp" |
|
return 1 |
|
fi |
|
} |
|
|
|
# Refresh in the background so a render never blocks on the network. The mkdir |
|
# lock is atomic on bash 3.2/macOS (no flock needed); a lock dir older than 60s |
|
# is a crashed refresh and gets reclaimed. |
|
maybe_refresh_usage() { |
|
[ "$(cache_age)" -le "$REFRESH_INTERVAL" ] && return |
|
# Reclaim a crashed refresh's lock (or any foreign file squatting on the |
|
# path) once it is older than 60s. |
|
if [ -e "$LOCK_DIR" ] && [ $((NOW - $(file_mtime "$LOCK_DIR"))) -gt 60 ]; then |
|
rm -rf "$LOCK_DIR" 2>/dev/null |
|
fi |
|
mkdir "$LOCK_DIR" 2>/dev/null || return |
|
( refresh_usage; rmdir "$LOCK_DIR" 2>/dev/null ) >/dev/null 2>&1 & |
|
} |
|
|
|
# "Fable: 31% ๐ข / Snt: 12% ๐ข" โ per-model weekly quotas. Prefers the modern |
|
# .limits[] weekly_scoped entries; falls back to the legacy seven_day_<model> |
|
# keys for older API responses. @tsv keeps the jq free of escape sequences. |
|
weekly_models_part() { |
|
[ -f "$USAGE_CACHE" ] || return |
|
local out="" key pct label dot |
|
while IFS=$'\t' read -r key pct; do |
|
[ -z "$key" ] && continue |
|
pct=$(printf '%.0f' "$pct" 2>/dev/null) || continue |
|
case "$key" in |
|
[Ff]able*) label="Fable" ;; |
|
[Ss]onnet*) label="Snt" ;; |
|
[Oo]pus*) label="Opus" ;; |
|
[Hh]aiku*) label="Hku" ;; |
|
[Mm]ythos*) label="Myth" ;; |
|
*) label=$(sanitize "$key") ;; |
|
esac |
|
dot=$(usage_dot "$pct") |
|
[ -n "$out" ] && out="$out / " |
|
out="${out}${label}: ${pct}% ${dot}" |
|
done <<EOF |
|
$(jq -r ' |
|
([.limits // [] | .[] |
|
| select(.kind == "weekly_scoped" and .scope != null and .scope.model != null and .percent != null) |
|
| [(.scope.model.display_name // .scope.model.id // "model"), .percent]] |
|
) as $scoped |
|
| (if ($scoped | length) > 0 then $scoped |
|
else [to_entries[] |
|
| select(.key | startswith("seven_day_")) |
|
| select(.value != null and (.value | type == "object") and .value.utilization != null) |
|
| [(.key | sub("seven_day_"; "")), .value.utilization]] |
|
end) |
|
| .[] | @tsv' "$USAGE_CACHE" 2>/dev/null) |
|
EOF |
|
printf '%s' "$out" |
|
} |
|
|
|
# โโ Parse stdin in a single jq pass โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
|
# Fields joined on the unit separator so a "|" inside a branch or model name |
|
# cannot shift columns. |
|
|
|
INPUT=$(cat) |
|
NOW=$(date +%s) |
|
|
|
[ "$USE_USAGE_API" = "1" ] && maybe_refresh_usage |
|
|
|
FIELDS=$(printf '%s' "$INPUT" | jq -r '[ |
|
(.workspace.current_dir // .cwd // ""), |
|
(.model | if type == "object" then (.display_name // .id // "") else "" end), |
|
(.effort.level // ""), |
|
(if .context_window.current_usage == null then "0" else "1" end), |
|
(.context_window.current_usage.input_tokens // 0 | tostring), |
|
(.context_window.current_usage.cache_creation_input_tokens // 0 | tostring), |
|
(.context_window.current_usage.cache_read_input_tokens // 0 | tostring), |
|
(.context_window.context_window_size // 0 | tostring), |
|
(.rate_limits.five_hour.used_percentage // "" | tostring), |
|
(.rate_limits.five_hour.resets_at // "" | tostring), |
|
(.rate_limits.seven_day.used_percentage // "" | tostring), |
|
(.rate_limits.seven_day.resets_at // "" | tostring), |
|
(.pr.number // "" | tostring), |
|
(.pr.review_state // "") |
|
] | join("\u001f")' 2>/dev/null) |
|
IFS=$'\x1f' read -r CWD MODEL EFFORT HAVE_USAGE ITOK CCTOK CRTOK CTXSZ \ |
|
R5H R5H_RESET R7D R7D_RESET PR_NUM PR_STATE <<< "$FIELDS" |
|
|
|
# stdin may give resets_at as ISO 8601, but merge_quota and quota_segment both |
|
# do epoch ARITHMETIC on it โ an unconverted ISO value errors ("value too great |
|
# for base") and blanks both quota segments. Guarded on non-numeric, so an |
|
# already-epoch value passes through untouched. (Masked on macOS when the usage |
|
# cache wins the merge and supplies the epoch; bites the USE_USAGE_API=0 path.) |
|
case "${R5H_RESET:-}" in "") ;; *[!0-9]*) R5H_RESET=$(iso_to_epoch "$R5H_RESET") ;; esac |
|
case "${R7D_RESET:-}" in "") ;; *[!0-9]*) R7D_RESET=$(iso_to_epoch "$R7D_RESET") ;; esac |
|
|
|
# โโ Account-fresh quota merge + burn history โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
|
# The usage cache is shared by every session and refreshed by whichever |
|
# renders first, so it can be fresher than this session's stdin when another |
|
# session is doing the burning. |
|
|
|
if [ "$USE_USAGE_API" = "1" ] && [ -f "$USAGE_CACHE" ]; then |
|
IFS=$'\t' read -r C5P C5R C7P C7R <<EOF |
|
$(jq -r '[(.five_hour.utilization // ""), (.five_hour.resets_at // ""), |
|
(.seven_day.utilization // ""), (.seven_day.resets_at // "")] | @tsv' "$USAGE_CACHE" 2>/dev/null) |
|
EOF |
|
[ -n "$C5R" ] && C5R=$(iso_to_epoch "$C5R") |
|
[ -n "$C7R" ] && C7R=$(iso_to_epoch "$C7R") |
|
merged=$(merge_quota "$R5H" "$R5H_RESET" "$C5P" "$C5R") |
|
R5H=${merged%%|*}; R5H_RESET=${merged##*|} |
|
merged=$(merge_quota "$R7D" "$R7D_RESET" "$C7P" "$C7R") |
|
R7D=${merged%%|*}; R7D_RESET=${merged##*|} |
|
fi |
|
|
|
# One sample per ~30s across all sessions: "ts 5h% 5h_reset 7d% 7d_reset". |
|
# Sliding-window burn rates read this; entries older than 8 days are pruned. |
|
if [ -n "$R5H" ] || [ -n "$R7D" ]; then |
|
last_ts=$(tail -1 "$HISTORY_FILE" 2>/dev/null | awk '{print $1}') |
|
case "$last_ts" in |
|
''|*[!0-9]*) last_ts=0 ;; |
|
esac |
|
if [ $((NOW - last_ts)) -ge 30 ]; then |
|
h5p="-"; h5r="-"; h7p="-"; h7r="-" |
|
[ -n "$R5H" ] && h5p=$(printf '%.0f' "$R5H" 2>/dev/null) |
|
[ -n "$R5H_RESET" ] && h5r="$R5H_RESET" |
|
[ -n "$R7D" ] && h7p=$(printf '%.0f' "$R7D" 2>/dev/null) |
|
[ -n "$R7D_RESET" ] && h7r="$R7D_RESET" |
|
echo "$NOW ${h5p:--} ${h5r} ${h7p:--} ${h7r}" >> "$HISTORY_FILE" 2>/dev/null |
|
first_ts=$(head -1 "$HISTORY_FILE" 2>/dev/null | awk '{print $1}') |
|
case "$first_ts" in |
|
''|*[!0-9]*) first_ts=$NOW ;; |
|
esac |
|
if [ $((NOW - first_ts)) -gt 691200 ]; then |
|
tmp=$(mktemp "${HISTORY_FILE}.XXXXXX") \ |
|
&& awk -v cut=$((NOW - 691200)) '$1+0 >= cut' "$HISTORY_FILE" > "$tmp" 2>/dev/null \ |
|
&& mv -f "$tmp" "$HISTORY_FILE" |
|
fi |
|
fi |
|
fi |
|
|
|
# โโ Row 1: path + git โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
|
|
|
[ -z "$CWD" ] && CWD="$PWD" |
|
if [ -n "$HOME" ] && [ "${CWD#$HOME}" != "$CWD" ]; then |
|
display_path="~${CWD#$HOME}" |
|
else |
|
display_path="$CWD" |
|
fi |
|
|
|
# Machine indicator: ๐ sandbox / ๐ container / ๐ฅ๏ธ normal, with the short |
|
# hostname (no FQDN). The path always keeps ๐. |
|
if [ -n "$IS_SANDBOX" ]; then |
|
host_emoji="๐" |
|
elif [ -f /.dockerenv ] || [ -f /run/.containerenv ] \ |
|
|| grep -qE '(docker|kubepods|containerd|lxc)' /proc/1/cgroup 2>/dev/null; then |
|
host_emoji="๐" |
|
else |
|
host_emoji="๐ฅ๏ธ" |
|
fi |
|
host_short=$(hostname -s 2>/dev/null || hostname 2>/dev/null) |
|
host_short="${host_short%%.*}" |
|
host_short=$(sanitize "$host_short") |
|
line1="${host_emoji} ${host_short} | ๐ ${display_path}" |
|
|
|
if [ -d "$CWD" ] && git -C "$CWD" rev-parse --is-inside-work-tree >/dev/null 2>&1; then |
|
gs=$(git -C "$CWD" --no-optional-locks status --porcelain=v2 --branch 2>/dev/null) |
|
parsed=$(echo "$gs" | awk ' |
|
BEGIN { branch="-"; upstream="-"; oid="-"; ahead="+0"; behind="-0" |
|
staged=0; modified=0; untracked=0; unmerged=0 } |
|
/^# branch\.oid/ { oid=$3 } |
|
/^# branch\.head/ { branch=$3 } |
|
/^# branch\.upstream/ { upstream=$3 } |
|
/^# branch\.ab/ { ahead=$3; behind=$4 } |
|
/^[12] / { x=substr($2,1,1); y=substr($2,2,1) |
|
if (x != ".") staged++ |
|
if (y != ".") modified++ } |
|
/^\? / { untracked++ } |
|
/^u / { unmerged++ } |
|
END { printf "%s\t%s\t%s\t%s\t%s\t%d\t%d\t%d\t%d", |
|
branch, upstream, oid, ahead, behind, |
|
staged, modified, untracked, unmerged }') |
|
IFS=$'\t' read -r branch upstream oid ahead behind staged modified untracked unmerged <<< "$parsed" |
|
branch=$(sanitize "$branch") |
|
upstream=$(sanitize "$upstream") |
|
|
|
if [ "$branch" = "(detached)" ]; then |
|
git_part="๐ฟ detached@${oid:0:7}" |
|
else |
|
git_part="๐ฟ ${branch}" |
|
fi |
|
if [ "$upstream" != "-" ]; then |
|
git_part="${git_part} โก๏ธ ${upstream}" |
|
else |
|
git_part="${git_part} (no upstream)" |
|
fi |
|
ahead_num=${ahead#+} |
|
behind_num=${behind#-} |
|
[ "${ahead_num:-0}" != "0" ] && git_part="${git_part} โฌ๏ธ ahead: ${ahead_num}" |
|
[ "${behind_num:-0}" != "0" ] && git_part="${git_part} โฌ๏ธ behind: ${behind_num}" |
|
|
|
changes="" |
|
[ "$staged" -gt 0 ] && changes="${changes} ๐ฅ staged: ${staged}" |
|
[ "$modified" -gt 0 ] && changes="${changes} ๐ mod: ${modified}" |
|
[ "$unmerged" -gt 0 ] && changes="${changes} ๐ฅ conflict: ${unmerged}" |
|
[ "$untracked" -gt 0 ] && changes="${changes} ๐ new: ${untracked}" |
|
if [ -n "$changes" ]; then |
|
git_part="${git_part} |${changes}" |
|
else |
|
git_part="${git_part} โ
clean" |
|
fi |
|
line1="${line1} | ${git_part}" |
|
fi |
|
|
|
if [ "$SHOW_PR" = "1" ] && [ -n "$PR_NUM" ]; then |
|
pr_icon="" |
|
case "$PR_STATE" in |
|
approved) pr_icon=" โ" ;; |
|
changes_requested) pr_icon=" โ" ;; |
|
pending) pr_icon=" โ" ;; |
|
draft) pr_icon=" โ" ;; |
|
esac |
|
line1="${line1} | ๐ PR#$(sanitize "$PR_NUM")${pr_icon}" |
|
fi |
|
|
|
# โโ Row 2: model + effort, context / memory / autocompact, battery โโโโโโโโโโโโ |
|
|
|
if [ -z "$EFFORT" ] && [ -f "$SETTINGS_FILE" ]; then |
|
EFFORT=$(jq -r '.effortLevel // empty' "$SETTINGS_FILE" 2>/dev/null) |
|
fi |
|
MODEL=$(sanitize "$MODEL") |
|
[ -n "$EFFORT" ] && MODEL="${MODEL} โฎ ๐ช ${EFFORT}" |
|
|
|
if [ -n "$MODEL" ]; then out="๐ค ${MODEL}"; else out=""; fi |
|
|
|
if [ "$HAVE_USAGE" = "1" ] && [ "${CTXSZ:-0}" -gt 0 ] 2>/dev/null; then |
|
cur=$((ITOK + CCTOK + CRTOK)) |
|
ctxpct=$((cur * 100 / CTXSZ)) |
|
cthou=$((cur / 1000)) |
|
ctxm=$((CTXSZ / 1000000)) |
|
if [ "$ctxm" -gt 0 ]; then ctxlbl="${ctxm}M"; else ctxlbl="$((CTXSZ / 1000))k"; fi |
|
|
|
mem_k=$((MEM_LIMIT / 1000)) |
|
mem_pct=$((cur * 100 / MEM_LIMIT)) |
|
mem_dot=$(usage_dot "$mem_pct") |
|
mem_seg="๐ง ${cthou}k/${ctxlbl} (${ctxpct}%) โฎ ๐ง ${cthou}k/${mem_k}k (${mem_pct}%) ${mem_dot}" |
|
if [ "$SHOW_WEEKLY_MODELS" = "1" ]; then |
|
models=$(weekly_models_part) |
|
[ -n "$models" ] && mem_seg="${mem_seg} โฎ ${models}" |
|
fi |
|
out="$out | ${mem_seg}" |
|
|
|
cpctdf=${CLAUDE_AUTOCOMPACT_PCT_OVERRIDE:-95} |
|
compact=${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-$((CTXSZ * cpctdf / 100))} |
|
if [ "$compact" -gt 0 ]; then |
|
cpct=$((cur * 100 / compact)) |
|
ctgt=$((compact / 1000)) |
|
dot=$(usage_dot "$cpct") |
|
curk=$((cur / 1000)) |
|
out="$out | โป๏ธ ${curk}k/${ctgt}k (${cpct}%) ${dot}" |
|
fi |
|
fi |
|
line2="${out# | }" |
|
|
|
batt_seg="" |
|
if command -v pmset >/dev/null 2>&1; then |
|
batt=$(pmset -g batt 2>/dev/null) |
|
pct=$(echo "$batt" | grep -oE '[0-9]+%' | head -1) |
|
if [ -n "$pct" ]; then |
|
if echo "$batt" | grep -q "AC Power"; then |
|
batt_seg="๐ plugged in ๐ข" |
|
else |
|
num=${pct%\%} |
|
rem_str="" |
|
time_rem=$(echo "$batt" | grep -oE '[0-9]+:[0-9]+ remaining' | head -1) |
|
if [ -n "$time_rem" ]; then |
|
hh=${time_rem%%:*}; mm=${time_rem#*:}; mm=${mm%% *} |
|
total_secs=$((10#$hh * 3600 + 10#$mm * 60)) |
|
[ "$total_secs" -gt 0 ] && rem_str=" ($(fmt_dur $total_secs))" |
|
fi |
|
if [ "$num" -le 20 ]; then batt_seg="๐ชซ ${pct}${rem_str} ๐จ" |
|
elif [ "$num" -lt 50 ]; then batt_seg="๐ ${pct}${rem_str} โ ๏ธ" |
|
else batt_seg="๐ ${pct}${rem_str} ๐ข" |
|
fi |
|
fi |
|
fi |
|
elif [ -d /sys/class/power_supply ]; then |
|
bat_dir="" |
|
for d in /sys/class/power_supply/BAT*; do |
|
[ -d "$d" ] && bat_dir="$d" && break |
|
done |
|
if [ -n "$bat_dir" ] && [ -f "$bat_dir/capacity" ]; then |
|
num=$(cat "$bat_dir/capacity" 2>/dev/null) |
|
pct="${num}%" |
|
on_ac=0 |
|
for a in /sys/class/power_supply/A*; do |
|
[ -f "$a/online" ] && [ "$(cat "$a/online" 2>/dev/null)" = "1" ] && on_ac=1 && break |
|
done |
|
if [ "$on_ac" = "1" ]; then |
|
batt_seg="๐ plugged in ๐ข" |
|
else |
|
rem_str="" |
|
time_rem=$(acpi -b 2>/dev/null | grep -oE '[0-9]+:[0-9]+:[0-9]+' | head -1) |
|
if [ -n "$time_rem" ]; then |
|
hh=${time_rem%%:*}; rest=${time_rem#*:}; mm=${rest%%:*} |
|
total_secs=$((10#$hh * 3600 + 10#$mm * 60)) |
|
[ "$total_secs" -gt 0 ] && rem_str=" ($(fmt_dur $total_secs))" |
|
fi |
|
if [ "$num" -le 20 ]; then batt_seg="๐ชซ ${pct}${rem_str} ๐จ" |
|
elif [ "$num" -lt 50 ]; then batt_seg="๐ ${pct}${rem_str} โ ๏ธ" |
|
else batt_seg="๐ ${pct}${rem_str} ๐ข" |
|
fi |
|
fi |
|
fi |
|
fi |
|
[ -n "$batt_seg" ] && line2="${line2} | ${batt_seg}" |
|
|
|
# โโ Row 3: clock, 5h reset flag, 5h + 7d quotas with burn rates โโโโโโโโโโโโโโโ |
|
|
|
out="" |
|
if [ -n "$R5H" ]; then |
|
seg=$(quota_segment "โฒ๏ธ" "$R5H" "$R5H_RESET" 18000 1800 3600 "%/h" 20 25 "<30m data" 3600 600 2 3) |
|
out="$out | $seg" |
|
else |
|
out="$out | โฒ๏ธ N/A" |
|
fi |
|
if [ -n "$R7D" ]; then |
|
seg=$(quota_segment "๐
" "$R7D" "$R7D_RESET" 604800 86400 86400 "%/d avg" 14 17 "<1d data" 86400 7200 4 5) |
|
out="$out | $seg" |
|
else |
|
out="$out | ๐
N/A" |
|
fi |
|
|
|
flag_seg="" |
|
if [ -n "$R5H_RESET" ]; then |
|
r5h_clock=$(date -r "$R5H_RESET" +"%I:%M %p" 2>/dev/null || date -d "@$R5H_RESET" +"%I:%M %p" 2>/dev/null) |
|
r5h_clock="${r5h_clock#0}" |
|
[ -n "$r5h_clock" ] && flag_seg="๐ ${r5h_clock} โฒ๏ธ" |
|
fi |
|
if [ "$SHOW_WEEK_DATE" = "1" ] && [ -n "$R7D_RESET" ]; then |
|
r7d_date=$(date -r "$R7D_RESET" +"%a %m/%d" 2>/dev/null || date -d "@$R7D_RESET" +"%a %m/%d" 2>/dev/null) |
|
r7d_date=$(echo "$r7d_date" | sed -e 's| 0| |' -e 's|/0|/|') |
|
if [ -n "$r7d_date" ]; then |
|
if [ -n "$flag_seg" ]; then |
|
flag_seg="${flag_seg} โฎ ๐ณ๏ธ ${r7d_date} ๐
" |
|
else |
|
flag_seg="๐ณ๏ธ ${r7d_date} ๐
" |
|
fi |
|
fi |
|
fi |
|
[ -n "$flag_seg" ] && flag_seg="${flag_seg} | " |
|
clock=$(date +"%I:%M %p") |
|
clock="${clock#0}" |
|
sep="|" |
|
[ -n "$flag_seg" ] && sep="โฎ" |
|
line3="โฐ ${clock} ${sep} ${flag_seg}${out# | }" |
|
|
|
# Style the separators: | gets bold bright-blue on black (strong major break); |
|
# โฎ gets a pale near-white blue (quiet, sits close to the text color) โ or, |
|
# with SUBSEP_FG=none, is left in the terminal's default foreground so it |
|
# matches the font exactly. All other text is untouched; each glyph |
|
# self-resets so no color bleeds. |
|
# Row 4: static keybinding hints. |
|
[ "$SHOW_HINTS" = "1" ] && line4="$HINTS_TEXT" |
|
|
|
if [ "$SHOW_PIPE_STYLE" = "1" ]; then |
|
esc=$(printf '\033') |
|
sp="${esc}[1;38;5;${PIPE_FG};48;5;${PIPE_BG}m|${esc}[0m" |
|
line1="${line1//|/$sp}" |
|
line2="${line2//|/$sp}" |
|
line3="${line3//|/$sp}" |
|
line4="${line4//|/$sp}" |
|
case "$SUBSEP_FG" in |
|
''|none|default) : ;; # leave โฎ in the default foreground (exact font match) |
|
*) |
|
ssp="${esc}[38;5;${SUBSEP_FG}mโฎ${esc}[0m" |
|
line1="${line1//โฎ/$ssp}" |
|
line2="${line2//โฎ/$ssp}" |
|
line3="${line3//โฎ/$ssp}" |
|
line4="${line4//โฎ/$ssp}" |
|
;; |
|
esac |
|
fi |
|
|
|
printf '%s\n%s\n%s' "$line1" "$line2" "$line3" |
|
[ "$SHOW_HINTS" = "1" ] && printf '\n%s' "$line4" |