@ramarivera/coding-buddy 0.4.0-alpha.7 → 0.4.0-alpha.9

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.
Files changed (43) hide show
  1. package/README.md +18 -39
  2. package/adapters/claude/hooks/buddy-comment.sh +4 -1
  3. package/adapters/claude/hooks/name-react.sh +4 -1
  4. package/adapters/claude/hooks/react.sh +4 -1
  5. package/adapters/claude/install/backup.ts +36 -118
  6. package/adapters/claude/install/disable.ts +9 -14
  7. package/adapters/claude/install/doctor.ts +26 -87
  8. package/adapters/claude/install/install.ts +39 -66
  9. package/adapters/claude/install/test-statusline.ts +8 -18
  10. package/adapters/claude/install/uninstall.ts +18 -26
  11. package/adapters/claude/plugin/marketplace.json +4 -4
  12. package/adapters/claude/plugin/plugin.json +3 -5
  13. package/adapters/claude/server/index.ts +132 -5
  14. package/adapters/claude/server/path.ts +12 -0
  15. package/adapters/claude/skills/buddy/SKILL.md +16 -1
  16. package/adapters/claude/statusline/buddy-status.sh +22 -3
  17. package/adapters/claude/storage/paths.ts +9 -0
  18. package/adapters/claude/storage/settings.ts +53 -3
  19. package/adapters/claude/storage/state.ts +22 -4
  20. package/adapters/pi/README.md +19 -0
  21. package/adapters/pi/events.ts +176 -19
  22. package/adapters/pi/index.ts +3 -1
  23. package/adapters/pi/logger.ts +52 -0
  24. package/adapters/pi/prompt.ts +18 -0
  25. package/adapters/pi/storage.ts +1 -0
  26. package/cli/biomes.ts +309 -0
  27. package/cli/buddy-shell.ts +818 -0
  28. package/cli/index.ts +7 -0
  29. package/cli/tui.tsx +2244 -0
  30. package/cli/upgrade.ts +213 -0
  31. package/core/model.ts +6 -0
  32. package/package.json +78 -62
  33. package/scripts/paths.sh +40 -0
  34. package/server/achievements.ts +15 -0
  35. package/server/art.ts +1 -0
  36. package/server/engine.ts +1 -0
  37. package/server/mcp-launcher.sh +16 -0
  38. package/server/path.ts +30 -0
  39. package/server/reactions.ts +1 -0
  40. package/server/state.ts +3 -0
  41. package/adapters/claude/popup/buddy-popup.sh +0 -92
  42. package/adapters/claude/popup/buddy-render.sh +0 -540
  43. package/adapters/claude/popup/popup-manager.sh +0 -355
@@ -1,540 +0,0 @@
1
- #!/usr/bin/env bash
2
- # claude-buddy popup render loop -- draws buddy in the tmux popup
3
- #
4
- # Runs as BACKGROUND process inside the popup. Stdin is /dev/null.
5
- # Only writes to stdout (popup display).
6
- #
7
- # Env vars:
8
- # BUDDY_DIR -- ~/.claude-buddy
9
-
10
- set -uo pipefail
11
-
12
- # Inner popup dimensions from env (set by popup-manager).
13
- # POPUP_INNER_W/H account for border on tmux < 3.4.
14
- PANE_W="${POPUP_INNER_W:-$(tput cols 2>/dev/null || echo 24)}"
15
- PANE_H="${POPUP_INNER_H:-$(tput lines 2>/dev/null || echo 14)}"
16
-
17
- BUDDY_STATE_DIR="${BUDDY_DIR:-$HOME/.claude-buddy}"
18
- # Session ID from env (set by popup-manager via -e or env file)
19
- _SID="${BUDDY_SID:-${CC_PANE#%}}"
20
- _SID="${_SID:-default}"
21
-
22
- STATE="$BUDDY_STATE_DIR/status.json"
23
- COMPANION="$BUDDY_STATE_DIR/companion.json"
24
- REACTION_FILE="$BUDDY_STATE_DIR/reaction.$_SID.json"
25
- RESIZE_FLAG="$BUDDY_STATE_DIR/popup-resize.$_SID"
26
- CONFIG_FILE="$BUDDY_STATE_DIR/config.json"
27
- REACTION_TTL=0
28
-
29
- # Bubble style: "classic" (pipes/dashes like status line) or "round" (parens/tildes)
30
- BUBBLE_STYLE="classic"
31
- BUBBLE_POSITION="top"
32
- SHOW_RARITY=1
33
- if [ -f "$CONFIG_FILE" ]; then
34
- _bs=$(jq -r '.bubbleStyle // "classic"' "$CONFIG_FILE" 2>/dev/null || echo "classic")
35
- case "$_bs" in classic|round) BUBBLE_STYLE="$_bs" ;; esac
36
- _bp=$(jq -r '.bubblePosition // "top"' "$CONFIG_FILE" 2>/dev/null || echo "top")
37
- case "$_bp" in top|left) BUBBLE_POSITION="$_bp" ;; esac
38
- _sr=$(jq -r 'if .showRarity == false then "false" else "true" end' "$CONFIG_FILE" 2>/dev/null || echo "true")
39
- [ "$_sr" = "false" ] && SHOW_RARITY=0
40
- _ttl=$(jq -r '.reactionTTL // 0' "$CONFIG_FILE" 2>/dev/null || echo 0)
41
- case "$_ttl" in ''|*[!0-9]*) ;; *) REACTION_TTL="$_ttl" ;; esac
42
- fi
43
-
44
- # Track whether we're currently showing a reaction bubble.
45
- # Initialized after reaction_fresh() is defined (see below main loop).
46
- SHOWING_REACTION=0
47
-
48
- SEQ=(0 0 0 0 1 0 0 0 -1 0 0 2 0 0 0)
49
- SEQ_LEN=${#SEQ[@]}
50
- TICK=0
51
-
52
- NC=$'\033[0m'
53
- DIM=$'\033[2;3m'
54
- BOLD=$'\033[1m'
55
-
56
- rarity_color() {
57
- case "$1" in
58
- common) echo -n $'\033[38;2;153;153;153m' ;;
59
- uncommon) echo -n $'\033[38;2;78;186;101m' ;;
60
- rare) echo -n $'\033[38;2;177;185;249m' ;;
61
- epic) echo -n $'\033[38;2;175;135;255m' ;;
62
- legendary) echo -n $'\033[38;2;255;193;7m' ;;
63
- *) echo -n "$NC" ;;
64
- esac
65
- }
66
-
67
- rarity_stars() {
68
- case "$1" in
69
- common) echo -n "★☆☆☆☆" ;;
70
- uncommon) echo -n "★★☆☆☆" ;;
71
- rare) echo -n "★★★☆☆" ;;
72
- epic) echo -n "★★★★☆" ;;
73
- legendary) echo -n "★★★★★" ;;
74
- esac
75
- }
76
-
77
- # ─── Check reaction TTL ─────────────────────────────────────────────────────
78
-
79
- reaction_fresh() {
80
- [ -f "$REACTION_FILE" ] || return 1
81
- # TTL=0 means permanent (always fresh)
82
- [ "$REACTION_TTL" -eq 0 ] && return 0
83
- local ts now age
84
- ts=$(jq -r '.timestamp // 0' "$REACTION_FILE" 2>/dev/null || echo 0)
85
- [ "$ts" = "0" ] && return 1
86
- # timestamp is in milliseconds (JS Date.now())
87
- now=$(date +%s)
88
- age=$(( now - ts / 1000 ))
89
- [ "$age" -lt "$REACTION_TTL" ]
90
- }
91
-
92
- # ─── Species art ─────────────────────────────────────────────────────────────
93
-
94
- get_art() {
95
- local species="$1" frame="$2" E="$3"
96
- case "$species" in
97
- duck)
98
- case $frame in
99
- 0) L1=" __"; L2=" <(${E} )___"; L3=" ( ._>"; L4=" \`--'" ;;
100
- 1) L1=" __"; L2=" <(${E} )___"; L3=" ( ._>"; L4=" \`--'~" ;;
101
- 2) L1=" __"; L2=" <(${E} )___"; L3=" ( .__>"; L4=" \`--'" ;;
102
- esac ;;
103
- goose)
104
- case $frame in
105
- 0) L1=" (${E}>"; L2=" ||"; L3=" _(__)_"; L4=" ^^^^" ;;
106
- 1) L1=" (${E}>"; L2=" ||"; L3=" _(__)_"; L4=" ^^^^" ;;
107
- 2) L1=" (${E}>>"; L2=" ||"; L3=" _(__)_"; L4=" ^^^^" ;;
108
- esac ;;
109
- blob)
110
- case $frame in
111
- 0) L1=" .----."; L2="( ${E} ${E} )"; L3="( )"; L4=" \`----'" ;;
112
- 1) L1=".------."; L2="( ${E} ${E} )"; L3="( )"; L4="\`------'" ;;
113
- 2) L1=" .--."; L2=" (${E} ${E})"; L3=" ( )"; L4=" \`--'" ;;
114
- esac ;;
115
- cat)
116
- case $frame in
117
- 0) L1=" /\\_/\\"; L2="( ${E} ${E})"; L3="( w )"; L4="(\")_(\")" ;;
118
- 1) L1=" /\\_/\\"; L2="( ${E} ${E})"; L3="( w )"; L4="(\")_(\")~" ;;
119
- 2) L1=" /\\-/\\"; L2="( ${E} ${E})"; L3="( w )"; L4="(\")_(\")" ;;
120
- esac ;;
121
- dragon)
122
- case $frame in
123
- 0) L1="/^\\ /^\\"; L2="< ${E} ${E} >"; L3="( ~~ )"; L4=" \`-vvvv-'" ;;
124
- 1) L1="/^\\ /^\\"; L2="< ${E} ${E} >"; L3="( )"; L4=" \`-vvvv-'" ;;
125
- 2) L1="/^\\ /^\\"; L2="< ${E} ${E} >"; L3="( ~~ )"; L4=" \`-vvvv-'" ;;
126
- esac ;;
127
- octopus)
128
- case $frame in
129
- 0) L1=" .----."; L2="( ${E} ${E} )"; L3="(______)"; L4="/\\/\\/\\/\\" ;;
130
- 1) L1=" .----."; L2="( ${E} ${E} )"; L3="(______)"; L4="\\/\\/\\/\\/" ;;
131
- 2) L1=" .----."; L2="( ${E} ${E} )"; L3="(______)"; L4="/\\/\\/\\/\\" ;;
132
- esac ;;
133
- owl)
134
- case $frame in
135
- 0) L1=" /\\ /\\"; L2="((${E})(${E}))"; L3="( >< )"; L4=" \`----'" ;;
136
- 1) L1=" /\\ /\\"; L2="((${E})(${E}))"; L3="( >< )"; L4=" .----." ;;
137
- 2) L1=" /\\ /\\"; L2="((${E})(-))"; L3="( >< )"; L4=" \`----'" ;;
138
- esac ;;
139
- penguin)
140
- case $frame in
141
- 0) L1=" .---."; L2=" (${E}>${E})"; L3="/( )\\"; L4=" \`---'" ;;
142
- 1) L1=" .---."; L2=" (${E}>${E})"; L3="|( )|"; L4=" \`---'" ;;
143
- 2) L1=" .---."; L2=" (${E}>${E})"; L3="/( )\\"; L4=" \`---'" ;;
144
- esac ;;
145
- turtle)
146
- case $frame in
147
- 0) L1=" _,--._"; L2="( ${E} ${E} )"; L3="[______]"; L4="\`\` \`\`" ;;
148
- 1) L1=" _,--._"; L2="( ${E} ${E} )"; L3="[______]"; L4=" \`\` \`\`" ;;
149
- 2) L1=" _,--._"; L2="( ${E} ${E} )"; L3="[======]"; L4="\`\` \`\`" ;;
150
- esac ;;
151
- snail)
152
- case $frame in
153
- 0) L1="${E} .--."; L2="\\ ( @ )"; L3=" \\_\`--'"; L4="~~~~~~~" ;;
154
- 1) L1=" ${E} .--."; L2="| ( @ )"; L3=" \\_\`--'"; L4="~~~~~~~" ;;
155
- 2) L1="${E} .--."; L2="\\ ( @ )"; L3=" \\_\`--'"; L4=" ~~~~~~" ;;
156
- esac ;;
157
- ghost)
158
- case $frame in
159
- 0) L1=" .----."; L2="/ ${E} ${E} \\"; L3="| |"; L4="~\`~\`\`~\`~" ;;
160
- 1) L1=" .----."; L2="/ ${E} ${E} \\"; L3="| |"; L4="\`~\`~~\`~\`" ;;
161
- 2) L1=" .----."; L2="/ ${E} ${E} \\"; L3="| |"; L4="~~\`~~\`~~" ;;
162
- esac ;;
163
- axolotl)
164
- case $frame in
165
- 0) L1="}~(____)~{"; L2="}~(${E}..${E})~{"; L3=" (.--.)"; L4=" (_/\\_)" ;;
166
- 1) L1="~}(____){~"; L2="~}(${E}..${E}){~"; L3=" (.--.)"; L4=" (_/\\_)" ;;
167
- 2) L1="}~(____)~{"; L2="}~(${E}..${E})~{"; L3=" ( -- )"; L4=" ~_/\\_~" ;;
168
- esac ;;
169
- capybara)
170
- case $frame in
171
- 0) L1="n______n"; L2="( ${E} ${E} )"; L3="( oo )"; L4="\`------'" ;;
172
- 1) L1="n______n"; L2="( ${E} ${E} )"; L3="( Oo )"; L4="\`------'" ;;
173
- 2) L1="u______n"; L2="( ${E} ${E} )"; L3="( oo )"; L4="\`------'" ;;
174
- esac ;;
175
- cactus)
176
- case $frame in
177
- 0) L1="n ____ n"; L2="||${E} ${E}||"; L3="|_| |_|"; L4=" | |" ;;
178
- 1) L1=" ____"; L2="n|${E} ${E}|n"; L3="|_| |_|"; L4=" | |" ;;
179
- 2) L1="n ____ n"; L2="||${E} ${E}||"; L3="|_| |_|"; L4=" | |" ;;
180
- esac ;;
181
- robot)
182
- case $frame in
183
- 0) L1=" .[||]."; L2="[ ${E} ${E} ]"; L3="[ ==== ]"; L4="\`------'" ;;
184
- 1) L1=" .[||]."; L2="[ ${E} ${E} ]"; L3="[ -==- ]"; L4="\`------'" ;;
185
- 2) L1=" .[||]."; L2="[ ${E} ${E} ]"; L3="[ ==== ]"; L4="\`------'" ;;
186
- esac ;;
187
- rabbit)
188
- case $frame in
189
- 0) L1=" (\\__/)"; L2="( ${E} ${E} )"; L3="=( .. )="; L4="(\")__(\")" ;;
190
- 1) L1=" (|__/)"; L2="( ${E} ${E} )"; L3="=( .. )="; L4="(\")__(\")" ;;
191
- 2) L1=" (\\__/)"; L2="( ${E} ${E} )"; L3="=( . . )="; L4="(\")__(\")" ;;
192
- esac ;;
193
- mushroom)
194
- case $frame in
195
- 0) L1="-o-OO-o-"; L2="(________)"; L3=" |${E}${E}|"; L4=" |__|" ;;
196
- 1) L1="-O-oo-O-"; L2="(________)"; L3=" |${E}${E}|"; L4=" |__|" ;;
197
- 2) L1="-o-OO-o-"; L2="(________)"; L3=" |${E}${E}|"; L4=" |__|" ;;
198
- esac ;;
199
- chonk)
200
- case $frame in
201
- 0) L1="/\\ /\\"; L2="( ${E} ${E} )"; L3="( .. )"; L4="\`------'" ;;
202
- 1) L1="/\\ /|"; L2="( ${E} ${E} )"; L3="( .. )"; L4="\`------'" ;;
203
- 2) L1="/\\ /\\"; L2="( ${E} ${E} )"; L3="( .. )"; L4="\`------'~" ;;
204
- esac ;;
205
- *)
206
- L1="(${E}${E})"; L2="( )"; L3=""; L4="" ;;
207
- esac
208
- }
209
-
210
- # ─── Center text, pad to full width ──────────────────────────────────────────
211
-
212
- center_pad() {
213
- local text="$1" width="$2"
214
- local len=${#text}
215
- local lpad=$(( (width - len) / 2 ))
216
- [ "$lpad" -lt 0 ] && lpad=0
217
- local rpad=$(( width - len - lpad ))
218
- [ "$rpad" -lt 0 ] && rpad=0
219
- printf '%*s%s%*s' "$lpad" '' "$text" "$rpad" ''
220
- }
221
-
222
- # Center a line within a block of known max_width, then center the block in pane
223
- center_block_line() {
224
- local text="$1" max_w="$2" pane="$3"
225
- local len=${#text}
226
- # Left-pad within the block to center each line relative to block width
227
- local inner_lpad=$(( (max_w - len) / 2 ))
228
- [ "$inner_lpad" -lt 0 ] && inner_lpad=0
229
- local inner_rpad=$(( max_w - len - inner_lpad ))
230
- [ "$inner_rpad" -lt 0 ] && inner_rpad=0
231
- local block_line
232
- block_line=$(printf '%*s%s%*s' "$inner_lpad" '' "$text" "$inner_rpad" '')
233
- # Now center the block within the pane
234
- center_pad "$block_line" "$pane"
235
- }
236
-
237
- # ─── Word wrap ───────────────────────────────────────────────────────────────
238
-
239
- word_wrap() {
240
- local text="$1" max_w="$2"
241
- local -a words=($text)
242
- local line=""
243
- WRAPPED_LINES=()
244
- for word in "${words[@]}"; do
245
- if [ -z "$line" ]; then
246
- line="$word"
247
- elif [ $(( ${#line} + 1 + ${#word} )) -le "$max_w" ]; then
248
- line="$line $word"
249
- else
250
- WRAPPED_LINES+=("$line")
251
- line="$word"
252
- fi
253
- done
254
- [ -n "$line" ] && WRAPPED_LINES+=("$line")
255
- }
256
-
257
- # ─── Render one frame ────────────────────────────────────────────────────────
258
- # Uses cursor positioning (\033[row;1H) for each line.
259
- # No clear screen, no newlines -- overwrites in place for flicker-free updates.
260
-
261
- render() {
262
- local frame_idx="$1"
263
- local pane_w="$PANE_W"
264
-
265
- [ -f "$STATE" ] || return
266
- local name species hat rarity reaction eye
267
- name=$(jq -r '.name // ""' "$STATE" 2>/dev/null)
268
- [ -z "$name" ] && return
269
- species=$(jq -r '.species // ""' "$STATE" 2>/dev/null)
270
- hat=$(jq -r '.hat // "none"' "$STATE" 2>/dev/null)
271
- rarity=$(jq -r '.rarity // "common"' "$STATE" 2>/dev/null)
272
- reaction=$(jq -r '.reaction // ""' "$STATE" 2>/dev/null)
273
- # Enforce TTL -- clear stale reactions
274
- if [ -n "$reaction" ] && [ "$reaction" != "null" ] && ! reaction_fresh; then
275
- reaction=""
276
- fi
277
- [ -f "$COMPANION" ] && eye=$(jq -r '.bones.eye // "o"' "$COMPANION" 2>/dev/null) || eye="o"
278
-
279
- local C
280
- C=$(rarity_color "$rarity")
281
-
282
- local frame=$frame_idx blink=0
283
- if [ "$frame" -eq -1 ]; then
284
- blink=1
285
- frame=0
286
- fi
287
-
288
- L1="" L2="" L3="" L4=""
289
- get_art "$species" "$frame" "$eye"
290
-
291
- if [ "$blink" -eq 1 ]; then
292
- L1="${L1//$eye/-}"; L2="${L2//$eye/-}"
293
- L3="${L3//$eye/-}"; L4="${L4//$eye/-}"
294
- fi
295
-
296
- # Build all output lines into an array, then write in one shot
297
- local -a OUT=()
298
- local row=1
299
-
300
- # Bubble style chars
301
- local bchar lside rside
302
- if [ "$BUBBLE_STYLE" = "round" ]; then
303
- bchar='~'; lside='('; rside=')'
304
- else
305
- bchar='-'; lside='|'; rside='|'
306
- fi
307
-
308
- # Collect art lines (hat + species art)
309
- local -a ART_LINES=()
310
- local hat_line=""
311
- case "$hat" in
312
- crown) hat_line="\\^^^/" ;;
313
- tophat) hat_line="[___]" ;;
314
- propeller) hat_line="-+-" ;;
315
- halo) hat_line="( )" ;;
316
- wizard) hat_line="/^\\" ;;
317
- beanie) hat_line="(___)" ;;
318
- tinyduck) hat_line=",>" ;;
319
- esac
320
- [ -n "$hat_line" ] && ART_LINES+=("$hat_line")
321
- for line in "$L1" "$L2" "$L3" "$L4"; do
322
- [ -n "$line" ] && ART_LINES+=("$line")
323
- done
324
-
325
- # Find widest art line for block centering
326
- local art_max_w=0
327
- for al in "${ART_LINES[@]}"; do
328
- [ ${#al} -gt "$art_max_w" ] && art_max_w=${#al}
329
- done
330
-
331
- # Determine if we have a reaction to show
332
- local has_reaction=0
333
- local -a BUBBLE_LINES_ARR=()
334
- local -a BUBBLE_TYPES=()
335
- if [ -n "$reaction" ] && [ "$reaction" != "null" ]; then
336
- has_reaction=1
337
- fi
338
-
339
- if [ "$has_reaction" -eq 1 ] && [ "$BUBBLE_POSITION" = "left" ]; then
340
- # ─── Left bubble: art stays in fixed right section, bubble on left ──
341
- local art_w="${POPUP_ART_W:-$pane_w}" # art area = base popup width
342
- local bubble_area=$(( pane_w - art_w )) # 0 when no extra width
343
-
344
- if [ "$bubble_area" -gt 6 ]; then
345
- local inner_w=$(( bubble_area - 6 )) # lside(1)+space(1)+text+space(1)+rside(1) + gap(2)
346
- [ "$inner_w" -lt 4 ] && inner_w=4
347
- local box_w=$(( inner_w + 4 )) # "| " + text + " |"
348
- local gap=1
349
-
350
- word_wrap "$reaction" "$inner_w"
351
-
352
- # Build bubble box lines
353
- local border
354
- border=$(printf '%*s' "$((inner_w + 2))" '' | tr ' ' "$bchar")
355
- BUBBLE_LINES_ARR+=(".${border}.")
356
- BUBBLE_TYPES+=("border")
357
- for tl in "${WRAPPED_LINES[@]}"; do
358
- local tpad=$(( inner_w - ${#tl} ))
359
- [ "$tpad" -lt 0 ] && tpad=0
360
- local padding
361
- padding=$(printf '%*s' "$tpad" '')
362
- BUBBLE_LINES_ARR+=("${lside} ${tl}${padding} ${rside}")
363
- BUBBLE_TYPES+=("text")
364
- done
365
- BUBBLE_LINES_ARR+=("\`${border}'")
366
- BUBBLE_TYPES+=("border")
367
-
368
- local bubble_count=${#BUBBLE_LINES_ARR[@]}
369
- local art_count=${#ART_LINES[@]}
370
-
371
- # Find connector line (middle text row)
372
- local connector_bi=-1
373
- if [ "$bubble_count" -gt 2 ]; then
374
- connector_bi=$(( (1 + bubble_count - 2) / 2 ))
375
- fi
376
-
377
- # Vertically center bubble on art
378
- local bubble_start=0
379
- if [ "$bubble_count" -lt "$art_count" ]; then
380
- bubble_start=$(( (art_count - bubble_count) / 2 ))
381
- fi
382
-
383
- local total_rows=$art_count
384
- [ "$((bubble_start + bubble_count))" -gt "$total_rows" ] && total_rows=$((bubble_start + bubble_count))
385
- local gap_str
386
- for (( i=0; i<total_rows; i++ )); do
387
- local bi=$(( i - bubble_start ))
388
- local art_part=""
389
- if [ "$i" -lt "$art_count" ]; then
390
- art_part="${ART_LINES[$i]}"
391
- fi
392
-
393
- if [ "$bi" -ge 0 ] && [ "$bi" -lt "$bubble_count" ]; then
394
- local bline="${BUBBLE_LINES_ARR[$bi]}"
395
- local btype="${BUBBLE_TYPES[$bi]}"
396
- # Pad bubble line to box_w
397
- local bline_padded
398
- bline_padded=$(printf '%-*s' "$box_w" "$bline")
399
- if [ "$bi" -eq "$connector_bi" ]; then
400
- gap_str="${C}--${NC}"
401
- else
402
- gap_str=" "
403
- fi
404
- if [ "$btype" = "border" ]; then
405
- OUT+=("$(printf '\033[%d;1H' "$row")${DIM}${bline_padded}${NC}${gap_str}${C}$(center_block_line "$art_part" "$art_max_w" "$art_w")${NC}")
406
- else
407
- OUT+=("$(printf '\033[%d;1H' "$row")${DIM}${bline_padded}${NC}${gap_str}${C}$(center_block_line "$art_part" "$art_max_w" "$art_w")${NC}")
408
- fi
409
- else
410
- local empty
411
- empty=$(printf '%*s' "$((box_w + 2))" '')
412
- OUT+=("$(printf '\033[%d;1H' "$row")${empty}${C}$(center_block_line "$art_part" "$art_max_w" "$art_w")${NC}")
413
- fi
414
- row=$((row + 1))
415
- done
416
- else
417
- # Bubble area too narrow, fall back to art-only
418
- for line in "${ART_LINES[@]}"; do
419
- OUT+=("$(printf '\033[%d;1H%s%s%s' "$row" "$C" "$(center_pad "$line" "$pane_w")" "$NC")")
420
- row=$((row + 1))
421
- done
422
- fi
423
-
424
- else
425
- # ─── Top bubble (or no bubble): original layout ─────────────────────
426
- local bubble_w=$(( pane_w - 5 ))
427
- [ "$bubble_w" -lt 8 ] && bubble_w=8
428
-
429
- if [ "$has_reaction" -eq 1 ]; then
430
- word_wrap "$reaction" "$bubble_w"
431
- if [ ${#WRAPPED_LINES[@]} -gt 0 ]; then
432
- local border
433
- border=$(printf '%*s' "$((bubble_w + 2))" '' | tr ' ' "$bchar")
434
- OUT+=("$(printf '\033[%d;1H%-*s' "$row" "$pane_w" " ${DIM}.${border}.${NC}")")
435
- row=$((row + 1))
436
- for tl in "${WRAPPED_LINES[@]}"; do
437
- local tpad=$(( bubble_w - ${#tl} ))
438
- [ "$tpad" -lt 0 ] && tpad=0
439
- local padding
440
- padding=$(printf '%*s' "$tpad" '')
441
- OUT+=("$(printf '\033[%d;1H%-*s' "$row" "$pane_w" " ${DIM}${lside}${NC} ${tl}${padding} ${DIM}${rside}${NC}")")
442
- row=$((row + 1))
443
- done
444
- OUT+=("$(printf '\033[%d;1H%-*s' "$row" "$pane_w" " ${DIM}\`${border}'${NC}")")
445
- row=$((row + 1))
446
- OUT+=("$(printf '\033[%d;1H%-*s' "$row" "$pane_w" "$(center_pad '\' "$pane_w")")")
447
- row=$((row + 1))
448
- fi
449
- fi
450
-
451
- # Art lines (hat + species) -- centered as a block
452
- for line in "${ART_LINES[@]}"; do
453
- OUT+=("$(printf '\033[%d;1H%s%s%s' "$row" "$C" "$(center_block_line "$line" "$art_max_w" "$pane_w")" "$NC")")
454
- row=$((row + 1))
455
- done
456
- fi
457
-
458
- # Width for name/stars: in left mode, keep them under the art area (right side)
459
- local label_w="$pane_w"
460
- local label_offset=""
461
- local art_base="${POPUP_ART_W:-$pane_w}"
462
- if [ "$BUBBLE_POSITION" = "left" ] && [ "$pane_w" -gt "$art_base" ]; then
463
- label_w=$art_base
464
- local offset_cols=$(( pane_w - art_base ))
465
- label_offset=$(printf '%*s' "$offset_cols" '')
466
- fi
467
-
468
- # Blank line
469
- OUT+=("$(printf '\033[%d;1H%*s' "$row" "$pane_w" '')")
470
- row=$((row + 1))
471
-
472
- # Name
473
- OUT+=("$(printf '\033[%d;1H%s%s%s%s' "$row" "$label_offset" "${BOLD}${C}" "$(center_pad "$name" "$label_w")" "$NC")")
474
- row=$((row + 1))
475
-
476
- # Stars + rarity (uses SHOW_RARITY from startup config)
477
- if [ "$SHOW_RARITY" -eq 1 ]; then
478
- local stars
479
- stars=$(rarity_stars "$rarity")
480
- OUT+=("$(printf '\033[%d;1H%s%s%s%s' "$row" "$label_offset" "$DIM" "$(center_pad "$stars $rarity" "$label_w")" "$NC")")
481
- row=$((row + 1))
482
- fi
483
-
484
- # Clear remaining rows
485
- while [ "$row" -le "$PANE_H" ]; do
486
- OUT+=("$(printf '\033[%d;1H%*s' "$row" "$pane_w" '')")
487
- row=$((row + 1))
488
- done
489
-
490
- # Write everything at once (minimize flicker)
491
- printf '%s' "${OUT[@]}"
492
- }
493
-
494
- # ─── Resize trigger ─────────────────────────────────────────────────────────
495
- # When reaction state changes (appears/disappears), request a popup resize
496
- # by writing a flag and killing the parent (perl forwarder). The reopen loop
497
- # sees the flag and reopens with the new height without forwarding ESC.
498
-
499
- request_resize() {
500
- touch "$RESIZE_FLAG"
501
- kill $PPID 2>/dev/null
502
- exit 0
503
- }
504
-
505
- # ─── Main loop ───────────────────────────────────────────────────────────────
506
-
507
- # Initialize SHOWING_REACTION to match current state so we don't
508
- # trigger a spurious resize on startup (which causes flicker loops).
509
- if [ -f "$REACTION_FILE" ] && [ -f "$STATE" ]; then
510
- _init_reaction=$(jq -r '.reaction // ""' "$STATE" 2>/dev/null || true)
511
- if [ -n "$_init_reaction" ] && [ "$_init_reaction" != "null" ] && reaction_fresh; then
512
- SHOWING_REACTION=1
513
- fi
514
- fi
515
-
516
- # Initial clear
517
- printf '\033[2J'
518
-
519
- while true; do
520
- [ -f "$STATE" ] || { sleep 0.5; continue; }
521
-
522
- # Check if reaction state changed (need resize)
523
- HAS_REACTION=0
524
- if [ -f "$REACTION_FILE" ] && [ -f "$STATE" ]; then
525
- local_reaction=$(jq -r '.reaction // ""' "$STATE" 2>/dev/null || true)
526
- if [ -n "$local_reaction" ] && [ "$local_reaction" != "null" ] && reaction_fresh; then
527
- HAS_REACTION=1
528
- fi
529
- fi
530
-
531
- if [ "$HAS_REACTION" -ne "$SHOWING_REACTION" ]; then
532
- SHOWING_REACTION=$HAS_REACTION
533
- request_resize
534
- fi
535
-
536
- FRAME_IDX=${SEQ[$((TICK % SEQ_LEN))]}
537
- render "$FRAME_IDX"
538
- TICK=$((TICK + 1))
539
- sleep 0.5
540
- done