@ramarivera/coding-buddy 0.8.0 → 0.9.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/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 +30 -1
- 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 +30 -1
- package/adapters/shared/file-storage.ts +21 -0
- package/adapters/shared/widget-layout.ts +134 -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 +2 -0
- package/core/ports.ts +4 -0
- package/core/reactions.ts +29 -2
- package/core/render-model.ts +4 -3
- package/package.json +6 -2
- package/scripts/ci/Dockerfile.vhs +125 -0
- package/scripts/ci/check-statusline.sh +328 -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 +99 -0
- package/scripts/ci/render-vhs-goldens.sh +235 -0
- package/scripts/ci/vhs-run-statusline.sh +16 -0
- package/scripts/gen-emoji-widths.ts +41 -17
- package/scripts/paths.sh +3 -1
- package/server/art.ts +45 -13
- 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 +63 -17
- package/skills/buddy/SKILL.md +1 -1
- package/statusline/buddy-status.sh +351 -59
- package/statusline/emoji-text.data +3 -0
- package/statusline/emoji-widths.data +2 -3
- package/statusline/substatus.sh +52 -10
|
@@ -12,6 +12,27 @@
|
|
|
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
|
+
# Width math and substring slicing must agree on UTF-8 codepoints. Claude Code
|
|
20
|
+
# emits Unicode art even when a caller inherited the POSIX C locale.
|
|
21
|
+
_LOCALE_HINT="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
22
|
+
case "$_LOCALE_HINT" in
|
|
23
|
+
*UTF-8|*utf-8|*utf8|*UTF8) ;;
|
|
24
|
+
*)
|
|
25
|
+
_UTF8_LOCALE=""
|
|
26
|
+
for _candidate in C.UTF-8 en_US.UTF-8; do
|
|
27
|
+
if locale -a 2>/dev/null | grep -Fx "$_candidate" >/dev/null 2>&1; then
|
|
28
|
+
_UTF8_LOCALE="$_candidate"
|
|
29
|
+
break
|
|
30
|
+
fi
|
|
31
|
+
done
|
|
32
|
+
[ -n "$_UTF8_LOCALE" ] && export LC_ALL="$_UTF8_LOCALE"
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
15
36
|
[ "$BUDDY_SHELL" = "1" ] && exit 0
|
|
16
37
|
|
|
17
38
|
# shellcheck source=../scripts/paths.sh
|
|
@@ -35,7 +56,6 @@ RARITY=$(jq -r '.rarity // "common"' "$STATE" 2>/dev/null)
|
|
|
35
56
|
STARS=$(jq -r '.stars // ""' "$STATE" 2>/dev/null)
|
|
36
57
|
SHINY=$(jq -r '.shiny // false' "$STATE" 2>/dev/null)
|
|
37
58
|
REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
|
|
38
|
-
REACTION=$(jq -r '.reaction // ""' "$REACTION_FILE" 2>/dev/null)
|
|
39
59
|
ACHIEVEMENT=$(jq -r '.achievement // ""' "$STATE" 2>/dev/null)
|
|
40
60
|
LEVEL=$(jq -r '.level // 1' "$STATE" 2>/dev/null)
|
|
41
61
|
MOOD=$(jq -r '.mood // "focused"' "$STATE" 2>/dev/null)
|
|
@@ -113,57 +133,132 @@ if [ -f "$CONFIG_FILE" ]; then
|
|
|
113
133
|
fi
|
|
114
134
|
fi
|
|
115
135
|
|
|
136
|
+
COLOR_ENABLED=1
|
|
137
|
+
[ -n "${NO_COLOR:-}" ] && COLOR_ENABLED=0
|
|
138
|
+
|
|
116
139
|
RAINBOW_LEN=${#RAINBOW[@]}
|
|
117
140
|
RAINBOW_OFFSET=$(( NOW % RAINBOW_LEN ))
|
|
118
141
|
|
|
142
|
+
_is_positive_cols() {
|
|
143
|
+
case "$1" in
|
|
144
|
+
''|*[!0-9]*) return 1 ;;
|
|
145
|
+
esac
|
|
146
|
+
[ "$1" -gt 0 ] 2>/dev/null && return 0
|
|
147
|
+
return 1
|
|
148
|
+
}
|
|
149
|
+
|
|
119
150
|
# ─── Terminal width ──────────────────────────────────────────────────────────
|
|
120
151
|
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
152
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
# Claude Code renders this command inside a content box, not across the full
|
|
154
|
+
# terminal. Reserve two columns for settings.json padding (1 per side) and
|
|
155
|
+
# twelve for Claude Code's internal left/right content margins. The 14-column
|
|
156
|
+
# reserve is calibrated against the reported ~74-column content box; the
|
|
157
|
+
# regression fixture is rendered at 60, 80, and 120 columns to assert the
|
|
158
|
+
# resulting budget before output.
|
|
159
|
+
CHROME_RESERVE=14
|
|
160
|
+
STATUSLINE_WIDTH_ADJUST=0
|
|
161
|
+
|
|
162
|
+
# Width resolution precedence (real use):
|
|
163
|
+
# 1. BUDDY_STATUSLINE_COLS (validated positive integer)
|
|
164
|
+
# 2. exported COLUMNS (validated positive integer)
|
|
165
|
+
# 3. Linux: /proc/$PID/fd/0 PTY via stty size
|
|
166
|
+
# 4. macOS: ps + stty on /dev/$TTY_NAME
|
|
167
|
+
# 5. Windows: PowerShell (Get-Host).UI.RawUI.WindowSize.Width
|
|
168
|
+
# 6. final default 125
|
|
169
|
+
if _is_positive_cols "${BUDDY_STATUSLINE_COLS:-}"; then
|
|
170
|
+
COLS=$((10#${BUDDY_STATUSLINE_COLS}))
|
|
171
|
+
elif _is_positive_cols "$_INHERITED_COLUMNS"; then
|
|
172
|
+
COLS=$((10#$_INHERITED_COLUMNS))
|
|
173
|
+
else
|
|
174
|
+
PID=$$
|
|
175
|
+
for _ in 1 2 3 4 5; do
|
|
176
|
+
PID=$(ps -o ppid= -p "$PID" 2>/dev/null | tr -d ' ')
|
|
177
|
+
[ -z "$PID" ] || [ "$PID" = "1" ] && break
|
|
178
|
+
|
|
179
|
+
# Linux: read PTY device from /proc
|
|
180
|
+
PTY=$(readlink "/proc/${PID}/fd/0" 2>/dev/null)
|
|
181
|
+
if [ -c "$PTY" ] 2>/dev/null; then
|
|
182
|
+
COLS=$(stty size < "$PTY" 2>/dev/null | awk '{print $2}')
|
|
183
|
+
_is_positive_cols "${COLS:-}" && break
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# macOS: /proc doesn't exist — get TTY name from process table
|
|
187
|
+
TTY_NAME=$(ps -o tty= -p "$PID" 2>/dev/null | tr -d ' ')
|
|
188
|
+
if [ -n "$TTY_NAME" ] && [ "$TTY_NAME" != "??" ] && [ "$TTY_NAME" != "?" ]; then
|
|
189
|
+
TTY_DEV="/dev/$TTY_NAME"
|
|
190
|
+
if [ -c "$TTY_DEV" ] 2>/dev/null; then
|
|
191
|
+
COLS=$(stty size < "$TTY_DEV" 2>/dev/null | awk '{print $2}')
|
|
192
|
+
_is_positive_cols "${COLS:-}" && break
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
done
|
|
196
|
+
|
|
197
|
+
[ "${COLS:-0}" -lt 1 ] 2>/dev/null && COLS=0
|
|
198
|
+
# Windows: /proc and TTY device detection don't exist; use PowerShell as fallback
|
|
199
|
+
if [ "${COLS:-0}" -lt 1 ] 2>/dev/null; then
|
|
200
|
+
_ps_cols=$(powershell.exe -NoProfile -Command "(Get-Host).UI.RawUI.WindowSize.Width" 2>/dev/null | tr -d '\r\n')
|
|
201
|
+
if _is_positive_cols "$_ps_cols"; then
|
|
202
|
+
[ "$_ps_cols" -gt 40 ] 2>/dev/null && COLS=$((10#$_ps_cols))
|
|
140
203
|
fi
|
|
141
204
|
fi
|
|
142
|
-
|
|
143
|
-
[ "${COLS:-0}" -lt 40 ] 2>/dev/null && COLS=${COLUMNS:-0}
|
|
144
|
-
# Windows: /proc and TTY device detection don't exist; use PowerShell as fallback
|
|
145
|
-
if [ "${COLS:-0}" -lt 40 ] 2>/dev/null; then
|
|
146
|
-
_ps_cols=$(powershell.exe -NoProfile -Command "(Get-Host).UI.RawUI.WindowSize.Width" 2>/dev/null | tr -d '\r\n')
|
|
147
|
-
case "$_ps_cols" in ''|*[!0-9]*) ;; *) [ "$_ps_cols" -gt 40 ] 2>/dev/null && COLS=$_ps_cols ;; esac
|
|
205
|
+
[ "${COLS:-0}" -lt 1 ] 2>/dev/null && COLS=125
|
|
148
206
|
fi
|
|
149
|
-
|
|
207
|
+
COLS=$((10#$COLS))
|
|
208
|
+
DETECTED_COLS="$COLS"
|
|
150
209
|
|
|
151
210
|
# ─── Reaction bubble (with TTL check) ────────────────────────────────────────
|
|
152
211
|
BUBBLE=""
|
|
153
212
|
if [ -n "$ACHIEVEMENT" ] && [ "$ACHIEVEMENT" != "null" ] && [ "$ACHIEVEMENT" != "" ]; then
|
|
154
213
|
BUBBLE=$'\xf0\x9f\x8f\x86'" $ACHIEVEMENT"
|
|
155
214
|
fi
|
|
156
|
-
REACTION_TTL=
|
|
215
|
+
REACTION_TTL=900
|
|
157
216
|
INNER_W=44
|
|
158
217
|
MARGIN=8
|
|
159
218
|
if [ -f "$CONFIG_FILE" ]; then
|
|
160
|
-
_ttl=$(jq -r '.reactionTTL //
|
|
219
|
+
_ttl=$(jq -r '.reactionTTL // 900' "$CONFIG_FILE" 2>/dev/null || echo 900)
|
|
161
220
|
case "$_ttl" in ''|*[!0-9]*) ;; *) REACTION_TTL="$_ttl" ;; esac
|
|
162
221
|
_bw=$(jq -r '.bubbleWidth // 44' "$CONFIG_FILE" 2>/dev/null || echo 44)
|
|
163
222
|
case "$_bw" in ''|*[!0-9]*) ;; *) INNER_W="$_bw" ;; esac
|
|
164
223
|
_bm=$(jq -r '.bubbleMargin // 8' "$CONFIG_FILE" 2>/dev/null || echo 8)
|
|
165
224
|
case "$_bm" in ''|*[!0-9]*) ;; *) MARGIN="$_bm" ;; esac
|
|
225
|
+
_wa=$(jq -r '.statuslineWidthAdjust // 0' "$CONFIG_FILE" 2>/dev/null || echo 0)
|
|
226
|
+
if printf '%s' "$_wa" | grep -Eq '^[+-]?[0-9]+$'; then
|
|
227
|
+
case "$_wa" in
|
|
228
|
+
+*) STATUSLINE_WIDTH_ADJUST=$((10#${_wa#+})) ;;
|
|
229
|
+
-*) STATUSLINE_WIDTH_ADJUST=$((-10#${_wa#-})) ;;
|
|
230
|
+
*) STATUSLINE_WIDTH_ADJUST=$((10#$_wa)) ;;
|
|
231
|
+
esac
|
|
232
|
+
fi
|
|
166
233
|
fi
|
|
234
|
+
_sweep_expired_reactions() {
|
|
235
|
+
[ "$REACTION_TTL" -gt 0 ] 2>/dev/null || return 0
|
|
236
|
+
local now cutoff_ms cutoff_seconds file ts
|
|
237
|
+
now=$(date +%s)
|
|
238
|
+
cutoff_ms=$(( (now - REACTION_TTL) * 1000 ))
|
|
239
|
+
cutoff_seconds=$(( now - REACTION_TTL ))
|
|
240
|
+
|
|
241
|
+
for file in "$BUDDY_STATE_DIR"/reaction.*.json; do
|
|
242
|
+
[ -f "$file" ] || continue
|
|
243
|
+
ts=$(jq -r '.timestamp // 0' "$file" 2>/dev/null || echo 0)
|
|
244
|
+
case "$ts" in
|
|
245
|
+
''|*[!0-9]*) rm -f "$file" 2>/dev/null ;;
|
|
246
|
+
*) [ "$ts" -le "$cutoff_ms" ] 2>/dev/null && rm -f "$file" 2>/dev/null ;;
|
|
247
|
+
esac
|
|
248
|
+
done
|
|
249
|
+
|
|
250
|
+
for file in "$BUDDY_STATE_DIR"/.last_comment.*; do
|
|
251
|
+
[ -f "$file" ] || continue
|
|
252
|
+
ts=$(cat "$file" 2>/dev/null)
|
|
253
|
+
case "$ts" in
|
|
254
|
+
''|*[!0-9]*) rm -f "$file" 2>/dev/null ;;
|
|
255
|
+
*) [ "$ts" -le "$cutoff_seconds" ] 2>/dev/null && rm -f "$file" 2>/dev/null ;;
|
|
256
|
+
esac
|
|
257
|
+
done
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_sweep_expired_reactions
|
|
261
|
+
REACTION=$(jq -r '.reaction // ""' "$REACTION_FILE" 2>/dev/null)
|
|
167
262
|
if [ -n "$REACTION" ] && [ "$REACTION" != "null" ] && [ "$REACTION" != "" ]; then
|
|
168
263
|
FRESH=0
|
|
169
264
|
if [ "$REACTION_TTL" -eq 0 ]; then
|
|
@@ -172,7 +267,7 @@ if [ -n "$REACTION" ] && [ "$REACTION" != "null" ] && [ "$REACTION" != "" ]; the
|
|
|
172
267
|
TS=$(jq -r '.timestamp // 0' "$REACTION_FILE" 2>/dev/null || echo 0)
|
|
173
268
|
if [ "$TS" != "0" ]; then
|
|
174
269
|
NOW=$(date +%s)
|
|
175
|
-
AGE=$(( NOW - TS / 1000 ))
|
|
270
|
+
AGE=$(( (NOW * 1000 - TS) / 1000 ))
|
|
176
271
|
[ "$AGE" -lt "$REACTION_TTL" ] && FRESH=1
|
|
177
272
|
fi
|
|
178
273
|
fi
|
|
@@ -182,6 +277,8 @@ if [ -n "$REACTION" ] && [ "$REACTION" != "null" ] && [ "$REACTION" != "" ]; the
|
|
|
182
277
|
else
|
|
183
278
|
BUBBLE="\"${REACTION}\""
|
|
184
279
|
fi
|
|
280
|
+
else
|
|
281
|
+
rm -f "$REACTION_FILE" 2>/dev/null
|
|
185
282
|
fi
|
|
186
283
|
fi
|
|
187
284
|
|
|
@@ -209,6 +306,15 @@ NAME_PAD=$(( ART_CENTER - NAME_LEN / 2 ))
|
|
|
209
306
|
NAME_LINE="$(printf '%*s%s' "$NAME_PAD" '' "$NAME_WITH_LEVEL")"
|
|
210
307
|
|
|
211
308
|
DIM=$'\033[2;3m'
|
|
309
|
+
if [ "$COLOR_ENABLED" -eq 0 ]; then
|
|
310
|
+
C=""
|
|
311
|
+
NC=""
|
|
312
|
+
NEUTRAL=""
|
|
313
|
+
DIM=""
|
|
314
|
+
for _rainbow_index in "${!RAINBOW[@]}"; do
|
|
315
|
+
RAINBOW[$_rainbow_index]=""
|
|
316
|
+
done
|
|
317
|
+
fi
|
|
212
318
|
|
|
213
319
|
ALL_LINES=()
|
|
214
320
|
ALL_COLORS=()
|
|
@@ -218,19 +324,12 @@ for line in "${ART_LINES[@]}"; do
|
|
|
218
324
|
if [ "$SHINY" = "true" ]; then
|
|
219
325
|
ALL_COLORS+=("${RAINBOW[$(( (_arc + RAINBOW_OFFSET) % RAINBOW_LEN ))]}")
|
|
220
326
|
else
|
|
221
|
-
ALL_COLORS+=("$
|
|
327
|
+
ALL_COLORS+=("$C")
|
|
222
328
|
fi
|
|
223
329
|
_arc=$(( _arc + 1 ))
|
|
224
330
|
done
|
|
225
331
|
ALL_LINES+=("$NAME_LINE"); ALL_COLORS+=("$C")
|
|
226
332
|
|
|
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
333
|
ART_COUNT=${#ALL_LINES[@]}
|
|
235
334
|
|
|
236
335
|
# ─── Speech bubble (left of art, word-wrapped) ──────────────────────────────
|
|
@@ -243,25 +342,33 @@ fi
|
|
|
243
342
|
|
|
244
343
|
# ─── Display width (emojis count as 2 cols) ──────────────────────────────────
|
|
245
344
|
# 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.
|
|
345
|
+
# widths. Rules mirror server/art.ts:displayWidth; the generated data lists
|
|
346
|
+
# every Unicode Emoji_Presentation codepoint, while VS16 upgrades a previous
|
|
347
|
+
# narrow emoji to 2 cols (e.g. ❤ + VS16).
|
|
251
348
|
EMOJI_WIDTHS_DATA="$(dirname "${BASH_SOURCE[0]}")/emoji-widths.data"
|
|
252
349
|
EMOJI_PRES_2600="$(grep -v '^#' "$EMOJI_WIDTHS_DATA" 2>/dev/null | tr -d '\n')"
|
|
350
|
+
EMOJI_TEXT_DATA="$(dirname "${BASH_SOURCE[0]}")/emoji-text.data"
|
|
351
|
+
EMOJI_TEXT="$(grep -v '^#' "$EMOJI_TEXT_DATA" 2>/dev/null | tr -d '\n')"
|
|
253
352
|
|
|
254
353
|
dwidth() {
|
|
255
|
-
printf '%s' "$1" | iconv -f UTF-8 -t UTF-32LE 2>/dev/null | od -An -tu4 | awk -v pres="$EMOJI_PRES_2600" '
|
|
354
|
+
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" '
|
|
355
|
+
function load_ranges(value, target, n, i, count, piece, bounds, start, end, cp) {
|
|
356
|
+
n = split(value, ranges, ",")
|
|
357
|
+
for (i = 1; i <= n; i++) {
|
|
358
|
+
count = split(ranges[i], bounds, "-")
|
|
359
|
+
start = bounds[1] + 0
|
|
360
|
+
end = (count == 2) ? bounds[2] + 0 : start
|
|
361
|
+
for (cp = start; cp <= end; cp++) target[cp] = 1
|
|
362
|
+
}
|
|
363
|
+
}
|
|
256
364
|
BEGIN {
|
|
257
|
-
|
|
258
|
-
|
|
365
|
+
load_ranges(pres, wide)
|
|
366
|
+
load_ranges(text, text_default)
|
|
259
367
|
}
|
|
260
368
|
# Precondition: cp is neither a variation selector (65024-65039) nor ZWJ
|
|
261
369
|
# (8205); the main loop filters those before calling in.
|
|
262
370
|
function char_width(cp) {
|
|
263
|
-
if (cp
|
|
264
|
-
if (cp >= 9728 && cp <= 10175) return (cp in wide) ? 2 : 1
|
|
371
|
+
if (cp in wide) return 2
|
|
265
372
|
if (cp >= 9472 && cp <= 9631) return 1
|
|
266
373
|
if (cp >= 12288 && cp <= 40959) return 2
|
|
267
374
|
if (cp >= 65281 && cp <= 65376) return 2
|
|
@@ -276,15 +383,117 @@ dwidth() {
|
|
|
276
383
|
if ((cp >= 65024 && cp <= 65038) || cp == 8205) { upgradable = 0; continue }
|
|
277
384
|
cw = char_width(cp)
|
|
278
385
|
w += cw
|
|
279
|
-
upgradable =
|
|
386
|
+
upgradable = 0
|
|
387
|
+
if (cw == 1 && (cp in text_default)) upgradable = 1
|
|
280
388
|
} }
|
|
281
389
|
END { print w+0 }'
|
|
282
390
|
}
|
|
391
|
+
# Emit one display-width value per UTF-8 codepoint. ANSI-aware truncation uses
|
|
392
|
+
# this profile to make one Unicode-width pass over the complete output row.
|
|
393
|
+
dwidth_profile() {
|
|
394
|
+
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" '
|
|
395
|
+
function load_ranges(value, target, n, i, count, piece, bounds, start, end, cp) {
|
|
396
|
+
n = split(value, ranges, ",")
|
|
397
|
+
for (i = 1; i <= n; i++) {
|
|
398
|
+
count = split(ranges[i], bounds, "-")
|
|
399
|
+
start = bounds[1] + 0
|
|
400
|
+
end = (count == 2) ? bounds[2] + 0 : start
|
|
401
|
+
for (cp = start; cp <= end; cp++) target[cp] = 1
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
BEGIN {
|
|
405
|
+
load_ranges(pres, wide)
|
|
406
|
+
load_ranges(text, text_default)
|
|
407
|
+
}
|
|
408
|
+
function char_width(cp) {
|
|
409
|
+
if (cp in wide) return 2
|
|
410
|
+
if (cp >= 9472 && cp <= 9631) return 1
|
|
411
|
+
if (cp >= 12288 && cp <= 40959) return 2
|
|
412
|
+
if (cp >= 65281 && cp <= 65376) return 2
|
|
413
|
+
return 1
|
|
414
|
+
}
|
|
415
|
+
{
|
|
416
|
+
for (j = 1; j <= NF; j++) {
|
|
417
|
+
cp = $j + 0
|
|
418
|
+
idx++
|
|
419
|
+
if (cp == 65039) {
|
|
420
|
+
if (upgradable && idx > 1) widths[idx - 1] += 1
|
|
421
|
+
widths[idx] = 0
|
|
422
|
+
upgradable = 0
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
if ((cp >= 65024 && cp <= 65038) || cp == 8205) {
|
|
426
|
+
widths[idx] = 0
|
|
427
|
+
upgradable = 0
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
430
|
+
cw = char_width(cp)
|
|
431
|
+
widths[idx] = cw
|
|
432
|
+
upgradable = 0
|
|
433
|
+
if (cw == 1 && (cp in text_default)) upgradable = 1
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
END {
|
|
437
|
+
for (j = 1; j <= idx; j++) print widths[j] + 0
|
|
438
|
+
}'
|
|
439
|
+
}
|
|
440
|
+
ART_W=0
|
|
441
|
+
for line in "${ART_LINES[@]}"; do
|
|
442
|
+
line_w=$(dwidth "$line")
|
|
443
|
+
[ "$line_w" -gt "$ART_W" ] && ART_W="$line_w"
|
|
444
|
+
done
|
|
445
|
+
|
|
446
|
+
# Keep the label inside the same sprite column as the art. The exact Unicode
|
|
447
|
+
# width rules live in dwidth(), so the shell and TS renderers agree on bounds.
|
|
448
|
+
LABEL_W=$(dwidth "$NAME_WITH_LEVEL")
|
|
449
|
+
if [ "$LABEL_W" -gt "$ART_W" ] 2>/dev/null; then
|
|
450
|
+
ART_W="$LABEL_W"
|
|
451
|
+
NAME_PAD=$(( (ART_W - LABEL_W) / 2 ))
|
|
452
|
+
NAME_LINE="$(printf '%*s%s' "$NAME_PAD" '' "$NAME_WITH_LEVEL")"
|
|
453
|
+
ALL_LINES[$(( ART_COUNT - 1 ))]="$NAME_LINE"
|
|
454
|
+
fi
|
|
455
|
+
NAME_LINE_W=$(dwidth "$NAME_LINE")
|
|
456
|
+
# Centering the name against a short fixture frame can make the label wider
|
|
457
|
+
# than every art row; include that width before sizing the card.
|
|
458
|
+
[ "$NAME_LINE_W" -gt "$ART_W" ] && ART_W="$NAME_LINE_W"
|
|
459
|
+
|
|
460
|
+
STATUSLINE_BUDGET=$(( DETECTED_COLS - CHROME_RESERVE + STATUSLINE_WIDTH_ADJUST ))
|
|
461
|
+
# Preserve the existing compact-card behavior at the smallest supported
|
|
462
|
+
# terminal size; the reserve applies once there is enough room for chrome.
|
|
463
|
+
if [ "$DETECTED_COLS" -ge 40 ] 2>/dev/null && [ "$STATUSLINE_BUDGET" -lt 40 ]; then
|
|
464
|
+
STATUSLINE_BUDGET=40
|
|
465
|
+
fi
|
|
466
|
+
[ "$STATUSLINE_BUDGET" -gt "$DETECTED_COLS" ] && STATUSLINE_BUDGET="$DETECTED_COLS"
|
|
467
|
+
|
|
468
|
+
# The sprite and its identifying name are the irreducible minimum of the card.
|
|
469
|
+
# Never let the chrome reserve push the usable budget below the sprite's own
|
|
470
|
+
# width; if it does, fall back to the raw terminal width. If the terminal
|
|
471
|
+
# itself is narrower than the sprite, use the historical default rather than
|
|
472
|
+
# slicing art or name.
|
|
473
|
+
if [ "$STATUSLINE_BUDGET" -lt "$ART_W" ]; then
|
|
474
|
+
STATUSLINE_BUDGET="$DETECTED_COLS"
|
|
475
|
+
fi
|
|
476
|
+
if [ "$STATUSLINE_BUDGET" -lt "$ART_W" ]; then
|
|
477
|
+
STATUSLINE_BUDGET=125
|
|
478
|
+
fi
|
|
479
|
+
COLS="$STATUSLINE_BUDGET"
|
|
480
|
+
|
|
481
|
+
# The bubble, tail, and sprite are one unit. At narrow widths, drop the
|
|
482
|
+
# bubble rather than allowing a partial border or tail to escape the panel.
|
|
483
|
+
MIN_BUBBLE_INNER=12
|
|
484
|
+
TAIL_W=3
|
|
485
|
+
MAX_INNER=$(( COLS - ART_W - TAIL_W - 4 - MARGIN ))
|
|
486
|
+
if [ -n "$BUBBLE" ] && [ "$MAX_INNER" -ge "$MIN_BUBBLE_INNER" ] 2>/dev/null; then
|
|
487
|
+
[ "$INNER_W" -gt "$MAX_INNER" ] && INNER_W="$MAX_INNER"
|
|
488
|
+
else
|
|
489
|
+
BUBBLE=""
|
|
490
|
+
BUBBLE_TEXT=""
|
|
491
|
+
fi
|
|
283
492
|
|
|
284
493
|
# ─── Word-wrap bubble text ────────────────────────────────────────────────────
|
|
285
494
|
TEXT_LINES=()
|
|
286
495
|
if [ -n "$BUBBLE_TEXT" ]; then
|
|
287
|
-
WORDS
|
|
496
|
+
read -r -a WORDS <<< "$BUBBLE_TEXT"
|
|
288
497
|
CUR_LINE=""
|
|
289
498
|
CUR_W=0
|
|
290
499
|
for word in "${WORDS[@]}"; do
|
|
@@ -329,13 +538,16 @@ fi
|
|
|
329
538
|
BUBBLE_COUNT=${#BUBBLE_LINES[@]}
|
|
330
539
|
|
|
331
540
|
# ─── Right-align with bubble box to the left ─────────────────────────────────
|
|
332
|
-
GAP=
|
|
541
|
+
GAP=3
|
|
333
542
|
if [ $BUBBLE_COUNT -gt 0 ]; then
|
|
334
543
|
TOTAL_W=$(( BOX_W + GAP + ART_W ))
|
|
335
544
|
else
|
|
336
545
|
TOTAL_W=$ART_W
|
|
337
546
|
fi
|
|
338
|
-
|
|
547
|
+
# COLS already includes the Claude Code chrome reserve. The spacer starts with
|
|
548
|
+
# one Braille Blank cell, so account for that cell but don't subtract MARGIN
|
|
549
|
+
# again; doing so leaves the card visibly short of the pane's right edge.
|
|
550
|
+
PAD=$(( COLS - TOTAL_W - 1 ))
|
|
339
551
|
[ "$PAD" -lt 0 ] && PAD=0
|
|
340
552
|
|
|
341
553
|
# On Windows (Git Bash / MSYS2), Braille Blank (U+2800) renders as double-width,
|
|
@@ -344,7 +556,6 @@ case "$(uname -s)" in
|
|
|
344
556
|
MINGW*|CYGWIN*|MSYS*) SPACER=$(printf '%*s' "$PAD" '') ;;
|
|
345
557
|
*) SPACER=$(printf "${B}%${PAD}s" "") ;;
|
|
346
558
|
esac
|
|
347
|
-
GAP_STR=$(printf '%*s' "$GAP" '')
|
|
348
559
|
|
|
349
560
|
# Vertically center bubble box on the art
|
|
350
561
|
BUBBLE_START=0
|
|
@@ -362,9 +573,10 @@ if [ $BUBBLE_COUNT -gt 2 ]; then
|
|
|
362
573
|
CONNECTOR_BI=$(( (FIRST_TEXT + LAST_TEXT) / 2 ))
|
|
363
574
|
fi
|
|
364
575
|
|
|
365
|
-
# ─── Output: merged bubble box +
|
|
576
|
+
# ─── Output: merged bubble box + art per line ──────────────────────────────────
|
|
366
577
|
TOTAL_BUBBLE=$(( BUBBLE_START + BUBBLE_COUNT ))
|
|
367
578
|
MAX_LINES=$(( ART_COUNT > TOTAL_BUBBLE ? ART_COUNT : TOTAL_BUBBLE ))
|
|
579
|
+
OUTPUT_LINES=()
|
|
368
580
|
for (( i=0; i<MAX_LINES; i++ )); do
|
|
369
581
|
# Art part: actual art line or blank filler
|
|
370
582
|
if [ $i -lt $ART_COUNT ]; then
|
|
@@ -379,30 +591,110 @@ for (( i=0; i<MAX_LINES; i++ )); do
|
|
|
379
591
|
bline="${BUBBLE_LINES[$bi]}"
|
|
380
592
|
btype="${BUBBLE_TYPES[$bi]}"
|
|
381
593
|
|
|
382
|
-
# Connector: "-- " on the middle text line, spaces otherwise
|
|
594
|
+
# Connector: "-- " on the middle text line, spaces otherwise.
|
|
383
595
|
if [ $bi -eq $CONNECTOR_BI ]; then
|
|
384
|
-
gap="${
|
|
596
|
+
gap="${C}--${NC} "
|
|
385
597
|
else
|
|
386
598
|
gap=" "
|
|
387
599
|
fi
|
|
388
600
|
|
|
389
601
|
if [ "$btype" = "border" ]; then
|
|
390
|
-
|
|
602
|
+
OUTPUT_LINES+=("${SPACER}${C}${bline}${NC}${gap}${art_part}")
|
|
391
603
|
else
|
|
392
604
|
pipe_l="${bline:0:1}"
|
|
393
605
|
pipe_r="${bline: -1}"
|
|
394
606
|
inner="${bline:1:$(( ${#bline} - 2 ))}"
|
|
395
|
-
|
|
607
|
+
OUTPUT_LINES+=("${SPACER}${C}${pipe_l}${NC}${DIM}${inner}${NC}${C}${pipe_r}${NC}${gap}${art_part}")
|
|
396
608
|
fi
|
|
397
609
|
else
|
|
398
610
|
empty=$(printf '%*s' "$BOX_W" '')
|
|
399
|
-
|
|
611
|
+
OUTPUT_LINES+=("${SPACER}${empty} ${art_part}")
|
|
400
612
|
fi
|
|
401
613
|
else
|
|
402
|
-
|
|
614
|
+
OUTPUT_LINES+=("${SPACER}${art_part}")
|
|
403
615
|
fi
|
|
404
616
|
done
|
|
405
617
|
|
|
618
|
+
ansi_truncate() {
|
|
619
|
+
local text="$1"
|
|
620
|
+
local max_width="$2"
|
|
621
|
+
local out=""
|
|
622
|
+
local plain=""
|
|
623
|
+
local i=0
|
|
624
|
+
local text_len=${#text}
|
|
625
|
+
local char seq char_width truncated=0 saw_sgr=0
|
|
626
|
+
local visible_width=0
|
|
627
|
+
local -a widths
|
|
628
|
+
local visible_index=0
|
|
629
|
+
|
|
630
|
+
[ "$max_width" -lt 0 ] && max_width=0
|
|
631
|
+
|
|
632
|
+
# Strip SGR while building the one string sent to dwidth_profile. The
|
|
633
|
+
# profile uses one iconv/od/awk pass for the whole row; never spawn a
|
|
634
|
+
# subprocess for each Unicode character.
|
|
635
|
+
while [ "$i" -lt "$text_len" ]; do
|
|
636
|
+
char="${text:$i:1}"
|
|
637
|
+
if [ "$char" = $'\033' ]; then
|
|
638
|
+
saw_sgr=1
|
|
639
|
+
i=$(( i + 1 ))
|
|
640
|
+
while [ "$i" -lt "$text_len" ]; do
|
|
641
|
+
char="${text:$i:1}"
|
|
642
|
+
i=$(( i + 1 ))
|
|
643
|
+
[ "$char" = "m" ] && break
|
|
644
|
+
done
|
|
645
|
+
continue
|
|
646
|
+
fi
|
|
647
|
+
plain="${plain}${char}"
|
|
648
|
+
i=$(( i + 1 ))
|
|
649
|
+
done
|
|
650
|
+
|
|
651
|
+
widths=()
|
|
652
|
+
if [ -n "$plain" ]; then
|
|
653
|
+
while IFS= read -r char_width; do
|
|
654
|
+
widths+=("$char_width")
|
|
655
|
+
done < <(dwidth_profile "$plain")
|
|
656
|
+
fi
|
|
657
|
+
|
|
658
|
+
i=0
|
|
659
|
+
while [ "$i" -lt "$text_len" ]; do
|
|
660
|
+
char="${text:$i:1}"
|
|
661
|
+
if [ "$char" = $'\033' ]; then
|
|
662
|
+
seq="$char"
|
|
663
|
+
i=$(( i + 1 ))
|
|
664
|
+
while [ "$i" -lt "$text_len" ]; do
|
|
665
|
+
char="${text:$i:1}"
|
|
666
|
+
seq="${seq}${char}"
|
|
667
|
+
i=$(( i + 1 ))
|
|
668
|
+
[ "$char" = "m" ] && break
|
|
669
|
+
done
|
|
670
|
+
out="${out}${seq}"
|
|
671
|
+
continue
|
|
672
|
+
fi
|
|
673
|
+
|
|
674
|
+
char_width="${widths[$visible_index]:-1}"
|
|
675
|
+
if [ $(( visible_width + char_width )) -gt "$max_width" ]; then
|
|
676
|
+
truncated=1
|
|
677
|
+
break
|
|
678
|
+
fi
|
|
679
|
+
out="${out}${char}"
|
|
680
|
+
visible_width=$(( visible_width + char_width ))
|
|
681
|
+
visible_index=$(( visible_index + 1 ))
|
|
682
|
+
i=$(( i + 1 ))
|
|
683
|
+
done
|
|
684
|
+
|
|
685
|
+
[ "$truncated" -eq 1 ] && [ "$saw_sgr" -eq 1 ] && out="${out}${NC}"
|
|
686
|
+
printf '%s' "$out"
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
statusline_output_line() {
|
|
690
|
+
ansi_truncate "$1" "$STATUSLINE_BUDGET"
|
|
691
|
+
printf '\n'
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
for line in "${OUTPUT_LINES[@]}"; do
|
|
695
|
+
statusline_output_line "$line"
|
|
696
|
+
done
|
|
697
|
+
|
|
406
698
|
# Append the last cached sub-status result below the buddy panel and refresh
|
|
407
699
|
# it asynchronously when stale. The statusline itself never waits on it.
|
|
408
700
|
append_substatus
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Auto-generated by scripts/gen-emoji-widths.ts — do not edit by hand.
|
|
2
|
+
# Decimal codepoint ranges with Unicode Emoji=Yes and Emoji_Presentation=No.
|
|
3
|
+
35,42,48-57,169,174,8252,8265,8482,8505,8596-8601,8617-8618,9000,9167,9197-9199,9201-9202,9208-9210,9410,9642-9643,9654,9664,9723-9724,9728-9732,9742,9745,9752,9757,9760,9762-9763,9766,9770,9774-9775,9784-9786,9792,9794,9823-9824,9827,9829-9830,9832,9851,9854,9874,9876-9879,9881,9883-9884,9888,9895,9904-9905,9928,9935,9937,9939,9961,9968-9969,9972,9975-9977,9986,9992-9993,9996-9997,9999,10002,10004,10006,10013,10017,10035-10036,10052,10055,10083-10084,10145,10548-10549,11013-11015,12336,12349,12951,12953,127344-127345,127358-127359,127490,127543,127777,127780-127788,127798,127869,127894-127895,127897-127899,127902-127903,127947-127950,127956-127967,127987,127989,127991,128063,128065,128253,128329-128330,128367-128368,128371-128377,128391,128394-128397,128400,128421,128424,128433-128434,128444,128450-128452,128465-128467,128476-128478,128481,128483,128488,128495,128499,128506,128715,128717-128719,128736-128741,128745,128752,128755
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
# Auto-generated by scripts/gen-emoji-widths.ts — do not edit by hand.
|
|
2
|
-
# Decimal
|
|
3
|
-
|
|
4
|
-
9748 9749 9800 9801 9802 9803 9804 9805 9806 9807 9808 9809 9810 9811 9855 9875 9889 9898 9899 9917 9918 9924 9925 9934 9940 9962 9970 9971 9973 9978 9981 9989 9994 9995 10024 10060 10062 10067 10068 10069 10071 10133 10134 10135 10160 10175
|
|
2
|
+
# Decimal codepoint ranges with Unicode Emoji_Presentation=Yes.
|
|
3
|
+
8986-8987,9193-9196,9200,9203,9725-9726,9748-9749,9800-9811,9855,9875,9889,9898-9899,9917-9918,9924-9925,9934,9940,9962,9970-9971,9973,9978,9981,9989,9994-9995,10024,10060,10062,10067-10069,10071,10133-10135,10160,10175,11035-11036,11088,11093,126980,127183,127374,127377-127386,127462-127487,127489,127514,127535,127538-127542,127544-127546,127568-127569,127744-127776,127789-127797,127799-127868,127870-127891,127904-127946,127951-127955,127968-127984,127988,127992-128062,128064,128066-128252,128255-128317,128331-128334,128336-128359,128378,128405-128406,128420,128507-128591,128640-128709,128716,128720-128722,128725-128728,128732-128735,128747-128748,128756-128764,128992-129003,129008,129292-129338,129340-129349,129351-129535,129648-129660,129664-129674,129678-129734,129736,129741-129756,129759-129770,129775-129784
|
package/statusline/substatus.sh
CHANGED
|
@@ -25,12 +25,24 @@ append_substatus() {
|
|
|
25
25
|
local cache_file="$state_dir/.substatus.$sid"
|
|
26
26
|
local lock_dir="$state_dir/.substatus.$sid.lock"
|
|
27
27
|
local command=""
|
|
28
|
-
local now cache_age lock_age refresh_seconds configured_refresh_seconds
|
|
28
|
+
local now cache_age lock_age refresh_seconds configured_refresh_seconds line sweep_lock sweep_lock_age
|
|
29
29
|
|
|
30
|
-
#
|
|
31
|
-
# the
|
|
32
|
-
#
|
|
33
|
-
|
|
30
|
+
# Cleanup is intentionally detached: stale temp files are housekeeping and
|
|
31
|
+
# must not make the statusline wait on a filesystem walk. The lock keeps
|
|
32
|
+
# concurrent statusline ticks from launching duplicate sweeps.
|
|
33
|
+
sweep_lock="$state_dir/.substatus-sweep.$sid.lock"
|
|
34
|
+
if [ -d "$sweep_lock" ]; then
|
|
35
|
+
sweep_lock_age=$(( $(date +%s) - $(_substatus_mtime "$sweep_lock") ))
|
|
36
|
+
if [ "$sweep_lock_age" -gt 300 ]; then
|
|
37
|
+
rmdir "$sweep_lock" 2>/dev/null || rm -rf "$sweep_lock" 2>/dev/null
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
if mkdir "$sweep_lock" 2>/dev/null; then
|
|
41
|
+
(
|
|
42
|
+
trap 'rmdir "$sweep_lock" 2>/dev/null' EXIT
|
|
43
|
+
find "$state_dir" -type f -name ".substatus.$sid.*" -mmin +60 -exec rm -f {} + 2>/dev/null
|
|
44
|
+
) >/dev/null 2>&1 &
|
|
45
|
+
fi
|
|
34
46
|
|
|
35
47
|
[ -f "$config_file" ] || return 0
|
|
36
48
|
command=$(jq -r '.subStatusCommand // ""' "$config_file" 2>/dev/null)
|
|
@@ -43,10 +55,21 @@ append_substatus() {
|
|
|
43
55
|
*) [ "$configured_refresh_seconds" -gt 0 ] && refresh_seconds="$configured_refresh_seconds" ;;
|
|
44
56
|
esac
|
|
45
57
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
# The main statusline supplies ANSI-aware output when it has a width
|
|
59
|
+
# budget. Keep this helper standalone for callers that don't.
|
|
60
|
+
_substatus_print_line() {
|
|
61
|
+
local line="$1"
|
|
62
|
+
if [ "$(type -t statusline_output_line 2>/dev/null)" = "function" ]; then
|
|
63
|
+
statusline_output_line "$line"
|
|
64
|
+
else
|
|
65
|
+
printf '%s\n' "$line"
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
if [ -f "$cache_file" ]; then
|
|
69
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
70
|
+
_substatus_print_line "$line"
|
|
71
|
+
done < "$cache_file"
|
|
72
|
+
fi
|
|
50
73
|
now=$(date +%s)
|
|
51
74
|
cache_age=999999
|
|
52
75
|
if [ -f "$cache_file" ]; then
|
|
@@ -59,7 +82,11 @@ append_substatus() {
|
|
|
59
82
|
# over a minute old, so a slow but healthy command is never duplicated.
|
|
60
83
|
if [ -d "$lock_dir" ]; then
|
|
61
84
|
lock_age=$(( now - $(_substatus_mtime "$lock_dir") ))
|
|
62
|
-
|
|
85
|
+
# A refresh killed alongside its parent leaves this lock behind. The
|
|
86
|
+
# window must be short enough that a fresh session recovers within a
|
|
87
|
+
# few ticks instead of rendering no sub-status at all, but longer than
|
|
88
|
+
# a healthy slow command's runtime so it is never duplicated.
|
|
89
|
+
if [ "$lock_age" -gt 20 ]; then
|
|
63
90
|
rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir" 2>/dev/null
|
|
64
91
|
fi
|
|
65
92
|
fi
|
|
@@ -85,3 +112,18 @@ append_substatus() {
|
|
|
85
112
|
fi
|
|
86
113
|
) </dev/null > /dev/null 2>/dev/null &
|
|
87
114
|
}
|
|
115
|
+
|
|
116
|
+
# Claude Code kills the statusline process when the next render starts, and a
|
|
117
|
+
# plain `&` child stays in that process group — so the refresh was being killed
|
|
118
|
+
# mid-write, leaving a held lock and a 0-byte temp file behind. A session that
|
|
119
|
+
# lost the race that way never rendered a sub-status at all.
|
|
120
|
+
#
|
|
121
|
+
# setsid detaches into a new session so the refresh survives; it is Linux-only,
|
|
122
|
+
# so fall back to nohup (which at least survives SIGHUP) on macOS.
|
|
123
|
+
_substatus_detach() {
|
|
124
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
125
|
+
setsid "$@" </dev/null >/dev/null 2>&1 &
|
|
126
|
+
else
|
|
127
|
+
nohup "$@" </dev/null >/dev/null 2>&1 &
|
|
128
|
+
fi
|
|
129
|
+
}
|