cc-context-stats 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +33 -1
- package/.github/workflows/ci.yml +4 -2
- package/CHANGELOG.md +17 -0
- package/README.md +33 -11
- package/RELEASE_NOTES.md +9 -0
- package/assets/logo/favicon.svg +16 -0
- package/assets/logo/logo-black.svg +23 -0
- package/assets/logo/logo-full.svg +30 -0
- package/assets/logo/logo-icon.svg +26 -0
- package/assets/logo/logo-mark.svg +26 -0
- package/assets/logo/logo-white.svg +23 -0
- package/assets/logo/logo-wordmark.svg +7 -0
- package/install.sh +21 -2
- package/package.json +7 -3
- package/pyproject.toml +6 -4
- package/scripts/context-stats.sh +194 -26
- package/scripts/statusline-full.sh +57 -2
- package/scripts/statusline-git.sh +50 -1
- package/scripts/statusline-minimal.sh +50 -1
- package/scripts/statusline.js +50 -4
- package/scripts/statusline.py +44 -3
- package/src/claude_statusline/__init__.py +1 -1
- package/src/claude_statusline/cli/context_stats.py +106 -34
- package/src/claude_statusline/cli/statusline.py +5 -4
- package/src/claude_statusline/core/config.py +7 -0
- package/src/claude_statusline/formatters/layout.py +52 -0
- package/src/claude_statusline/graphs/renderer.py +44 -24
- package/src/claude_statusline/graphs/statistics.py +34 -0
- package/src/claude_statusline/ui/__init__.py +1 -0
- package/src/claude_statusline/ui/icons.py +93 -0
- package/src/claude_statusline/ui/waiting.py +62 -0
- package/tests/bash/test_statusline_full.bats +30 -0
- package/tests/node/statusline.test.js +44 -3
- package/tests/python/test_icons.py +152 -0
- package/tests/python/test_layout.py +110 -0
- package/tests/python/test_statusline.py +62 -3
- package/tests/python/test_waiting.py +127 -0
- package/PUBLISHING_GUIDE.md +0 -69
- package/npm-publish.sh +0 -33
- package/publish.sh +0 -24
- package/show_raw_claude_code_api.js +0 -11
package/scripts/context-stats.sh
CHANGED
|
@@ -57,6 +57,8 @@ COLOR_ENABLED=true
|
|
|
57
57
|
TOKEN_DETAIL_ENABLED=true
|
|
58
58
|
WATCH_MODE=true
|
|
59
59
|
WATCH_INTERVAL=2
|
|
60
|
+
REDUCED_MOTION=false
|
|
61
|
+
CYCLE_COUNTER=0
|
|
60
62
|
|
|
61
63
|
# === UTILITY FUNCTIONS ===
|
|
62
64
|
|
|
@@ -218,6 +220,144 @@ format_duration() {
|
|
|
218
220
|
fi
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
# === ACTIVITY ICONS & WAITING TEXT ===
|
|
224
|
+
|
|
225
|
+
# Waiting messages for rotating display
|
|
226
|
+
WAITING_MESSAGES="Thinking... Cooking... Crunching_tokens... Compiling_plan... Running_steps... Processing... Working_on_it... Analyzing..."
|
|
227
|
+
WAITING_MSG_COUNT=8
|
|
228
|
+
|
|
229
|
+
get_waiting_text() {
|
|
230
|
+
local cycle=$1
|
|
231
|
+
if [ "$REDUCED_MOTION" = "true" ]; then
|
|
232
|
+
echo "Working..."
|
|
233
|
+
return
|
|
234
|
+
fi
|
|
235
|
+
# Rotate every 2 cycles
|
|
236
|
+
local msg_idx=$(( (cycle / 2) % WAITING_MSG_COUNT + 1 ))
|
|
237
|
+
local msg
|
|
238
|
+
msg=$(echo "$WAITING_MESSAGES" | awk -v n="$msg_idx" '{ print $n }')
|
|
239
|
+
# Replace underscores with spaces
|
|
240
|
+
echo "$msg" | tr '_' ' '
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# Check if session is active (last entry within timeout seconds)
|
|
244
|
+
is_session_active() {
|
|
245
|
+
local last_ts=$1
|
|
246
|
+
local timeout=${2:-30}
|
|
247
|
+
local now
|
|
248
|
+
now=$(date +%s)
|
|
249
|
+
local diff=$((now - last_ts))
|
|
250
|
+
[ "$diff" -le "$timeout" ]
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Detect spike: latest delta > 15% of context window OR > 3x rolling avg of previous deltas
|
|
254
|
+
detect_spike() {
|
|
255
|
+
local deltas_str=$1
|
|
256
|
+
local context_window=$2
|
|
257
|
+
local window=${3:-5}
|
|
258
|
+
|
|
259
|
+
local count
|
|
260
|
+
count=$(echo "$deltas_str" | wc -w | tr -d ' ')
|
|
261
|
+
[ "$count" -eq 0 ] && return 1
|
|
262
|
+
|
|
263
|
+
local latest
|
|
264
|
+
latest=$(echo "$deltas_str" | awk '{ print $NF }')
|
|
265
|
+
|
|
266
|
+
# Absolute threshold: > 15% of context window
|
|
267
|
+
if [ "$context_window" -gt 0 ]; then
|
|
268
|
+
local threshold=$((context_window * 15 / 100))
|
|
269
|
+
[ "$latest" -gt "$threshold" ] && return 0
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
# Relative threshold: > 3x rolling avg of previous deltas
|
|
273
|
+
if [ "$count" -ge 2 ]; then
|
|
274
|
+
# Get previous deltas (exclude last)
|
|
275
|
+
local prev_count=$((count - 1))
|
|
276
|
+
local start=$((prev_count > window ? count - window - 1 : 0))
|
|
277
|
+
local prev_sum=0
|
|
278
|
+
local prev_n=0
|
|
279
|
+
local idx=0
|
|
280
|
+
for d in $deltas_str; do
|
|
281
|
+
if [ "$idx" -ge "$start" ] && [ "$idx" -lt "$prev_count" ]; then
|
|
282
|
+
prev_sum=$((prev_sum + d))
|
|
283
|
+
prev_n=$((prev_n + 1))
|
|
284
|
+
fi
|
|
285
|
+
idx=$((idx + 1))
|
|
286
|
+
done
|
|
287
|
+
if [ "$prev_n" -gt 0 ]; then
|
|
288
|
+
local avg=$((prev_sum / prev_n))
|
|
289
|
+
if [ "$avg" -gt 0 ] && [ "$latest" -gt $((avg * 3)) ]; then
|
|
290
|
+
return 0
|
|
291
|
+
fi
|
|
292
|
+
fi
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
return 1
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Determine activity tier: idle, low, medium, high, spike
|
|
299
|
+
get_activity_tier() {
|
|
300
|
+
local last_ts=$1
|
|
301
|
+
local context_window=$2
|
|
302
|
+
local deltas_str=$3
|
|
303
|
+
|
|
304
|
+
# Check if idle (>30s since last entry)
|
|
305
|
+
if ! is_session_active "$last_ts"; then
|
|
306
|
+
echo "idle"
|
|
307
|
+
return
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
local count
|
|
311
|
+
count=$(echo "$deltas_str" | wc -w | tr -d ' ')
|
|
312
|
+
if [ "$count" -eq 0 ]; then
|
|
313
|
+
echo "idle"
|
|
314
|
+
return
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
local latest
|
|
318
|
+
latest=$(echo "$deltas_str" | awk '{ print $NF }')
|
|
319
|
+
|
|
320
|
+
if [ "$latest" -le 0 ]; then
|
|
321
|
+
echo "idle"
|
|
322
|
+
return
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
# Check spike first
|
|
326
|
+
if detect_spike "$deltas_str" "$context_window"; then
|
|
327
|
+
echo "spike"
|
|
328
|
+
return
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
if [ "$context_window" -le 0 ]; then
|
|
332
|
+
echo "low"
|
|
333
|
+
return
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
# Delta as percentage of context window (x100 for integer math)
|
|
337
|
+
local delta_pct_x100=$((latest * 10000 / context_window))
|
|
338
|
+
|
|
339
|
+
if [ "$delta_pct_x100" -gt 500 ]; then
|
|
340
|
+
echo "high"
|
|
341
|
+
elif [ "$delta_pct_x100" -gt 200 ]; then
|
|
342
|
+
echo "medium"
|
|
343
|
+
else
|
|
344
|
+
echo "low"
|
|
345
|
+
fi
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# Get text label for tier
|
|
349
|
+
get_tier_label() {
|
|
350
|
+
local tier=$1
|
|
351
|
+
case "$tier" in
|
|
352
|
+
idle) echo "Idle" ;;
|
|
353
|
+
low) echo "Low activity" ;;
|
|
354
|
+
medium) echo "Active" ;;
|
|
355
|
+
high) echo "High activity" ;;
|
|
356
|
+
spike) echo "Spike!" ;;
|
|
357
|
+
*) echo "Idle" ;;
|
|
358
|
+
esac
|
|
359
|
+
}
|
|
360
|
+
|
|
221
361
|
# === DATA FUNCTIONS ===
|
|
222
362
|
|
|
223
363
|
migrate_old_state_files() {
|
|
@@ -814,6 +954,11 @@ load_config() {
|
|
|
814
954
|
TOKEN_DETAIL_ENABLED=false
|
|
815
955
|
fi
|
|
816
956
|
;;
|
|
957
|
+
reduced_motion)
|
|
958
|
+
if [ "$value" = "true" ]; then
|
|
959
|
+
REDUCED_MOTION=true
|
|
960
|
+
fi
|
|
961
|
+
;;
|
|
817
962
|
esac
|
|
818
963
|
done <"$CONFIG_FILE"
|
|
819
964
|
fi
|
|
@@ -841,6 +986,26 @@ render_once() {
|
|
|
841
986
|
echo -e "${BOLD}${MAGENTA}Context Stats${RESET} ${DIM}(Session: $session_name)${RESET}"
|
|
842
987
|
fi
|
|
843
988
|
|
|
989
|
+
# Activity indicator (waiting text + label)
|
|
990
|
+
local last_ts
|
|
991
|
+
last_ts=$(get_element "$TIMESTAMPS" "$DATA_COUNT")
|
|
992
|
+
local last_context
|
|
993
|
+
last_context=$(get_element "$CONTEXT_SIZES" "$DATA_COUNT")
|
|
994
|
+
[ -z "$last_context" ] && last_context=0
|
|
995
|
+
|
|
996
|
+
local tier
|
|
997
|
+
tier=$(get_activity_tier "$last_ts" "$last_context" "$DELTAS")
|
|
998
|
+
local label
|
|
999
|
+
label=$(get_tier_label "$tier")
|
|
1000
|
+
|
|
1001
|
+
if is_session_active "$last_ts"; then
|
|
1002
|
+
local wait_text
|
|
1003
|
+
wait_text=$(get_waiting_text "$CYCLE_COUNTER")
|
|
1004
|
+
echo -e " ${DIM}${wait_text} [${label}]${RESET}"
|
|
1005
|
+
else
|
|
1006
|
+
echo -e " ${DIM}${label}${RESET}"
|
|
1007
|
+
fi
|
|
1008
|
+
|
|
844
1009
|
# Render graphs (use CURRENT_USED_TOKENS for actual context window usage)
|
|
845
1010
|
case "$GRAPH_TYPE" in
|
|
846
1011
|
cumulative)
|
|
@@ -893,39 +1058,42 @@ run_watch_mode() {
|
|
|
893
1058
|
printf "%s%s" "${CLEAR_SCREEN}" "${CURSOR_HOME}"
|
|
894
1059
|
|
|
895
1060
|
while true; do
|
|
896
|
-
# Move cursor to home position (top-left) instead of clearing
|
|
897
|
-
# This prevents flickering by overwriting in place
|
|
898
|
-
printf "%s" "${CURSOR_HOME}"
|
|
899
|
-
|
|
900
1061
|
# Re-read terminal dimensions in case of resize
|
|
901
1062
|
get_terminal_dimensions
|
|
902
1063
|
|
|
903
|
-
#
|
|
904
|
-
local
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1064
|
+
# Capture all output into a variable for atomic write
|
|
1065
|
+
local output
|
|
1066
|
+
output=$(
|
|
1067
|
+
# Show watch mode indicator with live timestamp
|
|
1068
|
+
local current_time
|
|
1069
|
+
current_time=$(date +%H:%M:%S)
|
|
1070
|
+
echo -e "${DIM}[LIVE ${current_time}] Refresh: ${WATCH_INTERVAL}s | Ctrl+C to exit${RESET}"
|
|
1071
|
+
|
|
1072
|
+
# Handle case where state_file is empty (no sessions found at all)
|
|
1073
|
+
if [ -z "$state_file" ]; then
|
|
1074
|
+
local wait_msg
|
|
1075
|
+
wait_msg=$(get_waiting_text "$CYCLE_COUNTER")
|
|
1076
|
+
show_waiting_message "" "$wait_msg"
|
|
1077
|
+
# Re-validate and render (file might have new data)
|
|
1078
|
+
elif [ -f "$state_file" ]; then
|
|
1079
|
+
local line_count
|
|
1080
|
+
line_count=$(wc -l <"$state_file" | tr -d ' ')
|
|
1081
|
+
if [ "$line_count" -ge 2 ]; then
|
|
1082
|
+
render_once "$state_file"
|
|
1083
|
+
else
|
|
1084
|
+
show_waiting_message "$SESSION_ID" "Waiting for more data points..."
|
|
1085
|
+
echo -e " ${DIM}Current: ${line_count} point(s), need at least 2${RESET}"
|
|
1086
|
+
fi
|
|
917
1087
|
else
|
|
918
|
-
|
|
919
|
-
|
|
1088
|
+
# File doesn't exist yet (new session)
|
|
1089
|
+
show_waiting_message "$SESSION_ID" "Waiting for session data..."
|
|
920
1090
|
fi
|
|
921
|
-
|
|
922
|
-
# File doesn't exist yet (new session)
|
|
923
|
-
show_waiting_message "$SESSION_ID" "Waiting for session data..."
|
|
924
|
-
fi
|
|
1091
|
+
)
|
|
925
1092
|
|
|
926
|
-
#
|
|
927
|
-
printf "%s" "${CLEAR_TO_END}"
|
|
1093
|
+
# Atomic write: CURSOR_HOME + content + CLEAR_TO_END (clean up stale trailing lines)
|
|
1094
|
+
printf "%s%s\n%s" "${CURSOR_HOME}" "$output" "${CLEAR_TO_END}"
|
|
928
1095
|
|
|
1096
|
+
CYCLE_COUNTER=$((CYCLE_COUNTER + 1))
|
|
929
1097
|
sleep "$WATCH_INTERVAL"
|
|
930
1098
|
done
|
|
931
1099
|
}
|
|
@@ -105,9 +105,61 @@ if [[ -f ~/.claude/statusline.conf ]]; then
|
|
|
105
105
|
if [[ "$show_session" == "false" ]]; then
|
|
106
106
|
show_session_enabled=false
|
|
107
107
|
fi
|
|
108
|
-
# Note: show_io_tokens setting is read but not yet implemented
|
|
109
108
|
fi
|
|
110
109
|
|
|
110
|
+
# Width-fitting helpers
|
|
111
|
+
visible_width() {
|
|
112
|
+
# Strip ANSI escape sequences (both literal \033 and actual ESC byte) and return string length
|
|
113
|
+
local stripped
|
|
114
|
+
stripped=$(printf '%s' "$1" | sed -e $'s/\033\[[0-9;]*m//g' -e 's/\\033\[[0-9;]*m//g')
|
|
115
|
+
printf '%s' "$stripped" | wc -m | tr -d ' '
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get_terminal_width() {
|
|
119
|
+
# Return terminal width, fallback to 80
|
|
120
|
+
if [[ -n "$COLUMNS" ]]; then
|
|
121
|
+
echo "$COLUMNS"
|
|
122
|
+
else
|
|
123
|
+
local cols
|
|
124
|
+
cols=$(tput cols 2>/dev/null || echo 80)
|
|
125
|
+
echo "$cols"
|
|
126
|
+
fi
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fit_to_width() {
|
|
130
|
+
# Assemble parts into a single line that fits within max_width.
|
|
131
|
+
# Usage: fit_to_width max_width part1 part2 part3 ...
|
|
132
|
+
# First part (base) is always included. Subsequent parts are
|
|
133
|
+
# included only if adding them does not exceed max_width.
|
|
134
|
+
local max_width=$1
|
|
135
|
+
shift
|
|
136
|
+
local parts=("$@")
|
|
137
|
+
|
|
138
|
+
if [[ ${#parts[@]} -eq 0 ]]; then
|
|
139
|
+
echo ""
|
|
140
|
+
return
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
local result="${parts[0]}"
|
|
144
|
+
local current_width
|
|
145
|
+
current_width=$(visible_width "$result")
|
|
146
|
+
|
|
147
|
+
for ((i = 1; i < ${#parts[@]}; i++)); do
|
|
148
|
+
local part="${parts[$i]}"
|
|
149
|
+
if [[ -z "$part" ]]; then
|
|
150
|
+
continue
|
|
151
|
+
fi
|
|
152
|
+
local part_width
|
|
153
|
+
part_width=$(visible_width "$part")
|
|
154
|
+
if (( current_width + part_width <= max_width )); then
|
|
155
|
+
result+="$part"
|
|
156
|
+
(( current_width += part_width ))
|
|
157
|
+
fi
|
|
158
|
+
done
|
|
159
|
+
|
|
160
|
+
echo -e "$result"
|
|
161
|
+
}
|
|
162
|
+
|
|
111
163
|
# Calculate context window - show remaining free space
|
|
112
164
|
context_info=""
|
|
113
165
|
total_size=$(echo "$input" | jq -r '.context_window.context_window_size // 0')
|
|
@@ -212,6 +264,7 @@ if [[ "$total_size" -gt 0 && "$current_usage" != "null" ]]; then
|
|
|
212
264
|
fi
|
|
213
265
|
# Calculate delta
|
|
214
266
|
delta=$((used_tokens - prev_tokens))
|
|
267
|
+
# delta calculated for display
|
|
215
268
|
# Only show positive delta (and skip first run when no previous state)
|
|
216
269
|
if [[ "$has_prev" == "true" && "$delta" -gt 0 ]]; then
|
|
217
270
|
if [[ "$token_detail_enabled" == "true" ]]; then
|
|
@@ -238,4 +291,6 @@ if [[ "$show_session_enabled" == "true" && -n "$session_id" ]]; then
|
|
|
238
291
|
fi
|
|
239
292
|
|
|
240
293
|
# Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [S:session_id]
|
|
241
|
-
|
|
294
|
+
base="${DIM}[${model}]${RESET} ${BLUE}${dir_name}${RESET}"
|
|
295
|
+
max_width=$(get_terminal_width)
|
|
296
|
+
fit_to_width "$max_width" "$base" "$git_info" "$context_info" "$delta_info" "$ac_info" "$session_info"
|
|
@@ -14,6 +14,53 @@ MODEL_DISPLAY=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
|
|
14
14
|
CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir // "~"')
|
|
15
15
|
DIR_NAME="${CURRENT_DIR##*/}"
|
|
16
16
|
|
|
17
|
+
# Width-fitting helpers
|
|
18
|
+
visible_width() {
|
|
19
|
+
local stripped
|
|
20
|
+
stripped=$(printf '%s' "$1" | sed -e $'s/\033\[[0-9;]*m//g' -e 's/\\033\[[0-9;]*m//g')
|
|
21
|
+
printf '%s' "$stripped" | wc -m | tr -d ' '
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get_terminal_width() {
|
|
25
|
+
if [[ -n "$COLUMNS" ]]; then
|
|
26
|
+
echo "$COLUMNS"
|
|
27
|
+
else
|
|
28
|
+
local cols
|
|
29
|
+
cols=$(tput cols 2>/dev/null || echo 80)
|
|
30
|
+
echo "$cols"
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fit_to_width() {
|
|
35
|
+
local max_width=$1
|
|
36
|
+
shift
|
|
37
|
+
local parts=("$@")
|
|
38
|
+
|
|
39
|
+
if [[ ${#parts[@]} -eq 0 ]]; then
|
|
40
|
+
echo ""
|
|
41
|
+
return
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
local result="${parts[0]}"
|
|
45
|
+
local current_width
|
|
46
|
+
current_width=$(visible_width "$result")
|
|
47
|
+
|
|
48
|
+
for ((i = 1; i < ${#parts[@]}; i++)); do
|
|
49
|
+
local part="${parts[$i]}"
|
|
50
|
+
if [[ -z "$part" ]]; then
|
|
51
|
+
continue
|
|
52
|
+
fi
|
|
53
|
+
local part_width
|
|
54
|
+
part_width=$(visible_width "$part")
|
|
55
|
+
if (( current_width + part_width <= max_width )); then
|
|
56
|
+
result+="$part"
|
|
57
|
+
(( current_width += part_width ))
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
echo -e "$result"
|
|
62
|
+
}
|
|
63
|
+
|
|
17
64
|
# Git branch detection
|
|
18
65
|
GIT_INFO=""
|
|
19
66
|
if git -C "$CURRENT_DIR" rev-parse --git-dir > /dev/null 2>&1; then
|
|
@@ -29,4 +76,6 @@ if git -C "$CURRENT_DIR" rev-parse --git-dir > /dev/null 2>&1; then
|
|
|
29
76
|
fi
|
|
30
77
|
fi
|
|
31
78
|
|
|
32
|
-
|
|
79
|
+
base="[${MODEL_DISPLAY}] ${BLUE}${DIR_NAME}${RESET}"
|
|
80
|
+
max_width=$(get_terminal_width)
|
|
81
|
+
fit_to_width "$max_width" "$base" "$GIT_INFO"
|
|
@@ -8,4 +8,53 @@ MODEL_DISPLAY=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
|
|
8
8
|
CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir // "~"')
|
|
9
9
|
DIR_NAME="${CURRENT_DIR##*/}"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# Width-fitting helpers
|
|
12
|
+
visible_width() {
|
|
13
|
+
local stripped
|
|
14
|
+
stripped=$(printf '%s' "$1" | sed -e $'s/\033\[[0-9;]*m//g' -e 's/\\033\[[0-9;]*m//g')
|
|
15
|
+
printf '%s' "$stripped" | wc -m | tr -d ' '
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get_terminal_width() {
|
|
19
|
+
if [[ -n "$COLUMNS" ]]; then
|
|
20
|
+
echo "$COLUMNS"
|
|
21
|
+
else
|
|
22
|
+
local cols
|
|
23
|
+
cols=$(tput cols 2>/dev/null || echo 80)
|
|
24
|
+
echo "$cols"
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fit_to_width() {
|
|
29
|
+
local max_width=$1
|
|
30
|
+
shift
|
|
31
|
+
local parts=("$@")
|
|
32
|
+
|
|
33
|
+
if [[ ${#parts[@]} -eq 0 ]]; then
|
|
34
|
+
echo ""
|
|
35
|
+
return
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
local result="${parts[0]}"
|
|
39
|
+
local current_width
|
|
40
|
+
current_width=$(visible_width "$result")
|
|
41
|
+
|
|
42
|
+
for ((i = 1; i < ${#parts[@]}; i++)); do
|
|
43
|
+
local part="${parts[$i]}"
|
|
44
|
+
if [[ -z "$part" ]]; then
|
|
45
|
+
continue
|
|
46
|
+
fi
|
|
47
|
+
local part_width
|
|
48
|
+
part_width=$(visible_width "$part")
|
|
49
|
+
if (( current_width + part_width <= max_width )); then
|
|
50
|
+
result+="$part"
|
|
51
|
+
(( current_width += part_width ))
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
echo -e "$result"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
base="[$MODEL_DISPLAY] $DIR_NAME"
|
|
59
|
+
max_width=$(get_terminal_width)
|
|
60
|
+
fit_to_width "$max_width" "$base"
|
package/scripts/statusline.js
CHANGED
|
@@ -42,6 +42,48 @@ const RED = '\x1b[0;31m';
|
|
|
42
42
|
const DIM = '\x1b[2m';
|
|
43
43
|
const RESET = '\x1b[0m';
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Return the visible width of a string after stripping ANSI escape sequences.
|
|
47
|
+
*/
|
|
48
|
+
function visibleWidth(s) {
|
|
49
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return the terminal width in columns, defaulting to 80.
|
|
54
|
+
*/
|
|
55
|
+
function getTerminalWidth() {
|
|
56
|
+
return process.stdout.columns || parseInt(process.env.COLUMNS, 10) || 80;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Assemble parts into a single line that fits within maxWidth.
|
|
61
|
+
* Parts are added in priority order (first = highest priority).
|
|
62
|
+
* The first part (base) is always included.
|
|
63
|
+
*/
|
|
64
|
+
function fitToWidth(parts, maxWidth) {
|
|
65
|
+
if (!parts.length) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let result = parts[0];
|
|
70
|
+
let currentWidth = visibleWidth(result);
|
|
71
|
+
|
|
72
|
+
for (let i = 1; i < parts.length; i++) {
|
|
73
|
+
const part = parts[i];
|
|
74
|
+
if (!part) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const partWidth = visibleWidth(part);
|
|
78
|
+
if (currentWidth + partWidth <= maxWidth) {
|
|
79
|
+
result += part;
|
|
80
|
+
currentWidth += partWidth;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
45
87
|
function getGitInfo(projectDir) {
|
|
46
88
|
const gitDir = path.join(projectDir, '.git');
|
|
47
89
|
if (!fs.existsSync(gitDir) || !fs.statSync(gitDir).isDirectory()) {
|
|
@@ -84,6 +126,7 @@ function readConfig() {
|
|
|
84
126
|
showDelta: true,
|
|
85
127
|
showSession: true,
|
|
86
128
|
showIoTokens: true,
|
|
129
|
+
reducedMotion: false,
|
|
87
130
|
};
|
|
88
131
|
const configPath = path.join(os.homedir(), '.claude', 'statusline.conf');
|
|
89
132
|
|
|
@@ -134,6 +177,8 @@ show_session=true
|
|
|
134
177
|
config.showSession = valueTrimmed !== 'false';
|
|
135
178
|
} else if (keyTrimmed === 'show_io_tokens') {
|
|
136
179
|
config.showIoTokens = valueTrimmed !== 'false';
|
|
180
|
+
} else if (keyTrimmed === 'reduced_motion') {
|
|
181
|
+
config.reducedMotion = valueTrimmed !== 'false';
|
|
137
182
|
}
|
|
138
183
|
}
|
|
139
184
|
} catch {
|
|
@@ -343,8 +388,9 @@ process.stdin.on('end', () => {
|
|
|
343
388
|
sessionInfo = ` ${DIM}${sessionId}${RESET}`;
|
|
344
389
|
}
|
|
345
390
|
|
|
346
|
-
// Output: [Model]
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
391
|
+
// Output: [Model] dir | branch [n] | free (%) [+delta] [AC] session
|
|
392
|
+
const base = `${DIM}[${model}]${RESET} ${BLUE}${dirName}${RESET}`;
|
|
393
|
+
const maxWidth = getTerminalWidth();
|
|
394
|
+
const parts = [base, gitInfo, contextInfo, deltaInfo, acInfo, sessionInfo];
|
|
395
|
+
console.log(fitToWidth(parts, maxWidth));
|
|
350
396
|
});
|
package/scripts/statusline.py
CHANGED
|
@@ -26,6 +26,8 @@ State file format (CSV):
|
|
|
26
26
|
|
|
27
27
|
import json
|
|
28
28
|
import os
|
|
29
|
+
import re
|
|
30
|
+
import shutil
|
|
29
31
|
import subprocess
|
|
30
32
|
import sys
|
|
31
33
|
|
|
@@ -39,6 +41,44 @@ RED = "\033[0;31m"
|
|
|
39
41
|
DIM = "\033[2m"
|
|
40
42
|
RESET = "\033[0m"
|
|
41
43
|
|
|
44
|
+
# Pattern to strip ANSI escape sequences
|
|
45
|
+
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def visible_width(s):
|
|
49
|
+
"""Return the visible width of a string after stripping ANSI escape sequences."""
|
|
50
|
+
return len(_ANSI_RE.sub("", s))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_terminal_width():
|
|
54
|
+
"""Return the terminal width in columns, defaulting to 80."""
|
|
55
|
+
return shutil.get_terminal_size().columns
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def fit_to_width(parts, max_width):
|
|
59
|
+
"""Assemble parts into a single line that fits within max_width.
|
|
60
|
+
|
|
61
|
+
Parts are added in priority order (first = highest priority).
|
|
62
|
+
The first part (base) is always included. Subsequent parts are
|
|
63
|
+
included only if adding them does not exceed max_width.
|
|
64
|
+
Empty parts are skipped.
|
|
65
|
+
"""
|
|
66
|
+
if not parts:
|
|
67
|
+
return ""
|
|
68
|
+
|
|
69
|
+
result = parts[0]
|
|
70
|
+
current_width = visible_width(result)
|
|
71
|
+
|
|
72
|
+
for part in parts[1:]:
|
|
73
|
+
if not part:
|
|
74
|
+
continue
|
|
75
|
+
part_width = visible_width(part)
|
|
76
|
+
if current_width + part_width <= max_width:
|
|
77
|
+
result += part
|
|
78
|
+
current_width += part_width
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
|
|
42
82
|
|
|
43
83
|
def get_git_info(project_dir):
|
|
44
84
|
"""Get git branch and change count"""
|
|
@@ -303,9 +343,10 @@ def main():
|
|
|
303
343
|
session_info = f" {DIM}{session_id}{RESET}"
|
|
304
344
|
|
|
305
345
|
# Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [S:session_id]
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
346
|
+
base = f"{DIM}[{model}]{RESET} {BLUE}{dir_name}{RESET}"
|
|
347
|
+
max_width = get_terminal_width()
|
|
348
|
+
parts = [base, git_info, context_info, delta_info, ac_info, session_info]
|
|
349
|
+
print(fit_to_width(parts, max_width))
|
|
309
350
|
|
|
310
351
|
|
|
311
352
|
if __name__ == "__main__":
|