@ramarivera/coding-buddy 0.4.0-alpha.2 → 0.4.0-alpha.3
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 +4 -5
- package/{hooks → adapters/claude/hooks}/hooks.json +3 -3
- package/{cli → adapters/claude/install}/backup.ts +3 -3
- package/{cli → adapters/claude/install}/disable.ts +22 -2
- package/{cli → adapters/claude/install}/doctor.ts +30 -23
- package/{cli → adapters/claude/install}/hunt.ts +4 -4
- package/{cli → adapters/claude/install}/install.ts +62 -26
- package/{cli → adapters/claude/install}/pick.ts +3 -3
- package/{cli → adapters/claude/install}/settings.ts +1 -1
- package/{cli → adapters/claude/install}/show.ts +2 -2
- package/{cli → adapters/claude/install}/uninstall.ts +22 -2
- package/{.claude-plugin → adapters/claude/plugin}/plugin.json +1 -1
- package/adapters/claude/popup/buddy-popup.sh +92 -0
- package/adapters/claude/popup/buddy-render.sh +540 -0
- package/adapters/claude/popup/popup-manager.sh +355 -0
- package/{server → adapters/claude/rendering}/art.ts +3 -115
- package/{server → adapters/claude/server}/index.ts +49 -71
- package/adapters/claude/server/instructions.ts +24 -0
- package/adapters/claude/server/resources.ts +38 -0
- package/adapters/claude/storage/achievements.ts +253 -0
- package/adapters/claude/storage/identity.ts +14 -0
- package/adapters/claude/storage/settings.ts +42 -0
- package/{server → adapters/claude/storage}/state.ts +3 -65
- package/adapters/pi/README.md +64 -0
- package/adapters/pi/commands.ts +173 -0
- package/adapters/pi/events.ts +150 -0
- package/adapters/pi/identity.ts +10 -0
- package/adapters/pi/index.ts +25 -0
- package/adapters/pi/renderers.ts +73 -0
- package/adapters/pi/storage.ts +295 -0
- package/adapters/pi/tools.ts +6 -0
- package/adapters/pi/ui.ts +39 -0
- package/cli/index.ts +11 -11
- package/cli/verify.ts +2 -2
- package/core/achievements.ts +203 -0
- package/core/art-data.ts +105 -0
- package/core/command-service.ts +338 -0
- package/core/model.ts +59 -0
- package/core/ports.ts +40 -0
- package/core/render-model.ts +10 -0
- package/package.json +23 -19
- package/server/achievements.ts +0 -445
- /package/{hooks → adapters/claude/hooks}/buddy-comment.sh +0 -0
- /package/{hooks → adapters/claude/hooks}/name-react.sh +0 -0
- /package/{hooks → adapters/claude/hooks}/react.sh +0 -0
- /package/{cli → adapters/claude/install}/test-statusline.sh +0 -0
- /package/{cli → adapters/claude/install}/test-statusline.ts +0 -0
- /package/{.claude-plugin → adapters/claude/plugin}/marketplace.json +0 -0
- /package/{skills → adapters/claude/skills}/buddy/SKILL.md +0 -0
- /package/{statusline → adapters/claude/statusline}/buddy-status.sh +0 -0
- /package/{server → core}/engine.ts +0 -0
- /package/{server → core}/reactions.ts +0 -0
|
@@ -0,0 +1,540 @@
|
|
|
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
|