@ramarivera/coding-buddy 0.8.0 → 0.9.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/README.md +23 -4
- package/adapters/omp/context.ts +1 -1
- package/adapters/omp/events.ts +286 -81
- package/adapters/omp/index.ts +3 -1
- package/adapters/omp/renderers.ts +4 -54
- package/adapters/omp/storage.ts +6 -1
- package/adapters/omp/ui.ts +17 -6
- package/adapters/pi/events.ts +280 -83
- package/adapters/pi/index.ts +11 -2
- package/adapters/pi/renderers.ts +3 -47
- package/adapters/pi/storage.ts +6 -1
- package/adapters/pi/ui.ts +17 -6
- package/adapters/shared/file-storage.ts +22 -0
- package/adapters/shared/widget-layout.ts +248 -2
- package/cli/settings.ts +1 -1
- package/cli/tui.tsx +1 -1
- package/core/command-service.ts +13 -0
- package/core/engine.ts +11 -2
- package/core/identity.ts +15 -0
- package/core/model.ts +3 -0
- package/core/ports.ts +4 -0
- package/core/reactions.ts +29 -2
- package/core/render-model.ts +4 -3
- package/package.json +6 -3
- package/scripts/ci/Dockerfile.vhs +125 -0
- package/scripts/ci/check-statusline.sh +441 -14
- package/scripts/ci/check-vhs-goldens.sh +97 -0
- package/scripts/ci/fonts.conf +37 -0
- package/scripts/ci/goldens/baseline-40.png +0 -0
- package/scripts/ci/goldens/baseline-80.png +0 -0
- package/scripts/ci/goldens/cjk-80.png +0 -0
- package/scripts/ci/goldens/emoji-80.png +0 -0
- package/scripts/ci/render-statusline-pty.py +107 -0
- package/scripts/ci/render-vhs-goldens.sh +226 -0
- package/scripts/ci/statusline.tape +28 -2
- package/scripts/ci/vhs-density-fixtures.sh +27 -0
- package/scripts/ci/vhs-run-statusline.sh +28 -0
- package/scripts/ci/write-density-status.ts +86 -0
- package/scripts/gen-emoji-widths.ts +41 -17
- package/scripts/paths.sh +3 -1
- package/server/art.ts +70 -17
- package/server/hooks/buddy-comment.ts +166 -17
- package/server/hooks/file-type-react.ts +1 -1
- package/server/hooks/mood-react.ts +1 -1
- package/server/hooks/name-react.ts +1 -1
- package/server/hooks/react.ts +1 -1
- package/server/hooks/reaction-data.ts +10 -3
- package/server/index.ts +67 -27
- package/server/path.ts +3 -0
- package/server/state.ts +147 -41
- package/skills/buddy/SKILL.md +1 -1
- package/statusline/buddy-status.sh +488 -76
- package/statusline/emoji-text.data +3 -0
- package/statusline/emoji-widths.data +2 -3
- package/statusline/substatus.sh +52 -10
- package/scripts/postinstall.cjs +0 -42
|
@@ -12,6 +12,28 @@
|
|
|
12
12
|
#
|
|
13
13
|
# When running inside buddy-shell (the PTY wrapper), skip status line rendering
|
|
14
14
|
# so the buddy doesn't show up twice (once in status line, once in wrapper panel).
|
|
15
|
+
# Bash treats COLUMNS as a special variable and may refresh it from a
|
|
16
|
+
# controlling PTY after startup. Snapshot the exported value before any
|
|
17
|
+
# sourced helper or external command can trigger that refresh.
|
|
18
|
+
_INHERITED_COLUMNS="${COLUMNS:-}"
|
|
19
|
+
_INHERITED_ROWS="${LINES:-}"
|
|
20
|
+
# Width math and substring slicing must agree on UTF-8 codepoints. Claude Code
|
|
21
|
+
# emits Unicode art even when a caller inherited the POSIX C locale.
|
|
22
|
+
_LOCALE_HINT="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
23
|
+
case "$_LOCALE_HINT" in
|
|
24
|
+
*UTF-8|*utf-8|*utf8|*UTF8) ;;
|
|
25
|
+
*)
|
|
26
|
+
_UTF8_LOCALE=""
|
|
27
|
+
for _candidate in C.UTF-8 en_US.UTF-8; do
|
|
28
|
+
if locale -a 2>/dev/null | grep -Fx "$_candidate" >/dev/null 2>&1; then
|
|
29
|
+
_UTF8_LOCALE="$_candidate"
|
|
30
|
+
break
|
|
31
|
+
fi
|
|
32
|
+
done
|
|
33
|
+
[ -n "$_UTF8_LOCALE" ] && export LC_ALL="$_UTF8_LOCALE"
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
|
|
15
37
|
[ "$BUDDY_SHELL" = "1" ] && exit 0
|
|
16
38
|
|
|
17
39
|
# shellcheck source=../scripts/paths.sh
|
|
@@ -35,31 +57,15 @@ RARITY=$(jq -r '.rarity // "common"' "$STATE" 2>/dev/null)
|
|
|
35
57
|
STARS=$(jq -r '.stars // ""' "$STATE" 2>/dev/null)
|
|
36
58
|
SHINY=$(jq -r '.shiny // false' "$STATE" 2>/dev/null)
|
|
37
59
|
REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
|
|
38
|
-
REACTION=$(jq -r '.reaction // ""' "$REACTION_FILE" 2>/dev/null)
|
|
39
60
|
ACHIEVEMENT=$(jq -r '.achievement // ""' "$STATE" 2>/dev/null)
|
|
40
61
|
LEVEL=$(jq -r '.level // 1' "$STATE" 2>/dev/null)
|
|
41
62
|
MOOD=$(jq -r '.mood // "focused"' "$STATE" 2>/dev/null)
|
|
42
63
|
|
|
43
64
|
BUDDY_STATUSLINE_INPUT=$(cat)
|
|
44
65
|
|
|
45
|
-
# ─── Animation
|
|
66
|
+
# ─── Animation timing ───────────────────────────────────────────────────────
|
|
46
67
|
NOW=${BUDDY_FAKE_NOW:-$(date +%s)}
|
|
47
|
-
|
|
48
|
-
.frameSequence[$now % (.frameSequence | length)] as $idx
|
|
49
|
-
| .frames[$idx] // ""
|
|
50
|
-
' "$STATE" 2>/dev/null)
|
|
51
|
-
|
|
52
|
-
# Fallback when status.json lacks .frames — e.g. server/bash version skew
|
|
53
|
-
# during install or while the MCP server hasn't rewritten the file yet. Keep
|
|
54
|
-
# the buddy visible in a degraded form instead of emitting an empty block.
|
|
55
|
-
if [ -z "$FRAME_BODY" ]; then
|
|
56
|
-
FRAME_BODY=$' \n (°°) \n ( ) \n \n '
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
ART_LINES=()
|
|
60
|
-
while IFS= read -r line; do
|
|
61
|
-
ART_LINES+=("$line")
|
|
62
|
-
done <<< "$FRAME_BODY"
|
|
68
|
+
# The actual frame body is selected later once density/rows are known.
|
|
63
69
|
|
|
64
70
|
# ─── Rarity color (theme-aware) ─────────────────────────────────────────────
|
|
65
71
|
_THEME="dark"
|
|
@@ -113,57 +119,195 @@ if [ -f "$CONFIG_FILE" ]; then
|
|
|
113
119
|
fi
|
|
114
120
|
fi
|
|
115
121
|
|
|
122
|
+
COLOR_ENABLED=1
|
|
123
|
+
[ -n "${NO_COLOR:-}" ] && COLOR_ENABLED=0
|
|
124
|
+
|
|
116
125
|
RAINBOW_LEN=${#RAINBOW[@]}
|
|
117
126
|
RAINBOW_OFFSET=$(( NOW % RAINBOW_LEN ))
|
|
118
127
|
|
|
119
|
-
|
|
128
|
+
_is_positive_int() {
|
|
129
|
+
case "$1" in
|
|
130
|
+
''|*[!0-9]*) return 1 ;;
|
|
131
|
+
esac
|
|
132
|
+
[ "$1" -gt 0 ] 2>/dev/null && return 0
|
|
133
|
+
return 1
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# ─── Terminal size (rows + columns from the same stty size probe) ─────────────
|
|
137
|
+
COLS=0
|
|
138
|
+
ROWS=0
|
|
139
|
+
|
|
140
|
+
# Claude Code renders this command inside a content box, not across the full
|
|
141
|
+
# terminal. Reserve two columns for settings.json padding (1 per side) and
|
|
142
|
+
# twelve for Claude Code's internal left/right content margins. The 14-column
|
|
143
|
+
# reserve is calibrated against the reported ~74-column content box; the
|
|
144
|
+
# regression fixture is rendered at 60, 80, and 120 columns to assert the
|
|
145
|
+
# resulting budget before output.
|
|
146
|
+
CHROME_RESERVE=14
|
|
147
|
+
STATUSLINE_WIDTH_ADJUST=0
|
|
148
|
+
|
|
149
|
+
ROWS=0
|
|
120
150
|
COLS=0
|
|
121
|
-
PID=$$
|
|
122
|
-
for _ in 1 2 3 4 5; do
|
|
123
|
-
PID=$(ps -o ppid= -p "$PID" 2>/dev/null | tr -d ' ')
|
|
124
|
-
[ -z "$PID" ] || [ "$PID" = "1" ] && break
|
|
125
|
-
|
|
126
|
-
# Linux: read PTY device from /proc
|
|
127
|
-
PTY=$(readlink "/proc/${PID}/fd/0" 2>/dev/null)
|
|
128
|
-
if [ -c "$PTY" ] 2>/dev/null; then
|
|
129
|
-
COLS=$(stty size < "$PTY" 2>/dev/null | awk '{print $2}')
|
|
130
|
-
[ "${COLS:-0}" -gt 40 ] 2>/dev/null && break
|
|
131
|
-
fi
|
|
132
151
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
152
|
+
# Rows/cols resolution precedence (real use):
|
|
153
|
+
# 1. BUDDY_STATUSLINE_ROWS / BUDDY_STATUSLINE_COLS (validated positive ints)
|
|
154
|
+
# 2. exported LINES / COLUMNS (validated positive ints)
|
|
155
|
+
# 3. Linux: /proc/$PID/fd/0 PTY via stty size (captures both values)
|
|
156
|
+
# 4. macOS: ps + stty on /dev/$TTY_NAME (captures both values)
|
|
157
|
+
# 5. Windows: PowerShell (Get-Host).UI.RawUI.WindowSize.Height/Width
|
|
158
|
+
# 6. final defaults: ROWS 999 (full), COLS 125
|
|
159
|
+
if _is_positive_int "${BUDDY_STATUSLINE_ROWS:-}"; then
|
|
160
|
+
ROWS=$((10#${BUDDY_STATUSLINE_ROWS}))
|
|
161
|
+
elif _is_positive_int "$_INHERITED_ROWS"; then
|
|
162
|
+
ROWS=$((10#$_INHERITED_ROWS))
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
if _is_positive_int "${BUDDY_STATUSLINE_COLS:-}"; then
|
|
166
|
+
COLS=$((10#${BUDDY_STATUSLINE_COLS}))
|
|
167
|
+
elif _is_positive_int "$_INHERITED_COLUMNS"; then
|
|
168
|
+
COLS=$((10#$_INHERITED_COLUMNS))
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# If either dimension is still unknown, walk the process tree and read the
|
|
172
|
+
# PTY dimensions once. stty size returns "rows cols"; parse both from the same
|
|
173
|
+
# probe so rows and cols are consistent and no extra detection pass is needed.
|
|
174
|
+
if [ "$ROWS" -lt 1 ] 2>/dev/null || [ "$COLS" -lt 1 ] 2>/dev/null; then
|
|
175
|
+
PID=$$
|
|
176
|
+
for _ in 1 2 3 4 5; do
|
|
177
|
+
PID=$(ps -o ppid= -p "$PID" 2>/dev/null | tr -d ' ')
|
|
178
|
+
[ -z "$PID" ] || [ "$PID" = "1" ] && break
|
|
179
|
+
|
|
180
|
+
# Linux: read PTY device from /proc
|
|
181
|
+
PTY=$(readlink "/proc/${PID}/fd/0" 2>/dev/null)
|
|
182
|
+
if [ -c "$PTY" ] 2>/dev/null; then
|
|
183
|
+
_stty=$(stty size < "$PTY" 2>/dev/null)
|
|
184
|
+
if [ -n "$_stty" ]; then
|
|
185
|
+
_rows=${_stty%% *}
|
|
186
|
+
_cols=${_stty##* }
|
|
187
|
+
[ "$ROWS" -lt 1 ] 2>/dev/null && _is_positive_int "$_rows" && ROWS=$((10#$_rows))
|
|
188
|
+
[ "$COLS" -lt 1 ] 2>/dev/null && _is_positive_int "$_cols" && COLS=$((10#$_cols))
|
|
189
|
+
[ "$ROWS" -gt 0 ] 2>/dev/null && [ "$COLS" -gt 0 ] 2>/dev/null && break
|
|
190
|
+
fi
|
|
140
191
|
fi
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
192
|
+
|
|
193
|
+
# macOS: /proc doesn't exist — get TTY name from process table
|
|
194
|
+
TTY_NAME=$(ps -o tty= -p "$PID" 2>/dev/null | tr -d ' ')
|
|
195
|
+
if [ -n "$TTY_NAME" ] && [ "$TTY_NAME" != "??" ] && [ "$TTY_NAME" != "?" ]; then
|
|
196
|
+
TTY_DEV="/dev/$TTY_NAME"
|
|
197
|
+
if [ -c "$TTY_DEV" ] 2>/dev/null; then
|
|
198
|
+
_stty=$(stty size < "$TTY_DEV" 2>/dev/null)
|
|
199
|
+
if [ -n "$_stty" ]; then
|
|
200
|
+
_rows=${_stty%% *}
|
|
201
|
+
_cols=${_stty##* }
|
|
202
|
+
[ "$ROWS" -lt 1 ] 2>/dev/null && _is_positive_int "$_rows" && ROWS=$((10#$_rows))
|
|
203
|
+
[ "$COLS" -lt 1 ] 2>/dev/null && _is_positive_int "$_cols" && COLS=$((10#$_cols))
|
|
204
|
+
[ "$ROWS" -gt 0 ] 2>/dev/null && [ "$COLS" -gt 0 ] 2>/dev/null && break
|
|
205
|
+
fi
|
|
206
|
+
fi
|
|
207
|
+
fi
|
|
208
|
+
done
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
[ "${COLS:-0}" -lt 1 ] 2>/dev/null && COLS=0
|
|
144
212
|
# Windows: /proc and TTY device detection don't exist; use PowerShell as fallback
|
|
145
|
-
if [ "${COLS:-0}" -lt
|
|
213
|
+
if [ "${COLS:-0}" -lt 1 ] 2>/dev/null; then
|
|
146
214
|
_ps_cols=$(powershell.exe -NoProfile -Command "(Get-Host).UI.RawUI.WindowSize.Width" 2>/dev/null | tr -d '\r\n')
|
|
147
|
-
|
|
215
|
+
if _is_positive_int "$_ps_cols"; then
|
|
216
|
+
[ "$_ps_cols" -gt 40 ] 2>/dev/null && COLS=$((10#$_ps_cols))
|
|
217
|
+
fi
|
|
148
218
|
fi
|
|
149
|
-
[ "${
|
|
219
|
+
if [ "${ROWS:-0}" -lt 1 ] 2>/dev/null; then
|
|
220
|
+
_ps_rows=$(powershell.exe -NoProfile -Command "(Get-Host).UI.RawUI.WindowSize.Height" 2>/dev/null | tr -d '\r\n')
|
|
221
|
+
_is_positive_int "$_ps_rows" && ROWS=$((10#$_ps_rows))
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
[ "${COLS:-0}" -lt 1 ] 2>/dev/null && COLS=125
|
|
225
|
+
[ "${ROWS:-0}" -lt 1 ] 2>/dev/null && ROWS=999
|
|
226
|
+
|
|
227
|
+
COLS=$((10#$COLS))
|
|
228
|
+
DETECTED_COLS="$COLS"
|
|
229
|
+
DETECTED_ROWS="$ROWS"
|
|
150
230
|
|
|
151
231
|
# ─── Reaction bubble (with TTL check) ────────────────────────────────────────
|
|
152
232
|
BUBBLE=""
|
|
153
233
|
if [ -n "$ACHIEVEMENT" ] && [ "$ACHIEVEMENT" != "null" ] && [ "$ACHIEVEMENT" != "" ]; then
|
|
154
234
|
BUBBLE=$'\xf0\x9f\x8f\x86'" $ACHIEVEMENT"
|
|
155
235
|
fi
|
|
156
|
-
REACTION_TTL=
|
|
236
|
+
REACTION_TTL=900
|
|
157
237
|
INNER_W=44
|
|
158
238
|
MARGIN=8
|
|
239
|
+
DENSITY="auto"
|
|
159
240
|
if [ -f "$CONFIG_FILE" ]; then
|
|
160
|
-
_ttl=$(jq -r '.reactionTTL //
|
|
241
|
+
_ttl=$(jq -r '.reactionTTL // 900' "$CONFIG_FILE" 2>/dev/null || echo 900)
|
|
161
242
|
case "$_ttl" in ''|*[!0-9]*) ;; *) REACTION_TTL="$_ttl" ;; esac
|
|
162
243
|
_bw=$(jq -r '.bubbleWidth // 44' "$CONFIG_FILE" 2>/dev/null || echo 44)
|
|
163
244
|
case "$_bw" in ''|*[!0-9]*) ;; *) INNER_W="$_bw" ;; esac
|
|
164
245
|
_bm=$(jq -r '.bubbleMargin // 8' "$CONFIG_FILE" 2>/dev/null || echo 8)
|
|
165
246
|
case "$_bm" in ''|*[!0-9]*) ;; *) MARGIN="$_bm" ;; esac
|
|
247
|
+
_wa=$(jq -r '.statuslineWidthAdjust // 0' "$CONFIG_FILE" 2>/dev/null || echo 0)
|
|
248
|
+
if printf '%s' "$_wa" | grep -Eq '^[+-]?[0-9]+$'; then
|
|
249
|
+
case "$_wa" in
|
|
250
|
+
+*) STATUSLINE_WIDTH_ADJUST=$((10#${_wa#+})) ;;
|
|
251
|
+
-*) STATUSLINE_WIDTH_ADJUST=$((-10#${_wa#-})) ;;
|
|
252
|
+
*) STATUSLINE_WIDTH_ADJUST=$((10#$_wa)) ;;
|
|
253
|
+
esac
|
|
254
|
+
fi
|
|
255
|
+
_density=$(jq -r '.statuslineDensity // "auto"' "$CONFIG_FILE" 2>/dev/null || echo "auto")
|
|
256
|
+
case "$_density" in
|
|
257
|
+
auto|full|compact|minimal) DENSITY="$_density" ;;
|
|
258
|
+
*) DENSITY="auto" ;;
|
|
259
|
+
esac
|
|
166
260
|
fi
|
|
261
|
+
# ─── Statusline density tier ─────────────────────────────────────────────────
|
|
262
|
+
# Explicit config/env density overrides pin the tier; otherwise rows drive it:
|
|
263
|
+
# full >= 40, compact 20-39, minimal < 20. Very narrow terminals also force minimal.
|
|
264
|
+
if [ -n "${BUDDY_STATUSLINE_DENSITY:-}" ]; then
|
|
265
|
+
DENSITY="${BUDDY_STATUSLINE_DENSITY}"
|
|
266
|
+
fi
|
|
267
|
+
case "$DENSITY" in
|
|
268
|
+
full|compact|minimal) TIER="$DENSITY" ;;
|
|
269
|
+
*)
|
|
270
|
+
TIER="full"
|
|
271
|
+
if [ "$DETECTED_ROWS" -lt 40 ] 2>/dev/null; then
|
|
272
|
+
TIER="compact"
|
|
273
|
+
fi
|
|
274
|
+
if [ "$DETECTED_ROWS" -lt 20 ] 2>/dev/null; then
|
|
275
|
+
TIER="minimal"
|
|
276
|
+
fi
|
|
277
|
+
if [ "$DETECTED_COLS" -lt 40 ] 2>/dev/null; then
|
|
278
|
+
TIER="minimal"
|
|
279
|
+
fi
|
|
280
|
+
;;
|
|
281
|
+
esac
|
|
282
|
+
|
|
283
|
+
_sweep_expired_reactions() {
|
|
284
|
+
[ "$REACTION_TTL" -gt 0 ] 2>/dev/null || return 0
|
|
285
|
+
local now cutoff_ms cutoff_seconds file ts
|
|
286
|
+
now=$(date +%s)
|
|
287
|
+
cutoff_ms=$(( (now - REACTION_TTL) * 1000 ))
|
|
288
|
+
cutoff_seconds=$(( now - REACTION_TTL ))
|
|
289
|
+
|
|
290
|
+
for file in "$BUDDY_STATE_DIR"/reaction.*.json; do
|
|
291
|
+
[ -f "$file" ] || continue
|
|
292
|
+
ts=$(jq -r '.timestamp // 0' "$file" 2>/dev/null || echo 0)
|
|
293
|
+
case "$ts" in
|
|
294
|
+
''|*[!0-9]*) rm -f "$file" 2>/dev/null ;;
|
|
295
|
+
*) [ "$ts" -le "$cutoff_ms" ] 2>/dev/null && rm -f "$file" 2>/dev/null ;;
|
|
296
|
+
esac
|
|
297
|
+
done
|
|
298
|
+
|
|
299
|
+
for file in "$BUDDY_STATE_DIR"/.last_comment.*; do
|
|
300
|
+
[ -f "$file" ] || continue
|
|
301
|
+
ts=$(cat "$file" 2>/dev/null)
|
|
302
|
+
case "$ts" in
|
|
303
|
+
''|*[!0-9]*) rm -f "$file" 2>/dev/null ;;
|
|
304
|
+
*) [ "$ts" -le "$cutoff_seconds" ] 2>/dev/null && rm -f "$file" 2>/dev/null ;;
|
|
305
|
+
esac
|
|
306
|
+
done
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_sweep_expired_reactions
|
|
310
|
+
REACTION=$(jq -r '.reaction // ""' "$REACTION_FILE" 2>/dev/null)
|
|
167
311
|
if [ -n "$REACTION" ] && [ "$REACTION" != "null" ] && [ "$REACTION" != "" ]; then
|
|
168
312
|
FRESH=0
|
|
169
313
|
if [ "$REACTION_TTL" -eq 0 ]; then
|
|
@@ -172,7 +316,7 @@ if [ -n "$REACTION" ] && [ "$REACTION" != "null" ] && [ "$REACTION" != "" ]; the
|
|
|
172
316
|
TS=$(jq -r '.timestamp // 0' "$REACTION_FILE" 2>/dev/null || echo 0)
|
|
173
317
|
if [ "$TS" != "0" ]; then
|
|
174
318
|
NOW=$(date +%s)
|
|
175
|
-
AGE=$(( NOW - TS / 1000 ))
|
|
319
|
+
AGE=$(( (NOW * 1000 - TS) / 1000 ))
|
|
176
320
|
[ "$AGE" -lt "$REACTION_TTL" ] && FRESH=1
|
|
177
321
|
fi
|
|
178
322
|
fi
|
|
@@ -182,9 +326,33 @@ if [ -n "$REACTION" ] && [ "$REACTION" != "null" ] && [ "$REACTION" != "" ]; the
|
|
|
182
326
|
else
|
|
183
327
|
BUBBLE="\"${REACTION}\""
|
|
184
328
|
fi
|
|
329
|
+
else
|
|
330
|
+
rm -f "$REACTION_FILE" 2>/dev/null
|
|
185
331
|
fi
|
|
186
332
|
fi
|
|
187
333
|
|
|
334
|
+
# ─── Animation: pick current density frame from server-rendered frames ───────
|
|
335
|
+
NOW=${BUDDY_FAKE_NOW:-$(date +%s)}
|
|
336
|
+
FRAME_BODY=$(jq -r --argjson now "$NOW" --arg tier "$TIER" '
|
|
337
|
+
.frameSequence[$now % (.frameSequence | length)] as $idx
|
|
338
|
+
| if $tier == "compact" then ((.compactFrames? // .frames) | .[$idx] // .frames[$idx])
|
|
339
|
+
elif $tier == "minimal" then ((.minimalFrames? // .frames) | .[$idx] // .frames[$idx])
|
|
340
|
+
else .frames[$idx]
|
|
341
|
+
end // ""
|
|
342
|
+
' "$STATE" 2>/dev/null)
|
|
343
|
+
|
|
344
|
+
# Fallback when status.json lacks .frames — e.g. server/bash version skew
|
|
345
|
+
# during install or while the MCP server hasn't rewritten the file yet. Keep
|
|
346
|
+
# the buddy visible in a degraded form instead of emitting an empty block.
|
|
347
|
+
if [ -z "$FRAME_BODY" ]; then
|
|
348
|
+
FRAME_BODY=$' \n (°°) \n ( ) \n \n '
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
ART_LINES=()
|
|
352
|
+
while IFS= read -r line; do
|
|
353
|
+
ART_LINES+=("$line")
|
|
354
|
+
done <<< "$FRAME_BODY"
|
|
355
|
+
|
|
188
356
|
# ─── Build all art lines ──────────────────────────────────────────────────────
|
|
189
357
|
# ART_LINES comes from the pre-rendered frame (already includes hat + blink).
|
|
190
358
|
# Center the name under the art. Frames are 12 cols wide (see server/art.ts),
|
|
@@ -209,6 +377,15 @@ NAME_PAD=$(( ART_CENTER - NAME_LEN / 2 ))
|
|
|
209
377
|
NAME_LINE="$(printf '%*s%s' "$NAME_PAD" '' "$NAME_WITH_LEVEL")"
|
|
210
378
|
|
|
211
379
|
DIM=$'\033[2;3m'
|
|
380
|
+
if [ "$COLOR_ENABLED" -eq 0 ]; then
|
|
381
|
+
C=""
|
|
382
|
+
NC=""
|
|
383
|
+
NEUTRAL=""
|
|
384
|
+
DIM=""
|
|
385
|
+
for _rainbow_index in "${!RAINBOW[@]}"; do
|
|
386
|
+
RAINBOW[$_rainbow_index]=""
|
|
387
|
+
done
|
|
388
|
+
fi
|
|
212
389
|
|
|
213
390
|
ALL_LINES=()
|
|
214
391
|
ALL_COLORS=()
|
|
@@ -218,19 +395,12 @@ for line in "${ART_LINES[@]}"; do
|
|
|
218
395
|
if [ "$SHINY" = "true" ]; then
|
|
219
396
|
ALL_COLORS+=("${RAINBOW[$(( (_arc + RAINBOW_OFFSET) % RAINBOW_LEN ))]}")
|
|
220
397
|
else
|
|
221
|
-
ALL_COLORS+=("$
|
|
398
|
+
ALL_COLORS+=("$C")
|
|
222
399
|
fi
|
|
223
400
|
_arc=$(( _arc + 1 ))
|
|
224
401
|
done
|
|
225
402
|
ALL_LINES+=("$NAME_LINE"); ALL_COLORS+=("$C")
|
|
226
403
|
|
|
227
|
-
case "$SPECIES" in
|
|
228
|
-
axolotl|capybara|rabbit|mushroom|chonk) ART_W=10 ;;
|
|
229
|
-
duck|blob|cat|dragon|robot|wyvern) ART_W=9 ;;
|
|
230
|
-
octopus|owl|turtle|snail|ghost|cactus) ART_W=8 ;;
|
|
231
|
-
goose|penguin) ART_W=7 ;;
|
|
232
|
-
*) ART_W=10 ;;
|
|
233
|
-
esac
|
|
234
404
|
ART_COUNT=${#ALL_LINES[@]}
|
|
235
405
|
|
|
236
406
|
# ─── Speech bubble (left of art, word-wrapped) ──────────────────────────────
|
|
@@ -243,25 +413,33 @@ fi
|
|
|
243
413
|
|
|
244
414
|
# ─── Display width (emojis count as 2 cols) ──────────────────────────────────
|
|
245
415
|
# iconv turns the string into a stream of UTF-32LE codepoints, then awk sums
|
|
246
|
-
# widths. Rules mirror server/art.ts:displayWidth
|
|
247
|
-
#
|
|
248
|
-
#
|
|
249
|
-
# The ambiguous codepoint list comes from emoji-widths.data, generated by
|
|
250
|
-
# scripts/gen-emoji-widths.ts from the Unicode Emoji_Presentation property.
|
|
416
|
+
# widths. Rules mirror server/art.ts:displayWidth; the generated data lists
|
|
417
|
+
# every Unicode Emoji_Presentation codepoint, while VS16 upgrades a previous
|
|
418
|
+
# narrow emoji to 2 cols (e.g. ❤ + VS16).
|
|
251
419
|
EMOJI_WIDTHS_DATA="$(dirname "${BASH_SOURCE[0]}")/emoji-widths.data"
|
|
252
420
|
EMOJI_PRES_2600="$(grep -v '^#' "$EMOJI_WIDTHS_DATA" 2>/dev/null | tr -d '\n')"
|
|
421
|
+
EMOJI_TEXT_DATA="$(dirname "${BASH_SOURCE[0]}")/emoji-text.data"
|
|
422
|
+
EMOJI_TEXT="$(grep -v '^#' "$EMOJI_TEXT_DATA" 2>/dev/null | tr -d '\n')"
|
|
253
423
|
|
|
254
424
|
dwidth() {
|
|
255
|
-
printf '%s' "$1" | iconv -f UTF-8 -t UTF-32LE 2>/dev/null | od -An -tu4 | awk -v pres="$EMOJI_PRES_2600" '
|
|
425
|
+
printf '%s' "$1" | iconv -f UTF-8 -t UTF-32LE 2>/dev/null | od -An -tu4 | awk -v pres="$EMOJI_PRES_2600" -v text="$EMOJI_TEXT" '
|
|
426
|
+
function load_ranges(value, target, n, i, count, piece, bounds, start, end, cp) {
|
|
427
|
+
n = split(value, ranges, ",")
|
|
428
|
+
for (i = 1; i <= n; i++) {
|
|
429
|
+
count = split(ranges[i], bounds, "-")
|
|
430
|
+
start = bounds[1] + 0
|
|
431
|
+
end = (count == 2) ? bounds[2] + 0 : start
|
|
432
|
+
for (cp = start; cp <= end; cp++) target[cp] = 1
|
|
433
|
+
}
|
|
434
|
+
}
|
|
256
435
|
BEGIN {
|
|
257
|
-
|
|
258
|
-
|
|
436
|
+
load_ranges(pres, wide)
|
|
437
|
+
load_ranges(text, text_default)
|
|
259
438
|
}
|
|
260
439
|
# Precondition: cp is neither a variation selector (65024-65039) nor ZWJ
|
|
261
440
|
# (8205); the main loop filters those before calling in.
|
|
262
441
|
function char_width(cp) {
|
|
263
|
-
if (cp
|
|
264
|
-
if (cp >= 9728 && cp <= 10175) return (cp in wide) ? 2 : 1
|
|
442
|
+
if (cp in wide) return 2
|
|
265
443
|
if (cp >= 9472 && cp <= 9631) return 1
|
|
266
444
|
if (cp >= 12288 && cp <= 40959) return 2
|
|
267
445
|
if (cp >= 65281 && cp <= 65376) return 2
|
|
@@ -276,15 +454,158 @@ dwidth() {
|
|
|
276
454
|
if ((cp >= 65024 && cp <= 65038) || cp == 8205) { upgradable = 0; continue }
|
|
277
455
|
cw = char_width(cp)
|
|
278
456
|
w += cw
|
|
279
|
-
upgradable =
|
|
457
|
+
upgradable = 0
|
|
458
|
+
if (cw == 1 && (cp in text_default)) upgradable = 1
|
|
280
459
|
} }
|
|
281
460
|
END { print w+0 }'
|
|
282
461
|
}
|
|
462
|
+
# Emit one display-width value per UTF-8 codepoint. ANSI-aware truncation uses
|
|
463
|
+
# this profile to make one Unicode-width pass over the complete output row.
|
|
464
|
+
dwidth_profile() {
|
|
465
|
+
printf '%s' "$1" | iconv -f UTF-8 -t UTF-32LE 2>/dev/null | od -An -tu4 | awk -v pres="$EMOJI_PRES_2600" -v text="$EMOJI_TEXT" '
|
|
466
|
+
function load_ranges(value, target, n, i, count, piece, bounds, start, end, cp) {
|
|
467
|
+
n = split(value, ranges, ",")
|
|
468
|
+
for (i = 1; i <= n; i++) {
|
|
469
|
+
count = split(ranges[i], bounds, "-")
|
|
470
|
+
start = bounds[1] + 0
|
|
471
|
+
end = (count == 2) ? bounds[2] + 0 : start
|
|
472
|
+
for (cp = start; cp <= end; cp++) target[cp] = 1
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
BEGIN {
|
|
476
|
+
load_ranges(pres, wide)
|
|
477
|
+
load_ranges(text, text_default)
|
|
478
|
+
}
|
|
479
|
+
function char_width(cp) {
|
|
480
|
+
if (cp in wide) return 2
|
|
481
|
+
if (cp >= 9472 && cp <= 9631) return 1
|
|
482
|
+
if (cp >= 12288 && cp <= 40959) return 2
|
|
483
|
+
if (cp >= 65281 && cp <= 65376) return 2
|
|
484
|
+
return 1
|
|
485
|
+
}
|
|
486
|
+
{
|
|
487
|
+
for (j = 1; j <= NF; j++) {
|
|
488
|
+
cp = $j + 0
|
|
489
|
+
idx++
|
|
490
|
+
if (cp == 65039) {
|
|
491
|
+
if (upgradable && idx > 1) widths[idx - 1] += 1
|
|
492
|
+
widths[idx] = 0
|
|
493
|
+
upgradable = 0
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
if ((cp >= 65024 && cp <= 65038) || cp == 8205) {
|
|
497
|
+
widths[idx] = 0
|
|
498
|
+
upgradable = 0
|
|
499
|
+
continue
|
|
500
|
+
}
|
|
501
|
+
cw = char_width(cp)
|
|
502
|
+
widths[idx] = cw
|
|
503
|
+
upgradable = 0
|
|
504
|
+
if (cw == 1 && (cp in text_default)) upgradable = 1
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
END {
|
|
508
|
+
for (j = 1; j <= idx; j++) print widths[j] + 0
|
|
509
|
+
}'
|
|
510
|
+
}
|
|
511
|
+
ART_W=0
|
|
512
|
+
for line in "${ART_LINES[@]}"; do
|
|
513
|
+
line_w=$(dwidth "$line")
|
|
514
|
+
[ "$line_w" -gt "$ART_W" ] && ART_W="$line_w"
|
|
515
|
+
done
|
|
516
|
+
|
|
517
|
+
# Keep the label inside the same sprite column as the art. The exact Unicode
|
|
518
|
+
# width rules live in dwidth(), so the shell and TS renderers agree on bounds.
|
|
519
|
+
LABEL_W=$(dwidth "$NAME_WITH_LEVEL")
|
|
520
|
+
if [ "$LABEL_W" -gt "$ART_W" ] 2>/dev/null; then
|
|
521
|
+
ART_W="$LABEL_W"
|
|
522
|
+
NAME_PAD=$(( (ART_W - LABEL_W) / 2 ))
|
|
523
|
+
NAME_LINE="$(printf '%*s%s' "$NAME_PAD" '' "$NAME_WITH_LEVEL")"
|
|
524
|
+
ALL_LINES[$(( ART_COUNT - 1 ))]="$NAME_LINE"
|
|
525
|
+
fi
|
|
526
|
+
NAME_LINE_W=$(dwidth "$NAME_LINE")
|
|
527
|
+
# Centering the name against a short fixture frame can make the label wider
|
|
528
|
+
# than every art row; include that width before sizing the card.
|
|
529
|
+
[ "$NAME_LINE_W" -gt "$ART_W" ] && ART_W="$NAME_LINE_W"
|
|
530
|
+
|
|
531
|
+
STATUSLINE_BUDGET=$(( DETECTED_COLS - CHROME_RESERVE + STATUSLINE_WIDTH_ADJUST ))
|
|
532
|
+
# Preserve the existing compact-card behavior at the smallest supported
|
|
533
|
+
# terminal size; the reserve applies once there is enough room for chrome.
|
|
534
|
+
if [ "$DETECTED_COLS" -ge 40 ] 2>/dev/null && [ "$STATUSLINE_BUDGET" -lt 40 ]; then
|
|
535
|
+
STATUSLINE_BUDGET=40
|
|
536
|
+
fi
|
|
537
|
+
[ "$STATUSLINE_BUDGET" -gt "$DETECTED_COLS" ] && STATUSLINE_BUDGET="$DETECTED_COLS"
|
|
538
|
+
|
|
539
|
+
# The sprite and its identifying name are the irreducible minimum of the card.
|
|
540
|
+
# Never let the chrome reserve push the usable budget below the sprite's own
|
|
541
|
+
# width; if it does, fall back to the raw terminal width. If the terminal
|
|
542
|
+
# itself is narrower than the sprite, use the historical default rather than
|
|
543
|
+
# slicing art or name.
|
|
544
|
+
if [ "$STATUSLINE_BUDGET" -lt "$ART_W" ]; then
|
|
545
|
+
STATUSLINE_BUDGET="$DETECTED_COLS"
|
|
546
|
+
fi
|
|
547
|
+
if [ "$STATUSLINE_BUDGET" -lt "$ART_W" ]; then
|
|
548
|
+
STATUSLINE_BUDGET=125
|
|
549
|
+
fi
|
|
550
|
+
COLS="$STATUSLINE_BUDGET"
|
|
551
|
+
|
|
552
|
+
# ─── Density branch: compact drops the bubble; minimal is a single line. ─────
|
|
553
|
+
if [ "$TIER" = "minimal" ]; then
|
|
554
|
+
_face_plain="${ART_LINES[0]:-}"
|
|
555
|
+
if [ -z "$_face_plain" ]; then
|
|
556
|
+
for _fline in "${ART_LINES[@]}"; do
|
|
557
|
+
[ -n "$_fline" ] && _face_plain="$_fline" && break
|
|
558
|
+
done
|
|
559
|
+
fi
|
|
560
|
+
[ -z "$_face_plain" ] && _face_plain=$' (°°) '
|
|
561
|
+
_min_plain="${_face_plain} ${NAME_WITH_LEVEL}"
|
|
562
|
+
_min_w=$(dwidth "$_min_plain")
|
|
563
|
+
if [ -n "$REACTION" ]; then
|
|
564
|
+
_react_plain=" │ \"${REACTION}\""
|
|
565
|
+
_react_w=$(dwidth "$_react_plain")
|
|
566
|
+
if [ $((_min_w + _react_w)) -le "$STATUSLINE_BUDGET" ]; then
|
|
567
|
+
_min_plain="${_min_plain}${_react_plain}"
|
|
568
|
+
_min_w=$(dwidth "$_min_plain")
|
|
569
|
+
fi
|
|
570
|
+
fi
|
|
571
|
+
if [ "$STATUSLINE_BUDGET" -lt "$_min_w" ]; then
|
|
572
|
+
STATUSLINE_BUDGET="$DETECTED_COLS"
|
|
573
|
+
fi
|
|
574
|
+
# Never widen past the real terminal: a hard-coded fallback here would
|
|
575
|
+
# defeat ansi_truncate below and emit rows wider than the pane, wrapping
|
|
576
|
+
# and clobbering the prompt on genuinely narrow terminals.
|
|
577
|
+
if [ "$STATUSLINE_BUDGET" -lt "$_min_w" ]; then
|
|
578
|
+
STATUSLINE_BUDGET="$DETECTED_COLS"
|
|
579
|
+
fi
|
|
580
|
+
COLS="$STATUSLINE_BUDGET"
|
|
581
|
+
ART_LINES=("$_min_plain")
|
|
582
|
+
ALL_COLORS=("$C")
|
|
583
|
+
ALL_LINES=("$_min_plain")
|
|
584
|
+
ART_COUNT=1
|
|
585
|
+
ART_W="$_min_w"
|
|
586
|
+
BUBBLE=""
|
|
587
|
+
BUBBLE_TEXT=""
|
|
588
|
+
elif [ "$TIER" = "compact" ]; then
|
|
589
|
+
BUBBLE=""
|
|
590
|
+
BUBBLE_TEXT=""
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
# The bubble, tail, and sprite are one unit. At narrow widths, drop the
|
|
594
|
+
# bubble rather than allowing a partial border or tail to escape the panel.
|
|
595
|
+
MIN_BUBBLE_INNER=12
|
|
596
|
+
TAIL_W=3
|
|
597
|
+
MAX_INNER=$(( COLS - ART_W - TAIL_W - 4 - MARGIN ))
|
|
598
|
+
if [ -n "$BUBBLE" ] && [ "$MAX_INNER" -ge "$MIN_BUBBLE_INNER" ] 2>/dev/null; then
|
|
599
|
+
[ "$INNER_W" -gt "$MAX_INNER" ] && INNER_W="$MAX_INNER"
|
|
600
|
+
else
|
|
601
|
+
BUBBLE=""
|
|
602
|
+
BUBBLE_TEXT=""
|
|
603
|
+
fi
|
|
283
604
|
|
|
284
605
|
# ─── Word-wrap bubble text ────────────────────────────────────────────────────
|
|
285
606
|
TEXT_LINES=()
|
|
286
607
|
if [ -n "$BUBBLE_TEXT" ]; then
|
|
287
|
-
WORDS
|
|
608
|
+
read -r -a WORDS <<< "$BUBBLE_TEXT"
|
|
288
609
|
CUR_LINE=""
|
|
289
610
|
CUR_W=0
|
|
290
611
|
for word in "${WORDS[@]}"; do
|
|
@@ -329,13 +650,16 @@ fi
|
|
|
329
650
|
BUBBLE_COUNT=${#BUBBLE_LINES[@]}
|
|
330
651
|
|
|
331
652
|
# ─── Right-align with bubble box to the left ─────────────────────────────────
|
|
332
|
-
GAP=
|
|
653
|
+
GAP=3
|
|
333
654
|
if [ $BUBBLE_COUNT -gt 0 ]; then
|
|
334
655
|
TOTAL_W=$(( BOX_W + GAP + ART_W ))
|
|
335
656
|
else
|
|
336
657
|
TOTAL_W=$ART_W
|
|
337
658
|
fi
|
|
338
|
-
|
|
659
|
+
# COLS already includes the Claude Code chrome reserve. The spacer starts with
|
|
660
|
+
# one Braille Blank cell, so account for that cell but don't subtract MARGIN
|
|
661
|
+
# again; doing so leaves the card visibly short of the pane's right edge.
|
|
662
|
+
PAD=$(( COLS - TOTAL_W - 1 ))
|
|
339
663
|
[ "$PAD" -lt 0 ] && PAD=0
|
|
340
664
|
|
|
341
665
|
# On Windows (Git Bash / MSYS2), Braille Blank (U+2800) renders as double-width,
|
|
@@ -344,7 +668,14 @@ case "$(uname -s)" in
|
|
|
344
668
|
MINGW*|CYGWIN*|MSYS*) SPACER=$(printf '%*s' "$PAD" '') ;;
|
|
345
669
|
*) SPACER=$(printf "${B}%${PAD}s" "") ;;
|
|
346
670
|
esac
|
|
347
|
-
|
|
671
|
+
|
|
672
|
+
# Minimal tier uses plain spaces so the one-line sprite + name fits without
|
|
673
|
+
# sacrificing a Braille Blank column on narrow terminals.
|
|
674
|
+
if [ "$TIER" = "minimal" ]; then
|
|
675
|
+
PAD=$(( COLS - ART_W ))
|
|
676
|
+
[ "$PAD" -lt 0 ] && PAD=0
|
|
677
|
+
SPACER=$(printf '%*s' "$PAD" '')
|
|
678
|
+
fi
|
|
348
679
|
|
|
349
680
|
# Vertically center bubble box on the art
|
|
350
681
|
BUBBLE_START=0
|
|
@@ -362,9 +693,10 @@ if [ $BUBBLE_COUNT -gt 2 ]; then
|
|
|
362
693
|
CONNECTOR_BI=$(( (FIRST_TEXT + LAST_TEXT) / 2 ))
|
|
363
694
|
fi
|
|
364
695
|
|
|
365
|
-
# ─── Output: merged bubble box +
|
|
696
|
+
# ─── Output: merged bubble box + art per line ──────────────────────────────────
|
|
366
697
|
TOTAL_BUBBLE=$(( BUBBLE_START + BUBBLE_COUNT ))
|
|
367
698
|
MAX_LINES=$(( ART_COUNT > TOTAL_BUBBLE ? ART_COUNT : TOTAL_BUBBLE ))
|
|
699
|
+
OUTPUT_LINES=()
|
|
368
700
|
for (( i=0; i<MAX_LINES; i++ )); do
|
|
369
701
|
# Art part: actual art line or blank filler
|
|
370
702
|
if [ $i -lt $ART_COUNT ]; then
|
|
@@ -379,30 +711,110 @@ for (( i=0; i<MAX_LINES; i++ )); do
|
|
|
379
711
|
bline="${BUBBLE_LINES[$bi]}"
|
|
380
712
|
btype="${BUBBLE_TYPES[$bi]}"
|
|
381
713
|
|
|
382
|
-
# Connector: "-- " on the middle text line, spaces otherwise
|
|
714
|
+
# Connector: "-- " on the middle text line, spaces otherwise.
|
|
383
715
|
if [ $bi -eq $CONNECTOR_BI ]; then
|
|
384
|
-
gap="${
|
|
716
|
+
gap="${C}--${NC} "
|
|
385
717
|
else
|
|
386
718
|
gap=" "
|
|
387
719
|
fi
|
|
388
720
|
|
|
389
721
|
if [ "$btype" = "border" ]; then
|
|
390
|
-
|
|
722
|
+
OUTPUT_LINES+=("${SPACER}${C}${bline}${NC}${gap}${art_part}")
|
|
391
723
|
else
|
|
392
724
|
pipe_l="${bline:0:1}"
|
|
393
725
|
pipe_r="${bline: -1}"
|
|
394
726
|
inner="${bline:1:$(( ${#bline} - 2 ))}"
|
|
395
|
-
|
|
727
|
+
OUTPUT_LINES+=("${SPACER}${C}${pipe_l}${NC}${DIM}${inner}${NC}${C}${pipe_r}${NC}${gap}${art_part}")
|
|
396
728
|
fi
|
|
397
729
|
else
|
|
398
730
|
empty=$(printf '%*s' "$BOX_W" '')
|
|
399
|
-
|
|
731
|
+
OUTPUT_LINES+=("${SPACER}${empty} ${art_part}")
|
|
400
732
|
fi
|
|
401
733
|
else
|
|
402
|
-
|
|
734
|
+
OUTPUT_LINES+=("${SPACER}${art_part}")
|
|
403
735
|
fi
|
|
404
736
|
done
|
|
405
737
|
|
|
738
|
+
ansi_truncate() {
|
|
739
|
+
local text="$1"
|
|
740
|
+
local max_width="$2"
|
|
741
|
+
local out=""
|
|
742
|
+
local plain=""
|
|
743
|
+
local i=0
|
|
744
|
+
local text_len=${#text}
|
|
745
|
+
local char seq char_width truncated=0 saw_sgr=0
|
|
746
|
+
local visible_width=0
|
|
747
|
+
local -a widths
|
|
748
|
+
local visible_index=0
|
|
749
|
+
|
|
750
|
+
[ "$max_width" -lt 0 ] && max_width=0
|
|
751
|
+
|
|
752
|
+
# Strip SGR while building the one string sent to dwidth_profile. The
|
|
753
|
+
# profile uses one iconv/od/awk pass for the whole row; never spawn a
|
|
754
|
+
# subprocess for each Unicode character.
|
|
755
|
+
while [ "$i" -lt "$text_len" ]; do
|
|
756
|
+
char="${text:$i:1}"
|
|
757
|
+
if [ "$char" = $'\033' ]; then
|
|
758
|
+
saw_sgr=1
|
|
759
|
+
i=$(( i + 1 ))
|
|
760
|
+
while [ "$i" -lt "$text_len" ]; do
|
|
761
|
+
char="${text:$i:1}"
|
|
762
|
+
i=$(( i + 1 ))
|
|
763
|
+
[ "$char" = "m" ] && break
|
|
764
|
+
done
|
|
765
|
+
continue
|
|
766
|
+
fi
|
|
767
|
+
plain="${plain}${char}"
|
|
768
|
+
i=$(( i + 1 ))
|
|
769
|
+
done
|
|
770
|
+
|
|
771
|
+
widths=()
|
|
772
|
+
if [ -n "$plain" ]; then
|
|
773
|
+
while IFS= read -r char_width; do
|
|
774
|
+
widths+=("$char_width")
|
|
775
|
+
done < <(dwidth_profile "$plain")
|
|
776
|
+
fi
|
|
777
|
+
|
|
778
|
+
i=0
|
|
779
|
+
while [ "$i" -lt "$text_len" ]; do
|
|
780
|
+
char="${text:$i:1}"
|
|
781
|
+
if [ "$char" = $'\033' ]; then
|
|
782
|
+
seq="$char"
|
|
783
|
+
i=$(( i + 1 ))
|
|
784
|
+
while [ "$i" -lt "$text_len" ]; do
|
|
785
|
+
char="${text:$i:1}"
|
|
786
|
+
seq="${seq}${char}"
|
|
787
|
+
i=$(( i + 1 ))
|
|
788
|
+
[ "$char" = "m" ] && break
|
|
789
|
+
done
|
|
790
|
+
out="${out}${seq}"
|
|
791
|
+
continue
|
|
792
|
+
fi
|
|
793
|
+
|
|
794
|
+
char_width="${widths[$visible_index]:-1}"
|
|
795
|
+
if [ $(( visible_width + char_width )) -gt "$max_width" ]; then
|
|
796
|
+
truncated=1
|
|
797
|
+
break
|
|
798
|
+
fi
|
|
799
|
+
out="${out}${char}"
|
|
800
|
+
visible_width=$(( visible_width + char_width ))
|
|
801
|
+
visible_index=$(( visible_index + 1 ))
|
|
802
|
+
i=$(( i + 1 ))
|
|
803
|
+
done
|
|
804
|
+
|
|
805
|
+
[ "$truncated" -eq 1 ] && [ "$saw_sgr" -eq 1 ] && out="${out}${NC}"
|
|
806
|
+
printf '%s' "$out"
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
statusline_output_line() {
|
|
810
|
+
ansi_truncate "$1" "$STATUSLINE_BUDGET"
|
|
811
|
+
printf '\n'
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
for line in "${OUTPUT_LINES[@]}"; do
|
|
815
|
+
statusline_output_line "$line"
|
|
816
|
+
done
|
|
817
|
+
|
|
406
818
|
# Append the last cached sub-status result below the buddy panel and refresh
|
|
407
819
|
# it asynchronously when stale. The statusline itself never waits on it.
|
|
408
820
|
append_substatus
|