feat(claude): update statusline
This commit is contained in:
@@ -10,7 +10,18 @@
|
||||
"hooks": {},
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "sh \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}/statusline-command.sh\""
|
||||
"command": "bash \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}/statusline-command.sh\""
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"typesafe@typesafe-ai": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"typesafe-ai": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "typesafe-ai/skills"
|
||||
}
|
||||
}
|
||||
},
|
||||
"effortLevel": "high",
|
||||
"tui": "default",
|
||||
|
||||
184
claude/statusline-command.sh
Normal file → Executable file
184
claude/statusline-command.sh
Normal file → Executable file
@@ -1,52 +1,158 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
# NOTE:(@janezicmatej) layout adapted from github.com/nejcr/grind statusline.js:
|
||||
# model + context, repo, 5h/7d limits, session id, optional dev-links row
|
||||
|
||||
input=$(cat)
|
||||
|
||||
model=$(echo "$input" | jq -r '.model.display_name // empty')
|
||||
total_in=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
|
||||
total_out=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0')
|
||||
ctx=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
|
||||
five_hr=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
|
||||
five_hr_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
|
||||
seven_day=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
|
||||
seven_day_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
|
||||
# one jq call emits every field as a quoted shell assignment
|
||||
eval "$(jq -r '
|
||||
def s: tostring | @sh;
|
||||
"model=\(.model.display_name // "" | s)",
|
||||
"used=\(.context_window.total_input_tokens // 0 | s)",
|
||||
"total=\(.context_window.context_window_size // 0 | s)",
|
||||
"ctx_pct=\(.context_window.used_percentage // "" | s)",
|
||||
"cwd=\(.workspace.current_dir // "" | s)",
|
||||
"project_dir=\(.workspace.project_dir // "" | s)",
|
||||
"session_id=\(.session_id // "" | s)"
|
||||
' <<<"$input")"
|
||||
|
||||
# format seconds until reset as human-readable
|
||||
fmt_remaining() {
|
||||
now=$(date +%s)
|
||||
diff=$(( $1 - now ))
|
||||
[ "$diff" -le 0 ] && return
|
||||
h=$(( diff / 3600 ))
|
||||
m=$(( (diff % 3600) / 60 ))
|
||||
if [ "$h" -gt 0 ]; then
|
||||
printf '%dh%dm' "$h" "$m"
|
||||
else
|
||||
printf '%dm' "$m"
|
||||
# claude only sends rate_limits after the first api response, so a fresh session
|
||||
# would show none until its first prompt; every refresh that has them writes the
|
||||
# last known limits next to the config, and a fresh session reads those back dimmed
|
||||
cfg_dir=${CLAUDE_CONFIG_DIR:-$HOME/.claude}
|
||||
limits_cache=$cfg_dir/statusline-limits.json
|
||||
read_limits() {
|
||||
jq -r '
|
||||
def s: tostring | @sh;
|
||||
"five_pct=\(.five_hour.used_percentage // "" | s)",
|
||||
"five_reset=\(.five_hour.resets_at // "" | s)",
|
||||
"seven_pct=\(.seven_day.used_percentage // "" | s)",
|
||||
"seven_reset=\(.seven_day.resets_at // "" | s)"
|
||||
' 2>/dev/null
|
||||
}
|
||||
current=$(jq -c '.rate_limits // {} | with_entries(select(.value.used_percentage != null))' <<<"$input")
|
||||
stale=""
|
||||
if [[ $current != "{}" ]]; then
|
||||
eval "$(read_limits <<<"$current")"
|
||||
# merged, not replaced: a turn carrying one window must not wipe the other;
|
||||
# written then renamed so a concurrent session never reads it half-written
|
||||
merged=$(jq -c --argjson cur "$current" '. * $cur' "$limits_cache" 2>/dev/null) || merged=$current
|
||||
tmp=$limits_cache.$$.tmp
|
||||
if { printf '%s\n' "$merged" > "$tmp"; } 2>/dev/null; then mv -f "$tmp" "$limits_cache" 2>/dev/null || rm -f "$tmp"; fi
|
||||
elif [[ -f $limits_cache ]]; then
|
||||
stale=1
|
||||
eval "$(read_limits < "$limits_cache")"
|
||||
fi
|
||||
|
||||
R=$'\e[0m' C=$'\e[36m' G=$'\e[32m' Y=$'\e[33m' RE=$'\e[31m' D=$'\e[2m'
|
||||
|
||||
# "Claude 3.5 Sonnet" -> "Sonnet 3.5", "Claude Fable 5.1" -> "Fable 5.1"
|
||||
model=$(sed -E -e 's/^Claude +([0-9.]+) +([A-Za-z0-9_]+)$/\2 \1/; t' -e 's/^Claude +//' <<<"${model:-unknown}")
|
||||
|
||||
# 61234 -> 61.2k, half-up rounding to match Math.round
|
||||
fmt() { awk -v n="$1" 'BEGIN { if (n >= 1000) printf "%.1fk", n / 1000; else printf "%d", n }'; }
|
||||
round() { awk -v x="$1" 'BEGIN { printf "%d", int(x + 0.5) }'; }
|
||||
|
||||
# same scale for context and limits: green under 50, yellow under 80, red above
|
||||
color_for() {
|
||||
if (( $1 >= 80 )); then printf %s "$RE"; elif (( $1 >= 50 )); then printf %s "$Y"; else printf %s "$G"; fi
|
||||
}
|
||||
|
||||
parts=""
|
||||
|
||||
[ -n "$model" ] && parts="$model"
|
||||
|
||||
if [ -n "$total_in" ] && [ "$total_in" != "0" ]; then
|
||||
total=$((total_in + total_out))
|
||||
parts="${parts:+$parts | }tokens:${total}"
|
||||
# repo: origin url from the host onwards (host/group/repo), else the folder name
|
||||
repo=""
|
||||
if url=$(git -C "$cwd" --no-optional-locks remote get-url origin 2>/dev/null); then
|
||||
repo=$(sed -E \
|
||||
-e 's#^[^@/]+@([^:/]+):(.+)$#\1/\2#; t done' \
|
||||
-e 's#^[a-z+]+://([^@/]+@)?([^/:]+)(:[0-9]+)?/(.+)$#\2/\4#I; t done' \
|
||||
-e 's/.*//' -e ':done' -e 's#/+$##; s/\.git$//' <<<"$url")
|
||||
fi
|
||||
if [[ -z $repo ]]; then
|
||||
top=$(git -C "$cwd" --no-optional-locks rev-parse --show-toplevel 2>/dev/null)
|
||||
repo=$(basename "${top:-${project_dir:-$cwd}}")
|
||||
fi
|
||||
|
||||
if [ -n "$ctx" ]; then
|
||||
parts="${parts:+$parts | }ctx:$(printf '%.0f' "$ctx")%"
|
||||
# limits, same shape as the context group: "5h 42% (15:36)" and "7d 81% (5d)"
|
||||
# the 5h window resets within hours so the clock answers "when", the 7d one is
|
||||
# far enough out that days are all that matters; a rolled-over window shows a dash
|
||||
now=$(date +%s)
|
||||
rl_part() {
|
||||
local label=$1 pct=$2 reset=${3%.*} mode=$4 p s left
|
||||
[[ -n $pct ]] || return 0
|
||||
s=$(( ${reset:-0} - now ))
|
||||
if [[ -z $reset ]] || (( s <= 0 )); then
|
||||
printf '%s%s —%s' "$D" "$label" "$R"
|
||||
return 0
|
||||
fi
|
||||
p=$(round "$pct")
|
||||
case $mode in
|
||||
clock) left=$(date -d "@$reset" +%H:%M) ;;
|
||||
days) if (( s < 86400 )); then left="$(( (s + 3599) / 3600 ))h"; else left="$(( (s + 43200) / 86400 ))d"; fi ;;
|
||||
esac
|
||||
# last known values from the cache render fully dim to say they may be behind
|
||||
local col; col=$(color_for "$p"); [[ -n $stale ]] && col=$D
|
||||
printf '%s%s%s %s%s%%%s %s(%s)%s' "$D" "$label" "$R" "$col" "$p" "$R" "$D" "$left" "$R"
|
||||
}
|
||||
|
||||
if [ -n "$five_hr" ]; then
|
||||
five_hr_ttl=""
|
||||
[ -n "$five_hr_reset" ] && five_hr_ttl=$(fmt_remaining "$five_hr_reset")
|
||||
parts="${parts:+$parts | }5h:$(printf '%.0f' "$five_hr")%${five_hr_ttl:+($five_hr_ttl)}"
|
||||
groups=()
|
||||
|
||||
session="${C}${model}${R} ${G}$(fmt "$used")/$(fmt "$total")${R}"
|
||||
if [[ -n $ctx_pct ]]; then
|
||||
p=$(round "$ctx_pct")
|
||||
session+=" ${D}(${R}$(color_for "$p")${p}%${R}${D})${R}"
|
||||
fi
|
||||
groups+=("$session")
|
||||
|
||||
if [ -n "$seven_day" ]; then
|
||||
seven_day_ttl=""
|
||||
[ -n "$seven_day_reset" ] && seven_day_ttl=$(fmt_remaining "$seven_day_reset")
|
||||
parts="${parts:+$parts | }7d:$(printf '%.0f' "$seven_day")%${seven_day_ttl:+($seven_day_ttl)}"
|
||||
[[ -n $repo ]] && groups+=("${C}${repo}${R}")
|
||||
|
||||
# limits are only sent once the session has talked to the api
|
||||
part=$(rl_part 5h "$five_pct" "$five_reset" clock); [[ -n $part ]] && groups+=("$part")
|
||||
part=$(rl_part 7d "$seven_pct" "$seven_reset" days); [[ -n $part ]] && groups+=("$part")
|
||||
|
||||
# full session id so it can be pasted into claude --resume, dim and last
|
||||
[[ -n $session_id ]] && groups+=("${D}${session_id}${R}")
|
||||
|
||||
sep=" ${D}│${R} "
|
||||
out=""
|
||||
for g in "${groups[@]}"; do out+="${out:+$sep}$g"; done
|
||||
printf '%s\n' "$out"
|
||||
|
||||
# pinned dev links, opt-in per project: .claude/dev-links.json adds a second row
|
||||
# a value is either "url" or {url, note}, note being a throwaway local credential
|
||||
link_file=""
|
||||
for d in "$project_dir" "$cwd"; do
|
||||
[[ -n $d && -f $d/.claude/dev-links.json ]] && { link_file=$d/.claude/dev-links.json; break; }
|
||||
done
|
||||
[[ -n $link_file ]] || exit 0
|
||||
|
||||
pins=$(jq -r '
|
||||
to_entries[]
|
||||
| { label: .key,
|
||||
url: (if (.value | type) == "string" then .value
|
||||
elif (.value | type) == "object" then .value.url else null end),
|
||||
note: (if (.value | type) == "object" and (.value.note | type) == "string" then .value.note else "" end) }
|
||||
| select((.url | type) == "string" and (.url | test("^https?://")))
|
||||
| [.label, .url, .note] | @tsv
|
||||
' "$link_file" 2>/dev/null) || exit 0
|
||||
[[ -n $pins ]] || exit 0
|
||||
|
||||
# bare urls rather than osc 8: claude code resolves plain http:// on ctrl+click
|
||||
# itself; COLUMNS is the only readable width since stdout is captured
|
||||
cols=${COLUMNS:-120}
|
||||
[[ $cols =~ ^[0-9]+$ ]] || cols=120
|
||||
gap=3
|
||||
row="" width=0
|
||||
while IFS=$'\t' read -r label url note; do
|
||||
len=$(( ${#label} + 1 + ${#url} + (${#note} > 0 ? ${#note} + 3 : 0) ))
|
||||
cost=$(( width ? len + gap : len ))
|
||||
if (( width && width + cost > cols - 4 )); then
|
||||
printf '%s\n' "$row"
|
||||
row="" width=$len
|
||||
else
|
||||
width=$(( width + cost ))
|
||||
fi
|
||||
|
||||
[ -n "$parts" ] && printf '%s' "$parts"
|
||||
cell="${C}${label}${R} ${D}${url}${R}"
|
||||
[[ -n $note ]] && cell+=" ${Y}${note}${R}"
|
||||
row+="${row:+${D} · ${R}}$cell"
|
||||
done <<<"$pins"
|
||||
[[ -n $row ]] && printf '%s\n' "$row"
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user