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/helpers.sh CHANGED
@@ -1,81 +1,119 @@
1
- #!/usr/bin/env bash
2
- # skill-statusline v2 — Shared helpers
3
-
4
- # Convert any path to forward slashes (safe on all OS)
5
- to_fwd() {
6
- echo "$1" | tr '\\' '/' | sed 's|//\+|/|g'
7
- }
8
-
9
- # Right-pad a colored string to a visible width
10
- rpad() {
11
- local str="$1" w="$2"
12
- local plain
13
- plain=$(printf '%b' "$str" | sed $'s/\033\\[[0-9;]*m//g')
14
- local vlen=${#plain}
15
- local need=$(( w - vlen ))
16
- printf '%b' "$str"
17
- [ "$need" -gt 0 ] && printf "%${need}s" ""
18
- }
19
-
20
- # Format token count with k/M suffixes
21
- fmt_tok() {
22
- awk -v t="$1" 'BEGIN {
23
- if (t >= 1000000) printf "%.1fM", t/1000000
24
- else if (t >= 1000) printf "%.0fk", t/1000
25
- else printf "%d", t
26
- }'
27
- }
28
-
29
- # Format duration from milliseconds to human-readable
30
- fmt_duration() {
31
- awk -v ms="$1" 'BEGIN {
32
- s = int(ms / 1000)
33
- if (s < 60) printf "%ds", s
34
- else if (s < 3600) printf "%dm%ds", int(s/60), s%60
35
- else printf "%dh%dm", int(s/3600), int((s%3600)/60)
36
- }'
37
- }
38
-
39
- # ── Filesystem caching with TTL ──
40
-
41
- CACHE_DIR="/tmp/sl-cache-${USER:-unknown}"
42
- CACHE_TTL="${SL_CACHE_TTL:-5}"
43
-
44
- _sl_cache_init() {
45
- [ -d "$CACHE_DIR" ] || mkdir -p "$CACHE_DIR" 2>/dev/null
46
- }
47
-
48
- # cache_get "key" "command" [ttl_seconds]
49
- # Returns cached result if fresh, otherwise runs command and caches
50
- cache_get() {
51
- local key="$1" cmd="$2" ttl="${3:-$CACHE_TTL}"
52
- local f="${CACHE_DIR}/${key}"
53
-
54
- if [ -f "$f" ]; then
55
- local now mtime age
56
- now=$(date +%s)
57
- # Cross-platform stat: Linux/Git Bash vs macOS
58
- if stat -c %Y /dev/null >/dev/null 2>&1; then
59
- mtime=$(stat -c %Y "$f" 2>/dev/null)
60
- else
61
- mtime=$(stat -f %m "$f" 2>/dev/null)
62
- fi
63
- if [ -n "$mtime" ]; then
64
- age=$(( now - mtime ))
65
- if [ "$age" -lt "$ttl" ]; then
66
- cat "$f"
67
- return 0
68
- fi
69
- fi
70
- fi
71
-
72
- local result
73
- result=$(eval "$cmd" 2>/dev/null)
74
- printf '%s' "$result" > "$f" 2>/dev/null
75
- printf '%s' "$result"
76
- }
77
-
78
- # Clear all cached data
79
- cache_clear() {
80
- [ -d "$CACHE_DIR" ] && rm -f "${CACHE_DIR}"/* 2>/dev/null
81
- }
1
+ #!/usr/bin/env bash
2
+ # skill-statusline v2 — Shared helpers
3
+
4
+ # Convert any path to forward slashes (safe on all OS)
5
+ to_fwd() {
6
+ echo "$1" | tr '\\' '/' | sed 's|//\+|/|g'
7
+ }
8
+
9
+ # Right-pad a colored string to a visible width
10
+ rpad() {
11
+ local str="$1" w="$2"
12
+ local plain
13
+ plain=$(printf '%b' "$str" | sed $'s/\033\\[[0-9;]*m//g')
14
+ local vlen=${#plain}
15
+ local need=$(( w - vlen ))
16
+ printf '%b' "$str"
17
+ [ "$need" -gt 0 ] && printf "%${need}s" ""
18
+ }
19
+
20
+ # Format token count with k/M suffixes
21
+ fmt_tok() {
22
+ awk -v t="$1" 'BEGIN {
23
+ if (t >= 1000000) printf "%.1fM", t/1000000
24
+ else if (t >= 1000) printf "%.0fk", t/1000
25
+ else printf "%d", t
26
+ }'
27
+ }
28
+
29
+ # Format duration from milliseconds to human-readable
30
+ fmt_duration() {
31
+ awk -v ms="$1" 'BEGIN {
32
+ s = int(ms / 1000)
33
+ if (s < 60) printf "%ds", s
34
+ else if (s < 3600) printf "%dm%ds", int(s/60), s%60
35
+ else printf "%dh%dm", int(s/3600), int((s%3600)/60)
36
+ }'
37
+ }
38
+
39
+ # ── Run a command with a timeout (seconds) ──
40
+ # Usage: run_with_timeout <seconds> <command...>
41
+ # Returns empty string if timeout exceeded
42
+ run_with_timeout() {
43
+ local secs="$1"; shift
44
+ if command -v timeout >/dev/null 2>&1; then
45
+ timeout "$secs" "$@" 2>/dev/null
46
+ else
47
+ # Fallback: background + wait (POSIX-ish)
48
+ "$@" 2>/dev/null &
49
+ local pid=$!
50
+ local i=0
51
+ while [ $i -lt $(( secs * 10 )) ]; do
52
+ if ! kill -0 "$pid" 2>/dev/null; then
53
+ wait "$pid" 2>/dev/null
54
+ return $?
55
+ fi
56
+ sleep 0.1
57
+ i=$((i + 1))
58
+ done
59
+ kill "$pid" 2>/dev/null
60
+ wait "$pid" 2>/dev/null
61
+ return 124
62
+ fi
63
+ }
64
+
65
+ # ── Filesystem caching with TTL ──
66
+
67
+ CACHE_DIR="/tmp/sl-cache-${USER:-unknown}"
68
+ CACHE_TTL="${SL_CACHE_TTL:-5}"
69
+
70
+ # Detect stat flavor once (not on every cache check)
71
+ _SL_STAT_GNU=""
72
+ _sl_cache_init() {
73
+ [ -d "$CACHE_DIR" ] || mkdir -p "$CACHE_DIR" 2>/dev/null
74
+ # Detect stat syntax once and cache the result
75
+ if stat -c %Y "$CACHE_DIR" >/dev/null 2>&1; then
76
+ _SL_STAT_GNU="yes"
77
+ else
78
+ _SL_STAT_GNU="no"
79
+ fi
80
+ }
81
+
82
+ # Get file mtime using cached stat detection
83
+ _sl_file_mtime() {
84
+ if [ "$_SL_STAT_GNU" = "yes" ]; then
85
+ stat -c %Y "$1" 2>/dev/null
86
+ else
87
+ stat -f %m "$1" 2>/dev/null
88
+ fi
89
+ }
90
+
91
+ # cache_get "key" "command" [ttl_seconds]
92
+ # Returns cached result if fresh, otherwise runs command and caches
93
+ cache_get() {
94
+ local key="$1" cmd="$2" ttl="${3:-$CACHE_TTL}"
95
+ local f="${CACHE_DIR}/${key}"
96
+
97
+ if [ -f "$f" ]; then
98
+ local now mtime age
99
+ now=$(date +%s)
100
+ mtime=$(_sl_file_mtime "$f")
101
+ if [ -n "$mtime" ]; then
102
+ age=$(( now - mtime ))
103
+ if [ "$age" -lt "$ttl" ]; then
104
+ cat "$f"
105
+ return 0
106
+ fi
107
+ fi
108
+ fi
109
+
110
+ local result
111
+ result=$(eval "$cmd" 2>/dev/null)
112
+ printf '%s' "$result" > "$f" 2>/dev/null
113
+ printf '%s' "$result"
114
+ }
115
+
116
+ # Clear all cached data
117
+ cache_clear() {
118
+ [ -d "$CACHE_DIR" ] && rm -f "${CACHE_DIR}"/* 2>/dev/null
119
+ }
@@ -1,71 +1,150 @@
1
1
  #!/usr/bin/env bash
2
- # skill-statusline v2 — JSON parser (no jq, pure grep/sed)
3
- # Handles both flat and nested Claude Code JSON structures
2
+ # skill-statusline v2 — JSON parser (no jq, single awk pass)
3
+ # Extracts ALL needed fields in one pass for speed on Windows/Git Bash
4
4
 
5
- # ── Flat parsers (v1 compat fallback) ──
5
+ # ── Bulk parser: extract all fields at once ──
6
+ # Sets SL_J_* variables for all known fields
7
+ # This avoids spawning 100+ subshells (grep|sed|head per field)
8
+ sl_parse_json() {
9
+ eval "$(echo "$input" | awk '
10
+ BEGIN { FS="" }
11
+ {
12
+ s = s $0
13
+ }
14
+ END {
15
+ # Helper: extract "key":value from a string
16
+ # For strings: "key":"value"
17
+ # For numbers: "key":number
6
18
 
7
- # Extract a quoted string value: json_val "key" → value
8
- json_val() {
9
- echo "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:.*"\(.*\)"/\1/'
10
- }
19
+ # Top-level strings
20
+ extract_str(s, "cwd")
21
+ extract_str(s, "version")
22
+ extract_str(s, "session_id")
11
23
 
12
- # Extract a numeric value: json_num "key" → number
13
- json_num() {
14
- echo "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*[0-9.]*" | head -1 | sed 's/.*:[[:space:]]*//'
15
- }
24
+ # model object
25
+ extract_nested_str(s, "model", "id")
26
+ extract_nested_str(s, "model", "display_name")
16
27
 
17
- # ── Nested parsers (v2 — handles Claude Code's real JSON structure) ──
28
+ # workspace object
29
+ extract_nested_str(s, "workspace", "current_dir")
30
+ extract_nested_str(s, "workspace", "project_dir")
18
31
 
19
- # Extract numeric from single-nested object: json_nested "context_window" "used_percentage"
20
- json_nested() {
21
- local parent="$1" key="$2"
22
- local block
23
- block=$(echo "$input" | sed -n 's/.*"'"$parent"'"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' | head -1)
24
- if [ -n "$block" ]; then
25
- echo "$block" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*[0-9.]*" | head -1 | sed 's/.*:[[:space:]]*//'
26
- fi
27
- }
32
+ # cost object
33
+ extract_nested_num(s, "cost", "total_cost_usd")
34
+ extract_nested_num(s, "cost", "total_duration_ms")
35
+ extract_nested_num(s, "cost", "total_api_duration_ms")
36
+ extract_nested_num(s, "cost", "total_lines_added")
37
+ extract_nested_num(s, "cost", "total_lines_removed")
28
38
 
29
- # Extract string from single-nested object: json_nested_val "model" "display_name"
30
- json_nested_val() {
31
- local parent="$1" key="$2"
32
- local block
33
- block=$(echo "$input" | sed -n 's/.*"'"$parent"'"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' | head -1)
34
- if [ -n "$block" ]; then
35
- echo "$block" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:.*"\(.*\)"/\1/'
36
- fi
37
- }
39
+ # context_window object
40
+ extract_nested_num(s, "context_window", "context_window_size")
41
+ extract_nested_num(s, "context_window", "used_percentage")
42
+ extract_nested_num(s, "context_window", "remaining_percentage")
43
+ extract_nested_num(s, "context_window", "total_input_tokens")
44
+ extract_nested_num(s, "context_window", "total_output_tokens")
45
+
46
+ # context_window.current_usage (double-nested)
47
+ extract_deep_num(s, "context_window", "current_usage", "input_tokens")
48
+ extract_deep_num(s, "context_window", "current_usage", "output_tokens")
49
+ extract_deep_num(s, "context_window", "current_usage", "cache_creation_input_tokens")
50
+ extract_deep_num(s, "context_window", "current_usage", "cache_read_input_tokens")
51
+
52
+ # vim object
53
+ extract_nested_str(s, "vim", "mode")
54
+
55
+ # agent object
56
+ extract_nested_str(s, "agent", "name")
38
57
 
39
- # Extract numeric from double-nested: json_deep "context_window" "current_usage" "input_tokens"
40
- # Handles: {"context_window":{..."current_usage":{"input_tokens":8500}...}}
41
- json_deep() {
42
- local p1="$1" p2="$2" key="$3"
43
- local outer inner
44
- # Get everything inside the outer object (greedy — captures nested braces)
45
- outer=$(echo "$input" | sed -n 's/.*"'"$p1"'"[[:space:]]*:[[:space:]]*{\(.*\)}/\1/p' | head -1)
46
- if [ -n "$outer" ]; then
47
- # Now extract the inner object
48
- inner=$(echo "$outer" | sed -n 's/.*"'"$p2"'"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' | head -1)
49
- if [ -n "$inner" ]; then
50
- echo "$inner" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*[0-9.]*" | head -1 | sed 's/.*:[[:space:]]*//'
51
- fi
52
- fi
58
+ # boolean
59
+ if (match(s, /"exceeds_200k_tokens"[ \t]*:[ \t]*true/)) {
60
+ print "SL_J_exceeds_200k_tokens=true"
61
+ }
62
+ }
63
+
64
+ function varname(parts, r, i) {
65
+ r = "SL_J"
66
+ for (i = 1; i <= length(parts); i++) {
67
+ r = r "_" parts[i]
68
+ }
69
+ return r
70
+ }
71
+
72
+ function extract_str(json, key, pat, val, pos, rest) {
73
+ pat = "\"" key "\"[ \t]*:[ \t]*\""
74
+ if (match(json, pat)) {
75
+ rest = substr(json, RSTART + RLENGTH)
76
+ if (match(rest, /^[^"]*/)) {
77
+ val = substr(rest, 1, RLENGTH)
78
+ gsub(/'\''/, "'\''\\'\'''\''", val)
79
+ print "SL_J_" key "='\''" val "'\''"
80
+ }
81
+ }
82
+ }
83
+
84
+ function extract_nested_str(json, parent, key, pat, block, rest) {
85
+ pat = "\"" parent "\"[ \t]*:[ \t]*\\{"
86
+ if (match(json, pat)) {
87
+ rest = substr(json, RSTART + RLENGTH)
88
+ # Find matching brace (simple: first })
89
+ if (match(rest, /[^}]*/)) {
90
+ block = substr(rest, 1, RLENGTH)
91
+ pat = "\"" key "\"[ \t]*:[ \t]*\""
92
+ if (match(block, pat)) {
93
+ rest = substr(block, RSTART + RLENGTH)
94
+ if (match(rest, /^[^"]*/)) {
95
+ val = substr(rest, 1, RLENGTH)
96
+ gsub(/'\''/, "'\''\\'\'''\''", val)
97
+ print "SL_J_" parent "_" key "='\''" val "'\''"
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ function extract_nested_num(json, parent, key, pat, block, rest, val) {
105
+ pat = "\"" parent "\"[ \t]*:[ \t]*\\{"
106
+ if (match(json, pat)) {
107
+ rest = substr(json, RSTART + RLENGTH)
108
+ # For nested nums, search the full remainder (handles double-nested too)
109
+ block = rest
110
+ pat = "\"" key "\"[ \t]*:[ \t]*"
111
+ if (match(block, pat)) {
112
+ rest = substr(block, RSTART + RLENGTH)
113
+ if (match(rest, /^[0-9.]+/)) {
114
+ val = substr(rest, 1, RLENGTH)
115
+ print "SL_J_" parent "_" key "=" val
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ function extract_deep_num(json, p1, p2, key, pat, outer, inner, rest, val) {
122
+ pat = "\"" p1 "\"[ \t]*:[ \t]*\\{"
123
+ if (match(json, pat)) {
124
+ outer = substr(json, RSTART + RLENGTH)
125
+ pat = "\"" p2 "\"[ \t]*:[ \t]*\\{"
126
+ if (match(outer, pat)) {
127
+ inner = substr(outer, RSTART + RLENGTH)
128
+ pat = "\"" key "\"[ \t]*:[ \t]*"
129
+ if (match(inner, pat)) {
130
+ rest = substr(inner, RSTART + RLENGTH)
131
+ if (match(rest, /^[0-9.]+/)) {
132
+ val = substr(rest, 1, RLENGTH)
133
+ print "SL_J_" p1 "_" p2 "_" key "=" val
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ ')"
53
140
  }
54
141
 
55
- # Extract string from double-nested: json_deep_val "context_window" "current_usage" "mode"
56
- json_deep_val() {
57
- local p1="$1" p2="$2" key="$3"
58
- local outer inner
59
- outer=$(echo "$input" | sed -n 's/.*"'"$p1"'"[[:space:]]*:[[:space:]]*{\(.*\)}/\1/p' | head -1)
60
- if [ -n "$outer" ]; then
61
- inner=$(echo "$outer" | sed -n 's/.*"'"$p2"'"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p' | head -1)
62
- if [ -n "$inner" ]; then
63
- echo "$inner" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:.*"\(.*\)"/\1/'
64
- fi
65
- fi
142
+ # ── Legacy single-field parsers (for v1 fallback only) ──
143
+
144
+ json_val() {
145
+ echo "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:.*"\(.*\)"/\1/'
66
146
  }
67
147
 
68
- # Extract boolean: json_bool "exceeds_200k_tokens" → "true" or ""
69
- json_bool() {
70
- echo "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*true" | head -1 | sed 's/.*:[[:space:]]*//'
148
+ json_num() {
149
+ echo "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*[0-9.]*" | head -1 | sed 's/.*:[[:space:]]*//'
71
150
  }
package/package.json CHANGED
@@ -1,52 +1,52 @@
1
- {
2
- "name": "skill-statusline",
3
- "version": "2.1.1",
4
- "description": "Rich, themeable statusline for Claude Code — accurate context tracking, 5 themes, 3 layouts, token/cost/GitHub/skill display. Pure bash, zero deps.",
5
- "bin": {
6
- "skill-statusline": "bin/cli.js",
7
- "ccsl": "bin/cli.js"
8
- },
9
- "files": [
10
- "bin/",
11
- "lib/",
12
- "themes/",
13
- "layouts/",
14
- "commands/",
15
- "README.md",
16
- "LICENSE"
17
- ],
18
- "scripts": {
19
- "test": "bash test/run-all.sh"
20
- },
21
- "keywords": [
22
- "claude-code",
23
- "statusline",
24
- "status-bar",
25
- "cli",
26
- "claude",
27
- "anthropic",
28
- "ai-coding",
29
- "developer-tools",
30
- "terminal",
31
- "prompt",
32
- "themes",
33
- "context-window",
34
- "token-tracking"
35
- ],
36
- "author": {
37
- "name": "Anit Chaudhary",
38
- "url": "https://github.com/AnitChaudhry"
39
- },
40
- "license": "MIT",
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/AnitChaudhry/skill-statusline.git"
44
- },
45
- "homepage": "https://skills.thinqmesh.com",
46
- "bugs": {
47
- "url": "https://github.com/AnitChaudhry/skill-statusline/issues"
48
- },
49
- "engines": {
50
- "node": ">=16"
51
- }
52
- }
1
+ {
2
+ "name": "skill-statusline",
3
+ "version": "2.2.1",
4
+ "description": "Rich, themeable statusline for Claude Code — accurate context tracking, 5 themes, 3 layouts, token/cost/GitHub/skill display. Pure bash, zero deps.",
5
+ "bin": {
6
+ "skill-statusline": "bin/cli.js",
7
+ "ccsl": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/",
12
+ "themes/",
13
+ "layouts/",
14
+ "commands/",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "test": "bash test/run-all.sh"
20
+ },
21
+ "keywords": [
22
+ "claude-code",
23
+ "statusline",
24
+ "status-bar",
25
+ "cli",
26
+ "claude",
27
+ "anthropic",
28
+ "ai-coding",
29
+ "developer-tools",
30
+ "terminal",
31
+ "prompt",
32
+ "themes",
33
+ "context-window",
34
+ "token-tracking"
35
+ ],
36
+ "author": {
37
+ "name": "Anit Chaudhary",
38
+ "url": "https://github.com/AnitChaudhry"
39
+ },
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/AnitChaudhry/skill-statusline.git"
44
+ },
45
+ "homepage": "https://skills.thinqmesh.com",
46
+ "bugs": {
47
+ "url": "https://github.com/AnitChaudhry/skill-statusline/issues"
48
+ },
49
+ "engines": {
50
+ "node": ">=16"
51
+ }
52
+ }