another claude code statusline
1#!/usr/bin/env bash
2
3KLAUS_THEME="${KLAUS_THEME:-dark}"
4KLAUS_GIT_CACHE_TTL="${KLAUS_GIT_CACHE_TTL:-5}"
5KLAUS_USAGE_CACHE_TTL="${KLAUS_USAGE_CACHE_TTL:-60}"
6BAR_LABEL_WIDTH=3
7
8mkdir -p /tmp/klaus
9
10RST='\033[0m'
11
12if [[ "$KLAUS_THEME" == "light" ]]; then
13 RED='\033[38;2;157;0;6m'
14 GREEN='\033[38;2;121;116;14m'
15 EMPTY_FG='\033[38;2;251;241;199m'
16 MODEL_BG='\033[48;2;143;63;113m'
17 MODEL_FG='\033[38;2;251;241;199m'
18 IN_BG='\033[48;2;7;102;120m'
19 IN_FG='\033[38;2;251;241;199m'
20 OUT_BG='\033[48;2;66;123;88m'
21 OUT_FG='\033[38;2;251;241;199m'
22 CTX_BG='\033[48;2;181;118;20m'
23 CTX_FG='\033[38;2;251;241;199m'
24 DAY_BG='\033[48;2;121;116;14m'
25 DAY_FG='\033[38;2;251;241;199m'
26 WK_BG='\033[48;2;157;0;6m'
27 WK_FG='\033[38;2;251;241;199m'
28 REPO_BG='\033[48;2;143;63;113m'
29 REPO_FG='\033[38;2;251;241;199m'
30 BRANCH_BG='\033[48;2;66;123;88m'
31 BRANCH_FG='\033[38;2;251;241;199m'
32 DIFF_BG='\033[48;2;60;56;54m'
33 AGENT_BG='\033[48;2;143;63;113m'
34 AGENT_FG='\033[38;2;251;241;199m'
35else
36 RED='\033[38;2;204;36;29m'
37 GREEN='\033[38;2;152;151;26m'
38 EMPTY_FG='\033[38;2;235;219;178m'
39 MODEL_BG='\033[48;2;177;98;134m'
40 MODEL_FG='\033[38;2;40;40;40m'
41 IN_BG='\033[48;2;69;133;136m'
42 IN_FG='\033[38;2;40;40;40m'
43 OUT_BG='\033[48;2;104;157;106m'
44 OUT_FG='\033[38;2;40;40;40m'
45 CTX_BG='\033[48;2;215;153;33m'
46 CTX_FG='\033[38;2;40;40;40m'
47 DAY_BG='\033[48;2;152;151;26m'
48 DAY_FG='\033[38;2;40;40;40m'
49 WK_BG='\033[48;2;204;36;29m'
50 WK_FG='\033[38;2;40;40;40m'
51 REPO_BG='\033[48;2;177;98;134m'
52 REPO_FG='\033[38;2;40;40;40m'
53 BRANCH_BG='\033[48;2;104;157;106m'
54 BRANCH_FG='\033[38;2;40;40;40m'
55 DIFF_BG='\033[48;2;80;73;69m'
56 AGENT_BG='\033[48;2;177;98;134m'
57 AGENT_FG='\033[38;2;40;40;40m'
58fi
59
60JSON="$(cat)"
61
62PARSED=$(echo "$JSON" | jq -r '
63 (.model.display_name // ""),
64 (.workspace.current_dir // ""),
65 (.context_window.used_percentage // 0),
66 (.context_window.total_input_tokens // 0),
67 (.context_window.total_output_tokens // 0),
68 (.cost.total_lines_added // 0),
69 (.cost.total_lines_removed // 0),
70 (.agent.name // "")
71')
72
73{
74 read -r MODEL
75 read -r DIR
76 read -r PCT
77 read -r IN_TOKENS
78 read -r OUT_TOKENS
79 read -r LINES_ADD
80 read -r LINES_DEL
81 read -r AGENT_NAME
82} <<<"$PARSED"
83
84get_usage() {
85 local cache_file="/tmp/klaus/usage"
86 local now=$(date +%s)
87
88 if [[ -f "$cache_file" ]]; then
89 local mtime
90 if [[ "$(uname)" == "Darwin" ]]; then
91 mtime=$(stat -f %m "$cache_file" 2>/dev/null || echo 0)
92 else
93 mtime=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
94 fi
95 if [[ $((now - mtime)) -lt "$KLAUS_USAGE_CACHE_TTL" ]]; then
96 cat "$cache_file"
97 return
98 fi
99 fi
100
101 local token
102 token=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null |
103 jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
104
105 if [[ -z "$token" ]]; then
106 echo "0|0"
107 return
108 fi
109
110 local resp
111 resp=$(curl -sf --max-time 3 \
112 -H "Authorization: Bearer $token" \
113 -H "anthropic-beta: oauth-2025-04-20" \
114 "https://api.anthropic.com/api/oauth/usage" 2>/dev/null)
115
116 if [[ -z "$resp" ]] || echo "$resp" | jq -e '.error' &>/dev/null; then
117 # fall back to stale cache if available
118 [[ -f "$cache_file" ]] && cat "$cache_file" && return
119 echo "0|0"
120 return
121 fi
122
123 local result
124 result=$(echo "$resp" | jq -r '
125 ((.five_hour.utilization // 0) | round),
126 ((.seven_day.utilization // 0) | round)
127 ' 2>/dev/null | paste -sd'|' -)
128
129 if [[ -z "$result" || "$result" == "|" ]]; then
130 [[ -f "$cache_file" ]] && cat "$cache_file" && return
131 echo "0|0"
132 return
133 fi
134
135 echo "$result" >"$cache_file"
136 echo "$result"
137}
138
139USAGE_DATA=$(get_usage)
140IFS='|' read -r FIVE_HOUR_PCT SEVEN_DAY_PCT <<<"$USAGE_DATA"
141
142format_tokens() {
143 echo "$1" | awk '{
144 if ($1 >= 1000000) printf "%.1fM", $1/1000000
145 else if ($1 >= 1000) printf "%.0fk", $1/1000
146 else printf "%s", $1
147 }'
148}
149
150osc_link() {
151 printf '\033]8;;%s\033\\%s\033]8;;\033\\' "$1" "$2"
152}
153
154progress_bar() {
155 local pct="$1" label="$2" bar_width="${3:-8}" bg="$4" fg="$5"
156 local pct_int=${pct%.*}
157 pct_int=${pct_int:-0}
158 [[ "$pct_int" -gt 100 ]] && pct_int=100
159
160 local filled=$((pct_int * bar_width / 100))
161 local empty=$((bar_width - filled))
162 local filled_bar=$(printf "%${filled}s" | tr ' ' '▓')
163 local empty_bar=$(printf "%${empty}s" | tr ' ' '░')
164
165 local right_len=$((bar_width + 7))
166 local label_pad=$((W - right_len - 1))
167 [[ "$label_pad" -lt ${#label} ]] && label_pad=${#label}
168
169 printf "${bg}${fg} %-${label_pad}s %s${EMPTY_FG}%s${fg} %3d%% ${RST}" "$label" "$filled_bar" "$empty_bar" "$pct_int"
170}
171
172strip_ansi() {
173 printf '%s' "$1" | sed $'s/\033\\[[0-9;]*m//g' | sed $'s/\033\\]8;;[^\033]*\033\\\\//g'
174}
175
176pad_line() {
177 local content="$1" bg="$2" fg="$3"
178 local visible=$(strip_ansi "$content")
179 local vlen=${#visible}
180 local pad=$((W - vlen))
181 [[ "$pad" -lt 0 ]] && pad=0
182 local padding=$(printf "%${pad}s" "")
183 printf '%s%b%s%b' "$content" "${bg}${fg}" "$padding" "$RST"
184}
185
186get_git_info() {
187 local dir="$1"
188 [[ -z "$dir" ]] && return
189
190 local cache_file="/tmp/klaus/git"
191 local now=$(date +%s)
192
193 if [[ -f "$cache_file" ]]; then
194 local mtime
195 if [[ "$(uname)" == "Darwin" ]]; then
196 mtime=$(stat -f %m "$cache_file" 2>/dev/null || echo 0)
197 else
198 mtime=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
199 fi
200 if [[ $((now - mtime)) -lt "$KLAUS_GIT_CACHE_TTL" ]]; then
201 cat "$cache_file"
202 return
203 fi
204 fi
205
206 local result=""
207 if git -C "$dir" rev-parse --is-inside-work-tree &>/dev/null; then
208 local branch=$(git -C "$dir" symbolic-ref --short HEAD 2>/dev/null || git -C "$dir" rev-parse --short HEAD 2>/dev/null)
209 local staged=$(git -C "$dir" diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
210 local modified=$(git -C "$dir" diff --numstat 2>/dev/null | wc -l | tr -d ' ')
211
212 local link_part=""
213 local remote_url=$(git -C "$dir" remote get-url origin 2>/dev/null)
214 if [[ -n "$remote_url" ]]; then
215 local repo_name=$(echo "$remote_url" | sed -E 's#\.git$##; s#.*[:/]([^/]+/[^/]+)$#\1#')
216 local web_url=$(echo "$remote_url" | sed -E 's#^git@([^:]+):#https://\1/#; s#\.git$##')
217 link_part="${repo_name}|${web_url}"
218 fi
219
220 result="${branch}|${staged}|${modified}|${link_part}"
221 fi
222
223 echo "$result" >"$cache_file"
224 echo "$result"
225}
226
227IN_FMT="$(format_tokens "$IN_TOKENS")"
228OUT_FMT="$(format_tokens "$OUT_TOKENS")"
229
230GIT_INFO=$(get_git_info "$DIR")
231branch_text="" diff_text="" diff_len=0 repo_label=""
232if [[ -n "$GIT_INFO" ]]; then
233 IFS='|' read -r BRANCH STAGED MODIFIED REPO_NAME WEB_URL <<<"$GIT_INFO"
234 branch_text=" ${BRANCH}"
235 [[ "$STAGED" -gt 0 ]] && branch_text+=" +${STAGED}"
236 [[ "$MODIFIED" -gt 0 ]] && branch_text+=" ~${MODIFIED}"
237 if [[ "$LINES_ADD" -gt 0 || "$LINES_DEL" -gt 0 ]]; then
238 diff_text=" +${LINES_ADD} -${LINES_DEL} "
239 diff_len=${#diff_text}
240 fi
241 repo_label="${REPO_NAME:-$(basename "$DIR")}"
242fi
243
244model_min=$((${#MODEL} + ${#IN_FMT} + ${#OUT_FMT} + 8))
245bar_min=$((BAR_LABEL_WIDTH + 8 + 4))
246if [[ -n "$GIT_INFO" ]]; then
247 git_min=$((${#repo_label} + 2 + ${#branch_text} + 1 + diff_len))
248elif [[ -n "$DIR" ]]; then
249 dn="$(basename "$DIR")"
250 git_min=$((${#dn} + 2))
251else
252 git_min=0
253fi
254
255agent_min=0
256[[ -n "$AGENT_NAME" ]] && agent_min=$((${#AGENT_NAME} + 2))
257
258W=$model_min
259[[ "$bar_min" -gt "$W" ]] && W=$bar_min
260[[ "$git_min" -gt "$W" ]] && W=$git_min
261[[ "$agent_min" -gt "$W" ]] && W=$agent_min
262
263# fill space with bar (label 3 chars + 8 fixed chars)
264BAR_WIDTH=$((W - 11))
265[[ "$BAR_WIDTH" -lt 4 ]] && BAR_WIDTH=4
266
267tokens_len=$((${#IN_FMT} + ${#OUT_FMT} + 6))
268model_pad=$((W - tokens_len - 1))
269[[ "$model_pad" -lt $((${#MODEL} + 1)) ]] && model_pad=$((${#MODEL} + 1))
270
271line1="$(printf "${MODEL_BG}${MODEL_FG} %-${model_pad}s${RST}" "$MODEL")"
272line1+="$(printf "${IN_BG}${IN_FG} ↓%s ${RST}${OUT_BG}${OUT_FG} ↑%s ${RST}" "$IN_FMT" "$OUT_FMT")"
273
274line2=""
275if [[ -n "$GIT_INFO" ]]; then
276 right_len=$((${#branch_text} + 1 + diff_len))
277 repo_pad=$((W - right_len - 1))
278 [[ "$repo_pad" -lt $((${#repo_label} + 1)) ]] && repo_pad=$((${#repo_label} + 1))
279
280 if [[ -n "$WEB_URL" ]]; then
281 pad_spaces=$((repo_pad - ${#repo_label}))
282 [[ "$pad_spaces" -lt 0 ]] && pad_spaces=0
283 line2+="$(printf "${REPO_BG}${REPO_FG} %s%${pad_spaces}s${RST}" "$(osc_link "$WEB_URL" "$repo_label")" "")"
284 else
285 line2+="$(printf "${REPO_BG}${REPO_FG} %-${repo_pad}s${RST}" "$repo_label")"
286 fi
287 line2+="$(printf "${BRANCH_BG}${BRANCH_FG}%s ${RST}" "$branch_text")"
288 if [[ -n "$diff_text" ]]; then
289 line2+="$(printf "${DIFF_BG}${GREEN} +%s ${RED}-%s ${RST}" "$LINES_ADD" "$LINES_DEL")"
290 fi
291elif [[ -n "$DIR" ]]; then
292 dir_name="$(basename "$DIR")"
293 dir_pad=$((W - 1))
294 [[ "$dir_pad" -lt $((${#dir_name} + 1)) ]] && dir_pad=$((${#dir_name} + 1))
295 line2+="$(printf "${REPO_BG}${REPO_FG} %-${dir_pad}s${RST}" "$dir_name")"
296fi
297
298if [[ -n "$AGENT_NAME" ]]; then
299 agent_label="$(echo "$AGENT_NAME" | tr '[:lower:]' '[:upper:]')"
300 agent_pad=$((W - 1))
301 [[ "$agent_pad" -lt $((${#agent_label} + 1)) ]] && agent_pad=$((${#agent_label} + 1))
302 printf '%b\n' "$(printf "${AGENT_BG}${AGENT_FG} %-${agent_pad}s${RST}" "$agent_label")"
303fi
304printf '%b\n' "$line1"
305printf '%b\n' "$(progress_bar "$PCT" "ctx" "$BAR_WIDTH" "$CTX_BG" "$CTX_FG")"
306printf '%b\n' "$(progress_bar "$FIVE_HOUR_PCT" "5h" "$BAR_WIDTH" "$DAY_BG" "$DAY_FG")"
307printf '%b\n' "$(progress_bar "$SEVEN_DAY_PCT" "7d" "$BAR_WIDTH" "$WK_BG" "$WK_FG")"
308printf '%b\n' "$line2"