skill-statusline 2.1.1 → 2.2.1

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/lib/core.sh CHANGED
@@ -1,382 +1,327 @@
1
- #!/usr/bin/env bash
2
- # skill-statusline v2.0 — Core engine
3
- # Reads Claude Code JSON from stdin, computes all fields, renders via layout
4
-
5
- STATUSLINE_DIR="${HOME}/.claude/statusline"
6
- CONFIG_FILE="${HOME}/.claude/statusline-config.json"
7
-
8
- # ── 0. Read stdin JSON ──
9
- input=$(cat)
10
-
11
- # ── 1. Source modules ──
12
- source "${STATUSLINE_DIR}/json-parser.sh"
13
- source "${STATUSLINE_DIR}/helpers.sh"
14
-
15
- # ── 2. Load config ──
16
- active_theme="default"
17
- active_layout="standard"
18
- cfg_warn_threshold=85
19
- cfg_bar_width=40
20
- cfg_show_burn_rate="false"
21
- cfg_show_vim="true"
22
- cfg_show_agent="true"
23
-
24
- if [ -f "$CONFIG_FILE" ]; then
25
- _cfg=$(cat "$CONFIG_FILE" 2>/dev/null)
26
- _t=$(echo "$_cfg" | grep -o '"theme"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/')
27
- _l=$(echo "$_cfg" | grep -o '"layout"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/')
28
- [ -n "$_t" ] && active_theme="$_t"
29
- [ -n "$_l" ] && active_layout="$_l"
30
- _w=$(echo "$_cfg" | grep -o '"compaction_warning_threshold"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
31
- _bw=$(echo "$_cfg" | grep -o '"bar_width"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
32
- _br=$(echo "$_cfg" | grep -o '"show_burn_rate"[[:space:]]*:[[:space:]]*true' | head -1)
33
- _sv=$(echo "$_cfg" | grep -o '"show_vim_mode"[[:space:]]*:[[:space:]]*false' | head -1)
34
- _sa=$(echo "$_cfg" | grep -o '"show_agent_name"[[:space:]]*:[[:space:]]*false' | head -1)
35
- [ -n "$_w" ] && cfg_warn_threshold="$_w"
36
- [ -n "$_bw" ] && cfg_bar_width="$_bw"
37
- [ -n "$_br" ] && cfg_show_burn_rate="true"
38
- [ -n "$_sv" ] && cfg_show_vim="false"
39
- [ -n "$_sa" ] && cfg_show_agent="false"
40
- fi
41
-
42
- # Allow env override (for ccsl preview)
43
- [ -n "$STATUSLINE_THEME_OVERRIDE" ] && active_theme="$STATUSLINE_THEME_OVERRIDE"
44
- [ -n "$STATUSLINE_LAYOUT_OVERRIDE" ] && active_layout="$STATUSLINE_LAYOUT_OVERRIDE"
45
-
46
- # ── 3. Source theme ──
47
- theme_file="${STATUSLINE_DIR}/themes/${active_theme}.sh"
48
- if [ -f "$theme_file" ]; then
49
- source "$theme_file"
50
- else
51
- source "${STATUSLINE_DIR}/themes/default.sh"
52
- fi
53
-
54
- # ── 4. Terminal width detection ──
55
- SL_TERM_WIDTH=${COLUMNS:-0}
56
- if [ "$SL_TERM_WIDTH" -eq 0 ] 2>/dev/null; then
57
- _tw=$(tput cols 2>/dev/null)
58
- [ -n "$_tw" ] && [ "$_tw" -gt 0 ] && SL_TERM_WIDTH="$_tw"
59
- fi
60
- [ "$SL_TERM_WIDTH" -eq 0 ] && SL_TERM_WIDTH=80
61
-
62
- # Auto-downgrade layout for narrow terminals
63
- if [ "$SL_TERM_WIDTH" -lt 60 ]; then
64
- active_layout="compact"
65
- elif [ "$SL_TERM_WIDTH" -lt 80 ] && [ "$active_layout" = "full" ]; then
66
- active_layout="standard"
67
- fi
68
-
69
- # Dynamic bar width
70
- BAR_WIDTH="$cfg_bar_width"
71
- if [ "$SL_TERM_WIDTH" -gt 100 ]; then
72
- _dyn=$(( SL_TERM_WIDTH - 20 ))
73
- [ "$_dyn" -gt 60 ] && _dyn=60
74
- [ "$_dyn" -gt "$BAR_WIDTH" ] && BAR_WIDTH="$_dyn"
75
- elif [ "$SL_TERM_WIDTH" -lt 70 ]; then
76
- BAR_WIDTH=20
77
- fi
78
-
79
- # ── 5. Initialize cache ──
80
- _sl_cache_init
81
-
82
- # ── 6. Parse ALL JSON fields ──
83
-
84
- # --- 6a. Directory ---
85
- SL_CWD=$(json_nested_val "workspace" "current_dir")
86
- [ -z "$SL_CWD" ] && SL_CWD=$(json_val "cwd")
87
- if [ -z "$SL_CWD" ]; then
88
- SL_DIR="~"
89
- clean_cwd=""
90
- else
91
- clean_cwd=$(to_fwd "$SL_CWD")
92
- SL_DIR=$(echo "$clean_cwd" | awk -F'/' '{if(NF>3) print $(NF-2)"/"$(NF-1)"/"$NF; else if(NF>2) print $(NF-1)"/"$NF; else print $0}')
93
- [ -z "$SL_DIR" ] && SL_DIR="~"
94
- fi
95
-
96
- # --- 6b. Model ---
97
- SL_MODEL_DISPLAY=$(json_nested_val "model" "display_name")
98
- SL_MODEL_ID=$(json_nested_val "model" "id")
99
- # Flat fallback
100
- [ -z "$SL_MODEL_DISPLAY" ] && SL_MODEL_DISPLAY=$(json_val "display_name")
101
- [ -z "$SL_MODEL_ID" ] && SL_MODEL_ID=$(json_val "id")
102
- [ -z "$SL_MODEL_DISPLAY" ] && SL_MODEL_DISPLAY="unknown"
103
-
104
- model_ver=""
105
- if [ -n "$SL_MODEL_ID" ]; then
106
- model_ver=$(echo "$SL_MODEL_ID" | sed -n 's/.*-\([0-9]*\)-\([0-9]*\)$/\1.\2/p')
107
- fi
108
- if [ -n "$model_ver" ] && ! echo "$SL_MODEL_DISPLAY" | grep -q '[0-9]'; then
109
- SL_MODEL="${SL_MODEL_DISPLAY} ${model_ver}"
110
- else
111
- SL_MODEL="$SL_MODEL_DISPLAY"
112
- fi
113
-
114
- # --- 6c. Context ACCURATE computation from current_usage ---
115
- ctx_size=$(json_nested "context_window" "context_window_size")
116
- cur_input=$(json_deep "context_window" "current_usage" "input_tokens")
117
- cur_output=$(json_deep "context_window" "current_usage" "output_tokens")
118
- cur_cache_create=$(json_deep "context_window" "current_usage" "cache_creation_input_tokens")
119
- cur_cache_read=$(json_deep "context_window" "current_usage" "cache_read_input_tokens")
120
-
121
- [ -z "$ctx_size" ] && ctx_size=200000
122
- [ -z "$cur_input" ] && cur_input=0
123
- [ -z "$cur_output" ] && cur_output=0
124
- [ -z "$cur_cache_create" ] && cur_cache_create=0
125
- [ -z "$cur_cache_read" ] && cur_cache_read=0
126
-
127
- # Claude's formula: input + cache_creation + cache_read (output excluded from context %)
128
- ctx_used=$(awk -v a="$cur_input" -v b="$cur_cache_create" -v c="$cur_cache_read" \
129
- 'BEGIN { printf "%d", a + b + c }')
130
-
131
- # Self-calculated percentage
132
- calc_pct=0
133
- if [ "$cur_input" -gt 0 ] 2>/dev/null; then
134
- calc_pct=$(awk -v used="$ctx_used" -v total="$ctx_size" \
135
- 'BEGIN { if (total > 0) printf "%d", (used * 100) / total; else print 0 }')
136
- fi
137
-
138
- # Reported percentage as fallback
139
- reported_pct=$(json_nested "context_window" "used_percentage")
140
-
141
- # Use self-calculated if we have current_usage data, else fallback
142
- if [ "$cur_input" -gt 0 ] 2>/dev/null; then
143
- SL_CTX_PCT="$calc_pct"
144
- elif [ -n "$reported_pct" ] && [ "$reported_pct" != "null" ]; then
145
- SL_CTX_PCT=$(echo "$reported_pct" | cut -d. -f1)
146
- else
147
- SL_CTX_PCT=0
148
- fi
149
-
150
- SL_CTX_REMAINING=$(( 100 - SL_CTX_PCT ))
151
- [ "$SL_CTX_REMAINING" -lt 0 ] && SL_CTX_REMAINING=0
152
-
153
- # Context color
154
- if [ "$SL_CTX_PCT" -gt 90 ] 2>/dev/null; then
155
- CTX_CLR="$CLR_CTX_CRIT"
156
- elif [ "$SL_CTX_PCT" -gt 75 ] 2>/dev/null; then
157
- CTX_CLR="$CLR_CTX_HIGH"
158
- elif [ "$SL_CTX_PCT" -gt 40 ] 2>/dev/null; then
159
- CTX_CLR="$CLR_CTX_MED"
160
- else
161
- CTX_CLR="$CLR_CTX_LOW"
162
- fi
163
-
164
- # Build context bar
165
- filled=$(( SL_CTX_PCT * BAR_WIDTH / 100 ))
166
- [ "$filled" -gt "$BAR_WIDTH" ] && filled=$BAR_WIDTH
167
- empty=$(( BAR_WIDTH - filled ))
168
- bar_filled=""; bar_empty=""
169
- i=0; while [ $i -lt $filled ]; do bar_filled="${bar_filled}${BAR_FILLED}"; i=$((i+1)); done
170
- i=0; while [ $i -lt $empty ]; do bar_empty="${bar_empty}${BAR_EMPTY}"; i=$((i+1)); done
171
- SL_CTX_BAR="${CTX_CLR}${bar_filled}${CLR_RST}${CLR_BAR_EMPTY}${bar_empty}${CLR_RST} ${CTX_CLR}${SL_CTX_PCT}%${CLR_RST}"
172
-
173
- # Compaction warning
174
- SL_COMPACT_WARNING=""
175
- if [ "$SL_CTX_PCT" -ge 95 ] 2>/dev/null; then
176
- SL_COMPACT_WARNING=" ${CLR_CTX_CRIT}${CLR_BOLD}COMPACTING${CLR_RST}"
177
- elif [ "$SL_CTX_PCT" -ge "$cfg_warn_threshold" ] 2>/dev/null; then
178
- SL_COMPACT_WARNING=" ${CLR_CTX_HIGH}${SL_CTX_REMAINING}% left${CLR_RST}"
179
- fi
180
-
181
- # --- 6d. GitHub (with caching) ---
182
- SL_BRANCH="no-git"
183
- SL_GIT_DIRTY=""
184
- SL_GITHUB=""
185
- gh_user=""
186
- gh_repo=""
187
-
188
- if [ -n "$clean_cwd" ]; then
189
- SL_BRANCH=$(cache_get "git-branch" "git --no-optional-locks -C '$clean_cwd' symbolic-ref --short HEAD 2>/dev/null || git --no-optional-locks -C '$clean_cwd' rev-parse --short HEAD 2>/dev/null" 5)
190
- [ -z "$SL_BRANCH" ] && SL_BRANCH="no-git"
191
-
192
- if [ "$SL_BRANCH" != "no-git" ]; then
193
- remote_url=$(cache_get "git-remote" "git --no-optional-locks -C '$clean_cwd' remote get-url origin" 10)
194
- if [ -n "$remote_url" ]; then
195
- gh_user=$(echo "$remote_url" | sed 's|.*github\.com[:/]\([^/]*\)/.*|\1|')
196
- [ "$gh_user" = "$remote_url" ] && gh_user=""
197
- gh_repo=$(echo "$remote_url" | sed 's|.*/\([^/]*\)\.git$|\1|; s|.*/\([^/]*\)$|\1|')
198
- [ "$gh_repo" = "$remote_url" ] && gh_repo=""
199
- fi
200
-
201
- # Dirty check (shorter cache — changes more often)
202
- _staged=$(cache_get "git-staged" "git --no-optional-locks -C '$clean_cwd' diff --cached --quiet 2>/dev/null && echo clean || echo dirty" 3)
203
- _unstaged=$(cache_get "git-unstaged" "git --no-optional-locks -C '$clean_cwd' diff --quiet 2>/dev/null && echo clean || echo dirty" 3)
204
- [ "$_staged" = "dirty" ] && SL_GIT_DIRTY="${CLR_GIT_STAGED}+${CLR_RST}"
205
- [ "$_unstaged" = "dirty" ] && SL_GIT_DIRTY="${SL_GIT_DIRTY}${CLR_GIT_UNSTAGED}~${CLR_RST}"
206
- fi
207
- fi
208
-
209
- if [ -n "$gh_repo" ]; then
210
- SL_GITHUB="${gh_user}/${gh_repo}/${SL_BRANCH}"
211
- else
212
- SL_GITHUB="$SL_BRANCH"
213
- fi
214
-
215
- # --- 6e. Cost ---
216
- cost_raw=$(json_nested "cost" "total_cost_usd")
217
- [ -z "$cost_raw" ] && cost_raw=$(json_num "total_cost_usd")
218
- if [ -z "$cost_raw" ] || [ "$cost_raw" = "0" ]; then
219
- SL_COST='$0.00'
220
- else
221
- SL_COST=$(awk -v c="$cost_raw" 'BEGIN { if (c < 0.01) printf "$%.4f", c; else printf "$%.2f", c }')
222
- fi
223
-
224
- # --- 6f. Tokens (window vs cumulative) ---
225
- # Current window tokens (what's actually loaded — accurate)
226
- [ -z "$cur_input" ] && cur_input=0
227
- [ -z "$cur_output" ] && cur_output=0
228
- SL_TOKENS_WIN_IN=$(fmt_tok "$cur_input")
229
- SL_TOKENS_WIN_OUT=$(fmt_tok "$cur_output")
230
-
231
- # Cumulative session tokens (grows forever, for reference)
232
- cum_input=$(json_nested "context_window" "total_input_tokens")
233
- cum_output=$(json_nested "context_window" "total_output_tokens")
234
- # Flat fallback
235
- [ -z "$cum_input" ] && cum_input=$(json_num "total_input_tokens")
236
- [ -z "$cum_output" ] && cum_output=$(json_num "total_output_tokens")
237
- [ -z "$cum_input" ] && cum_input=0
238
- [ -z "$cum_output" ] && cum_output=0
239
- SL_TOKENS_CUM_IN=$(fmt_tok "$cum_input")
240
- SL_TOKENS_CUM_OUT=$(fmt_tok "$cum_output")
241
-
242
- # --- 6g. Skill detection (with caching) ---
243
- SL_SKILL="Idle"
244
-
245
- _detect_skill() {
246
- local cwd="$1"
247
- local tpath="" search_path="$cwd" proj_hash proj_dir
248
-
249
- while [ -n "$search_path" ] && [ "$search_path" != "/" ]; do
250
- proj_hash=$(echo "$search_path" | sed 's|^/\([a-zA-Z]\)/|\U\1--|; s|^[A-Z]:/|&|; s|:/|--|; s|/|-|g')
251
- proj_dir="$HOME/.claude/projects/${proj_hash}"
252
- if [ -d "$proj_dir" ]; then
253
- tpath=$(ls -t "$proj_dir"/*.jsonl 2>/dev/null | head -1)
254
- [ -n "$tpath" ] && break
255
- fi
256
- search_path=$(echo "$search_path" | sed 's|/[^/]*$||')
257
- done
258
-
259
- if [ -n "$tpath" ] && [ -f "$tpath" ]; then
260
- local recent_block last_tool
261
- recent_block=$(tail -200 "$tpath" 2>/dev/null)
262
- last_tool=$(echo "$recent_block" | grep -o '"type":"tool_use","id":"[^"]*","name":"[^"]*"' | tail -1 | sed 's/.*"name":"\([^"]*\)".*/\1/')
263
-
264
- if [ -n "$last_tool" ]; then
265
- case "$last_tool" in
266
- Task)
267
- local agent_count
268
- agent_count=$(echo "$recent_block" | grep -c '"type":"tool_use","id":"[^"]*","name":"Task"')
269
- if [ "$agent_count" -gt 1 ]; then
270
- echo "${agent_count} Agents"
271
- else
272
- local agent_desc
273
- agent_desc=$(echo "$recent_block" | grep -o '"description":"[^"]*"' | tail -1 | sed 's/"description":"//;s/"$//')
274
- if [ -n "$agent_desc" ]; then
275
- echo "Agent($(echo "$agent_desc" | cut -c1-20))"
276
- else
277
- echo "Agent"
278
- fi
279
- fi ;;
280
- Read) echo "Read" ;;
281
- Write) echo "Write" ;;
282
- Edit) echo "Edit" ;;
283
- MultiEdit) echo "Multi Edit" ;;
284
- Glob) echo "Search(Files)" ;;
285
- Grep) echo "Search(Content)" ;;
286
- Bash) echo "Terminal" ;;
287
- WebSearch) echo "Web Search" ;;
288
- WebFetch) echo "Web Fetch" ;;
289
- Skill) echo "Skill" ;;
290
- AskUserQuestion) echo "Asking..." ;;
291
- EnterPlanMode) echo "Planning" ;;
292
- ExitPlanMode) echo "Plan Ready" ;;
293
- TaskCreate) echo "Task Create" ;;
294
- TaskUpdate) echo "Task Update" ;;
295
- TaskGet) echo "Task Get" ;;
296
- TaskList) echo "Task List" ;;
297
- TaskStop) echo "Task Stop" ;;
298
- TaskOutput) echo "Task Output" ;;
299
- NotebookEdit) echo "Notebook" ;;
300
- *) echo "$last_tool" ;;
301
- esac
302
- return
303
- fi
304
- fi
305
-
306
- # Fallback: check .ccs/task.md
307
- local task_file="${cwd}/.ccs/task.md"
308
- if [ -f "$task_file" ]; then
309
- local last_skill
310
- last_skill=$(grep -oE '/ccs-[a-z]+' "$task_file" 2>/dev/null | tail -1)
311
- [ -n "$last_skill" ] && { echo "$last_skill"; return; }
312
- fi
313
-
314
- echo "Idle"
315
- }
316
-
317
- if [ -n "$clean_cwd" ]; then
318
- SL_SKILL=$(cache_get "skill-label" "_detect_skill '$clean_cwd'" 2)
319
- fi
320
-
321
- # --- 6h. New fields ---
322
-
323
- # Session duration
324
- dur_ms=$(json_nested "cost" "total_duration_ms")
325
- [ -z "$dur_ms" ] && dur_ms=0
326
- SL_DURATION=$(fmt_duration "$dur_ms")
327
-
328
- # Lines changed
329
- SL_LINES_ADDED=$(json_nested "cost" "total_lines_added")
330
- SL_LINES_REMOVED=$(json_nested "cost" "total_lines_removed")
331
- [ -z "$SL_LINES_ADDED" ] && SL_LINES_ADDED=0
332
- [ -z "$SL_LINES_REMOVED" ] && SL_LINES_REMOVED=0
333
-
334
- # API duration
335
- api_ms=$(json_nested "cost" "total_api_duration_ms")
336
- [ -z "$api_ms" ] && api_ms=0
337
- SL_API_DURATION=$(fmt_duration "$api_ms")
338
-
339
- # Vim mode (absent when vim is off)
340
- SL_VIM_MODE=""
341
- if [ "$cfg_show_vim" = "true" ]; then
342
- SL_VIM_MODE=$(json_nested_val "vim" "mode")
343
- fi
344
-
345
- # Agent name (absent when not in agent mode)
346
- SL_AGENT_NAME=""
347
- if [ "$cfg_show_agent" = "true" ]; then
348
- SL_AGENT_NAME=$(json_nested_val "agent" "name")
349
- fi
350
-
351
- # Cache stats (formatted)
352
- SL_CACHE_CREATE=$(fmt_tok "$cur_cache_create")
353
- SL_CACHE_READ=$(fmt_tok "$cur_cache_read")
354
-
355
- # Burn rate (cost per minute)
356
- SL_BURN_RATE=""
357
- if [ "$cfg_show_burn_rate" = "true" ] && [ "$dur_ms" -gt 60000 ] 2>/dev/null; then
358
- SL_BURN_RATE=$(awk -v cost="$cost_raw" -v ms="$dur_ms" \
359
- 'BEGIN { if (ms > 0 && cost+0 > 0) { rate = cost / (ms / 60000); printf "$%.2f/m", rate } }')
360
- fi
361
-
362
- # Exceeds 200k flag
363
- SL_EXCEEDS_200K=""
364
- [ -n "$(json_bool "exceeds_200k_tokens")" ] && SL_EXCEEDS_200K="true"
365
-
366
- # Version
367
- SL_VERSION=$(json_val "version")
368
-
369
- # ── 7. Dynamic column widths ──
370
- SL_C1=$(( SL_TERM_WIDTH / 2 - 4 ))
371
- [ "$SL_C1" -lt 25 ] && SL_C1=25
372
- [ "$SL_C1" -gt 42 ] && SL_C1=42
373
-
374
- # ── 8. Source layout and render ──
375
- layout_file="${STATUSLINE_DIR}/layouts/${active_layout}.sh"
376
- if [ -f "$layout_file" ]; then
377
- source "$layout_file"
378
- else
379
- source "${STATUSLINE_DIR}/layouts/standard.sh"
380
- fi
381
-
382
- render_layout
1
+ #!/usr/bin/env bash
2
+ # skill-statusline v2 — Core engine
3
+ # Reads Claude Code JSON from stdin, computes all fields, renders via layout
4
+
5
+ STATUSLINE_DIR="${HOME}/.claude/statusline"
6
+ CONFIG_FILE="${HOME}/.claude/statusline-config.json"
7
+
8
+ # ── 0. Read stdin JSON first (before anything else) ──
9
+ # If stdin isn't piped properly, `cat` hangs forever — guard against that
10
+ if [ -t 0 ]; then
11
+ exit 0
12
+ fi
13
+ input=$(timeout 2 cat 2>/dev/null)
14
+ if [ -z "$input" ]; then
15
+ exit 0
16
+ fi
17
+
18
+ # Note: no global watchdog — individual timeouts on stdin (2s), git (2s),
19
+ # and tput (1s) protect against hangs without background process issues
20
+
21
+ # ── 1. Source modules ──
22
+ source "${STATUSLINE_DIR}/json-parser.sh"
23
+ source "${STATUSLINE_DIR}/helpers.sh"
24
+
25
+ # ── 2. Parse ALL JSON in one awk pass ──
26
+ # This sets SL_J_* variables avoids spawning 100+ subshells
27
+ sl_parse_json
28
+
29
+ # ── 3. Load config (using simple grep — much faster than multiple json calls) ──
30
+ active_theme="default"
31
+ active_layout="standard"
32
+ cfg_warn_threshold=85
33
+ cfg_bar_width=40
34
+ cfg_show_burn_rate="false"
35
+ cfg_show_vim="true"
36
+ cfg_show_agent="true"
37
+
38
+ if [ -f "$CONFIG_FILE" ]; then
39
+ _cfg=$(cat "$CONFIG_FILE" 2>/dev/null)
40
+ _t=$(echo "$_cfg" | grep -o '"theme"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/')
41
+ _l=$(echo "$_cfg" | grep -o '"layout"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/')
42
+ [ -n "$_t" ] && active_theme="$_t"
43
+ [ -n "$_l" ] && active_layout="$_l"
44
+ _w=$(echo "$_cfg" | grep -o '"compaction_warning_threshold"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
45
+ _bw=$(echo "$_cfg" | grep -o '"bar_width"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
46
+ _br=$(echo "$_cfg" | grep -o '"show_burn_rate"[[:space:]]*:[[:space:]]*true' | head -1)
47
+ _sv=$(echo "$_cfg" | grep -o '"show_vim_mode"[[:space:]]*:[[:space:]]*false' | head -1)
48
+ _sa=$(echo "$_cfg" | grep -o '"show_agent_name"[[:space:]]*:[[:space:]]*false' | head -1)
49
+ [ -n "$_w" ] && cfg_warn_threshold="$_w"
50
+ [ -n "$_bw" ] && cfg_bar_width="$_bw"
51
+ [ -n "$_br" ] && cfg_show_burn_rate="true"
52
+ [ -n "$_sv" ] && cfg_show_vim="false"
53
+ [ -n "$_sa" ] && cfg_show_agent="false"
54
+ fi
55
+
56
+ # Allow env override (for ccsl preview)
57
+ [ -n "$STATUSLINE_THEME_OVERRIDE" ] && active_theme="$STATUSLINE_THEME_OVERRIDE"
58
+ [ -n "$STATUSLINE_LAYOUT_OVERRIDE" ] && active_layout="$STATUSLINE_LAYOUT_OVERRIDE"
59
+
60
+ # ── 4. Source theme ──
61
+ theme_file="${STATUSLINE_DIR}/themes/${active_theme}.sh"
62
+ if [ -f "$theme_file" ]; then
63
+ source "$theme_file"
64
+ else
65
+ source "${STATUSLINE_DIR}/themes/default.sh"
66
+ fi
67
+
68
+ # ── 5. Terminal width detection (with timeout) ──
69
+ SL_TERM_WIDTH=${COLUMNS:-0}
70
+ if [ "$SL_TERM_WIDTH" -eq 0 ] 2>/dev/null; then
71
+ _tw=$(timeout 1 tput cols 2>/dev/null || echo "")
72
+ [ -n "$_tw" ] && [ "$_tw" -gt 0 ] && SL_TERM_WIDTH="$_tw"
73
+ fi
74
+ [ "$SL_TERM_WIDTH" -eq 0 ] && SL_TERM_WIDTH=80
75
+
76
+ # Auto-downgrade layout for narrow terminals
77
+ if [ "$SL_TERM_WIDTH" -lt 60 ]; then
78
+ active_layout="compact"
79
+ elif [ "$SL_TERM_WIDTH" -lt 80 ] && [ "$active_layout" = "full" ]; then
80
+ active_layout="standard"
81
+ fi
82
+
83
+ # Dynamic bar width
84
+ BAR_WIDTH="$cfg_bar_width"
85
+ if [ "$SL_TERM_WIDTH" -gt 100 ]; then
86
+ _dyn=$(( SL_TERM_WIDTH - 20 ))
87
+ [ "$_dyn" -gt 60 ] && _dyn=60
88
+ [ "$_dyn" -gt "$BAR_WIDTH" ] && BAR_WIDTH="$_dyn"
89
+ elif [ "$SL_TERM_WIDTH" -lt 70 ]; then
90
+ BAR_WIDTH=20
91
+ fi
92
+
93
+ # ── 6. Initialize cache ──
94
+ _sl_cache_init
95
+
96
+ # ── 7. Map parsed JSON vars to display vars ──
97
+
98
+ # --- Directory ---
99
+ SL_CWD="${SL_J_workspace_current_dir:-$SL_J_cwd}"
100
+ if [ -z "$SL_CWD" ]; then
101
+ SL_DIR="~"
102
+ clean_cwd=""
103
+ else
104
+ clean_cwd=$(to_fwd "$SL_CWD")
105
+ SL_DIR=$(echo "$clean_cwd" | awk -F'/' '{if(NF>3) print $(NF-2)"/"$(NF-1)"/"$NF; else if(NF>2) print $(NF-1)"/"$NF; else print $0}')
106
+ [ -z "$SL_DIR" ] && SL_DIR="~"
107
+ fi
108
+
109
+ # --- Model ---
110
+ SL_MODEL_DISPLAY="${SL_J_model_display_name:-unknown}"
111
+ SL_MODEL_ID="${SL_J_model_id}"
112
+ model_ver=""
113
+ if [ -n "$SL_MODEL_ID" ]; then
114
+ model_ver=$(echo "$SL_MODEL_ID" | sed -n 's/.*-\([0-9]*\)-\([0-9]*\)$/\1.\2/p')
115
+ fi
116
+ if [ -n "$model_ver" ] && ! echo "$SL_MODEL_DISPLAY" | grep -q '[0-9]'; then
117
+ SL_MODEL="${SL_MODEL_DISPLAY} ${model_ver}"
118
+ else
119
+ SL_MODEL="$SL_MODEL_DISPLAY"
120
+ fi
121
+
122
+ # --- Context ACCURATE computation from current_usage ---
123
+ ctx_size="${SL_J_context_window_context_window_size:-200000}"
124
+ cur_input="${SL_J_context_window_current_usage_input_tokens:-0}"
125
+ cur_output="${SL_J_context_window_current_usage_output_tokens:-0}"
126
+ cur_cache_create="${SL_J_context_window_current_usage_cache_creation_input_tokens:-0}"
127
+ cur_cache_read="${SL_J_context_window_current_usage_cache_read_input_tokens:-0}"
128
+
129
+ # Claude's formula: input + cache_creation + cache_read (output excluded from context %)
130
+ ctx_used=$(( cur_input + cur_cache_create + cur_cache_read ))
131
+
132
+ # Self-calculated percentage
133
+ calc_pct=0
134
+ if [ "$cur_input" -gt 0 ] 2>/dev/null && [ "$ctx_size" -gt 0 ] 2>/dev/null; then
135
+ calc_pct=$(( ctx_used * 100 / ctx_size ))
136
+ fi
137
+
138
+ # Reported percentage as fallback
139
+ reported_pct="${SL_J_context_window_used_percentage}"
140
+
141
+ # Use self-calculated if we have current_usage data, else fallback
142
+ if [ "$cur_input" -gt 0 ] 2>/dev/null; then
143
+ SL_CTX_PCT="$calc_pct"
144
+ elif [ -n "$reported_pct" ]; then
145
+ SL_CTX_PCT=$(echo "$reported_pct" | cut -d. -f1)
146
+ else
147
+ SL_CTX_PCT=0
148
+ fi
149
+
150
+ SL_CTX_REMAINING=$(( 100 - SL_CTX_PCT ))
151
+ [ "$SL_CTX_REMAINING" -lt 0 ] && SL_CTX_REMAINING=0
152
+
153
+ # Context color
154
+ if [ "$SL_CTX_PCT" -gt 90 ] 2>/dev/null; then
155
+ CTX_CLR="$CLR_CTX_CRIT"
156
+ elif [ "$SL_CTX_PCT" -gt 75 ] 2>/dev/null; then
157
+ CTX_CLR="$CLR_CTX_HIGH"
158
+ elif [ "$SL_CTX_PCT" -gt 40 ] 2>/dev/null; then
159
+ CTX_CLR="$CLR_CTX_MED"
160
+ else
161
+ CTX_CLR="$CLR_CTX_LOW"
162
+ fi
163
+
164
+ # Build context bar
165
+ filled=$(( SL_CTX_PCT * BAR_WIDTH / 100 ))
166
+ [ "$filled" -gt "$BAR_WIDTH" ] && filled=$BAR_WIDTH
167
+ empty=$(( BAR_WIDTH - filled ))
168
+ bar_filled=""; bar_empty=""
169
+ i=0; while [ $i -lt $filled ]; do bar_filled="${bar_filled}${BAR_FILLED}"; i=$((i+1)); done
170
+ i=0; while [ $i -lt $empty ]; do bar_empty="${bar_empty}${BAR_EMPTY}"; i=$((i+1)); done
171
+ SL_CTX_BAR="${CTX_CLR}${bar_filled}${CLR_RST}${CLR_BAR_EMPTY}${bar_empty}${CLR_RST} ${CTX_CLR}${SL_CTX_PCT}%${CLR_RST}"
172
+
173
+ # Compaction warning
174
+ SL_COMPACT_WARNING=""
175
+ if [ "$SL_CTX_PCT" -ge 95 ] 2>/dev/null; then
176
+ SL_COMPACT_WARNING=" ${CLR_CTX_CRIT}${CLR_BOLD}COMPACTING${CLR_RST}"
177
+ elif [ "$SL_CTX_PCT" -ge "$cfg_warn_threshold" ] 2>/dev/null; then
178
+ SL_COMPACT_WARNING=" ${CLR_CTX_HIGH}${SL_CTX_REMAINING}% left${CLR_RST}"
179
+ fi
180
+
181
+ # --- GitHub (with caching + timeouts) ---
182
+ SL_BRANCH="no-git"
183
+ SL_GIT_DIRTY=""
184
+ SL_GITHUB=""
185
+ gh_user=""
186
+ gh_repo=""
187
+
188
+ if [ -n "$clean_cwd" ]; then
189
+ SL_BRANCH=$(cache_get "git-branch" "timeout 2 git --no-optional-locks -C '$clean_cwd' symbolic-ref --short HEAD 2>/dev/null || timeout 2 git --no-optional-locks -C '$clean_cwd' rev-parse --short HEAD 2>/dev/null" 5)
190
+ [ -z "$SL_BRANCH" ] && SL_BRANCH="no-git"
191
+
192
+ if [ "$SL_BRANCH" != "no-git" ]; then
193
+ remote_url=$(cache_get "git-remote" "timeout 2 git --no-optional-locks -C '$clean_cwd' remote get-url origin" 10)
194
+ if [ -n "$remote_url" ]; then
195
+ gh_user=$(echo "$remote_url" | sed 's|.*github\.com[:/]\([^/]*\)/.*|\1|')
196
+ [ "$gh_user" = "$remote_url" ] && gh_user=""
197
+ gh_repo=$(echo "$remote_url" | sed 's|.*/\([^/]*\)\.git$|\1|; s|.*/\([^/]*\)$|\1|')
198
+ [ "$gh_repo" = "$remote_url" ] && gh_repo=""
199
+ fi
200
+
201
+ # Dirty check (shorter cache — changes more often)
202
+ _staged=$(cache_get "git-staged" "timeout 2 git --no-optional-locks -C '$clean_cwd' diff --cached --quiet 2>/dev/null && echo clean || echo dirty" 3)
203
+ _unstaged=$(cache_get "git-unstaged" "timeout 2 git --no-optional-locks -C '$clean_cwd' diff --quiet 2>/dev/null && echo clean || echo dirty" 3)
204
+ [ "$_staged" = "dirty" ] && SL_GIT_DIRTY="${CLR_GIT_STAGED}+${CLR_RST}"
205
+ [ "$_unstaged" = "dirty" ] && SL_GIT_DIRTY="${SL_GIT_DIRTY}${CLR_GIT_UNSTAGED}~${CLR_RST}"
206
+ fi
207
+ fi
208
+
209
+ if [ -n "$gh_repo" ]; then
210
+ SL_GITHUB="${gh_user}/${gh_repo}/${SL_BRANCH}"
211
+ else
212
+ SL_GITHUB="$SL_BRANCH"
213
+ fi
214
+
215
+ # --- Cost ---
216
+ cost_raw="${SL_J_cost_total_cost_usd:-0}"
217
+ if [ -z "$cost_raw" ] || [ "$cost_raw" = "0" ]; then
218
+ SL_COST='$0.00'
219
+ else
220
+ SL_COST=$(awk -v c="$cost_raw" 'BEGIN { if (c < 0.01) printf "$%.4f", c; else printf "$%.2f", c }')
221
+ fi
222
+
223
+ # --- Tokens (window vs cumulative) ---
224
+ SL_TOKENS_WIN_IN=$(fmt_tok "$cur_input")
225
+ SL_TOKENS_WIN_OUT=$(fmt_tok "$cur_output")
226
+
227
+ cum_input="${SL_J_context_window_total_input_tokens:-0}"
228
+ cum_output="${SL_J_context_window_total_output_tokens:-0}"
229
+ SL_TOKENS_CUM_IN=$(fmt_tok "$cum_input")
230
+ SL_TOKENS_CUM_OUT=$(fmt_tok "$cum_output")
231
+
232
+ # --- Skill detection (with caching) ---
233
+ SL_SKILL="Idle"
234
+
235
+ _detect_skill() {
236
+ local cwd="$1"
237
+ local tpath="" search_path="$cwd" proj_hash proj_dir
238
+
239
+ while [ -n "$search_path" ] && [ "$search_path" != "/" ]; do
240
+ proj_hash=$(echo "$search_path" | sed 's|^/\([a-zA-Z]\)/|\U\1--|; s|^[A-Z]:/|&|; s|:/|--|; s|/|-|g')
241
+ proj_dir="$HOME/.claude/projects/${proj_hash}"
242
+ if [ -d "$proj_dir" ]; then
243
+ tpath=$(ls -t "$proj_dir"/*.jsonl 2>/dev/null | head -1)
244
+ [ -n "$tpath" ] && break
245
+ fi
246
+ search_path=$(echo "$search_path" | sed 's|/[^/]*$||')
247
+ done
248
+
249
+ if [ -n "$tpath" ] && [ -f "$tpath" ]; then
250
+ local last_tool
251
+ last_tool=$(tail -50 "$tpath" 2>/dev/null | grep -o '"type":"tool_use","id":"[^"]*","name":"[^"]*"' | tail -1 | sed 's/.*"name":"\([^"]*\)".*/\1/')
252
+
253
+ if [ -n "$last_tool" ]; then
254
+ case "$last_tool" in
255
+ Task) echo "Agent" ;;
256
+ Read) echo "Read" ;;
257
+ Write) echo "Write" ;;
258
+ Edit) echo "Edit" ;;
259
+ MultiEdit) echo "Multi Edit" ;;
260
+ Glob) echo "Search(Files)" ;;
261
+ Grep) echo "Search(Content)" ;;
262
+ Bash) echo "Terminal" ;;
263
+ WebSearch) echo "Web Search" ;;
264
+ WebFetch) echo "Web Fetch" ;;
265
+ Skill) echo "Skill" ;;
266
+ AskUserQuestion) echo "Asking..." ;;
267
+ EnterPlanMode) echo "Planning" ;;
268
+ ExitPlanMode) echo "Plan Ready" ;;
269
+ TaskCreate) echo "Task Create" ;;
270
+ TaskUpdate) echo "Task Update" ;;
271
+ NotebookEdit) echo "Notebook" ;;
272
+ *) echo "$last_tool" ;;
273
+ esac
274
+ return
275
+ fi
276
+ fi
277
+
278
+ echo "Idle"
279
+ }
280
+
281
+ if [ -n "$clean_cwd" ]; then
282
+ SL_SKILL=$(cache_get "skill-label" "_detect_skill '$clean_cwd'" 5)
283
+ fi
284
+
285
+ # --- Extra fields ---
286
+ dur_ms="${SL_J_cost_total_duration_ms:-0}"
287
+ SL_DURATION=$(fmt_duration "$dur_ms")
288
+
289
+ SL_LINES_ADDED="${SL_J_cost_total_lines_added:-0}"
290
+ SL_LINES_REMOVED="${SL_J_cost_total_lines_removed:-0}"
291
+
292
+ api_ms="${SL_J_cost_total_api_duration_ms:-0}"
293
+ SL_API_DURATION=$(fmt_duration "$api_ms")
294
+
295
+ SL_VIM_MODE=""
296
+ [ "$cfg_show_vim" = "true" ] && SL_VIM_MODE="${SL_J_vim_mode}"
297
+
298
+ SL_AGENT_NAME=""
299
+ [ "$cfg_show_agent" = "true" ] && SL_AGENT_NAME="${SL_J_agent_name}"
300
+
301
+ SL_CACHE_CREATE=$(fmt_tok "$cur_cache_create")
302
+ SL_CACHE_READ=$(fmt_tok "$cur_cache_read")
303
+
304
+ SL_BURN_RATE=""
305
+ if [ "$cfg_show_burn_rate" = "true" ] && [ "$dur_ms" -gt 60000 ] 2>/dev/null; then
306
+ SL_BURN_RATE=$(awk -v cost="$cost_raw" -v ms="$dur_ms" \
307
+ 'BEGIN { if (ms > 0 && cost+0 > 0) { rate = cost / (ms / 60000); printf "$%.2f/m", rate } }')
308
+ fi
309
+
310
+ SL_EXCEEDS_200K="${SL_J_exceeds_200k_tokens}"
311
+ SL_VERSION="${SL_J_version}"
312
+
313
+ # ── 8. Dynamic column widths ──
314
+ SL_C1=$(( SL_TERM_WIDTH / 2 - 4 ))
315
+ [ "$SL_C1" -lt 25 ] && SL_C1=25
316
+ [ "$SL_C1" -gt 42 ] && SL_C1=42
317
+
318
+ # ── 9. Source layout and render ──
319
+ layout_file="${STATUSLINE_DIR}/layouts/${active_layout}.sh"
320
+ if [ -f "$layout_file" ]; then
321
+ source "$layout_file"
322
+ else
323
+ source "${STATUSLINE_DIR}/layouts/standard.sh"
324
+ fi
325
+
326
+ render_layout
327
+