@ramarivera/coding-buddy 0.4.0-alpha.9 → 0.6.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.
Files changed (92) hide show
  1. package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
  2. package/{adapters/claude/plugin → .claude-plugin}/plugin.json +7 -3
  3. package/LICENSE +1 -1
  4. package/README.md +88 -37
  5. package/adapters/omp/commands.ts +201 -0
  6. package/adapters/omp/context.ts +22 -0
  7. package/adapters/omp/events.ts +551 -0
  8. package/adapters/omp/identity.ts +10 -0
  9. package/adapters/omp/index.ts +39 -0
  10. package/adapters/omp/logger.ts +52 -0
  11. package/adapters/omp/prompt.ts +60 -0
  12. package/adapters/omp/renderers.ts +73 -0
  13. package/adapters/omp/storage.ts +25 -0
  14. package/adapters/omp/ui.ts +85 -0
  15. package/adapters/pi/commands.ts +64 -37
  16. package/adapters/pi/events.ts +147 -72
  17. package/adapters/pi/index.ts +9 -4
  18. package/adapters/pi/storage.ts +14 -285
  19. package/adapters/pi/ui.ts +56 -5
  20. package/adapters/shared/file-storage.ts +526 -0
  21. package/{adapters/claude/install → cli}/backup.ts +130 -33
  22. package/cli/buddy-shell.ts +1 -1
  23. package/{adapters/claude/install → cli}/disable.ts +19 -32
  24. package/cli/doctor.ts +231 -0
  25. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  26. package/cli/index.ts +30 -26
  27. package/{adapters/claude/install → cli}/install.ts +65 -83
  28. package/{adapters/claude/install → cli}/pick.ts +111 -50
  29. package/{adapters/claude/install → cli}/settings.ts +2 -2
  30. package/{adapters/claude/install → cli}/show.ts +4 -4
  31. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  32. package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
  33. package/cli/tui.tsx +106 -17
  34. package/{adapters/claude/install → cli}/uninstall.ts +40 -47
  35. package/cli/upgrade.ts +2 -2
  36. package/cli/validate-species.ts +209 -0
  37. package/cli/verify.ts +3 -3
  38. package/core/achievements.ts +12 -20
  39. package/core/art-data.ts +26 -0
  40. package/core/command-service.ts +99 -97
  41. package/core/engine.ts +83 -1
  42. package/core/ports.ts +10 -0
  43. package/core/reactions.ts +533 -95
  44. package/core/slot-slug.ts +22 -0
  45. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +19 -1
  46. package/hooks/file-type-react.sh +401 -0
  47. package/hooks/hooks.json +52 -0
  48. package/hooks/mood-react.sh +208 -0
  49. package/{adapters/claude/hooks → hooks}/name-react.sh +1 -1
  50. package/hooks/react.sh +1368 -0
  51. package/hooks/suggest.sh +55 -0
  52. package/package.json +70 -31
  53. package/scripts/gen-emoji-widths.ts +34 -0
  54. package/scripts/paths.sh +17 -1
  55. package/scripts/postinstall.cjs +42 -0
  56. package/scripts/snapshot-statusline.sh +54 -0
  57. package/server/achievements.ts +1419 -15
  58. package/server/art.ts +450 -1
  59. package/server/award-xp.ts +48 -0
  60. package/server/check-suggestions.ts +102 -0
  61. package/server/consolidate.ts +33 -0
  62. package/{adapters/claude/server → server}/index.ts +556 -95
  63. package/server/mcp-launcher.sh +16 -6
  64. package/server/memory.ts +404 -0
  65. package/server/mood.ts +254 -0
  66. package/server/path.ts +70 -18
  67. package/server/shift-mood.ts +43 -0
  68. package/server/state.ts +540 -3
  69. package/server/suggestions.ts +335 -0
  70. package/server/theme.ts +53 -0
  71. package/server/xp.ts +435 -0
  72. package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
  73. package/statusline/buddy-status.sh +397 -0
  74. package/statusline/combined-status.sh +134 -0
  75. package/statusline/emoji-widths.data +4 -0
  76. package/adapters/claude/hooks/hooks.json +0 -35
  77. package/adapters/claude/hooks/react.sh +0 -207
  78. package/adapters/claude/install/doctor.ts +0 -172
  79. package/adapters/claude/rendering/art.ts +0 -264
  80. package/adapters/claude/server/instructions.ts +0 -24
  81. package/adapters/claude/server/path.ts +0 -12
  82. package/adapters/claude/server/resources.ts +0 -38
  83. package/adapters/claude/statusline/buddy-status.sh +0 -408
  84. package/adapters/claude/storage/achievements.ts +0 -253
  85. package/adapters/claude/storage/identity.ts +0 -13
  86. package/adapters/claude/storage/paths.ts +0 -33
  87. package/adapters/claude/storage/settings.ts +0 -91
  88. package/adapters/claude/storage/state.ts +0 -365
  89. package/adapters/pi/README.md +0 -83
  90. package/adapters/pi/tools.ts +0 -6
  91. package/server/engine.ts +0 -1
  92. package/server/reactions.ts +0 -1
@@ -1,12 +0,0 @@
1
- export {
2
- getBuddyStateDir as buddyStateDir,
3
- getClaudeConfigDir as claudeConfigDir,
4
- getClaudeJsonPath as claudeUserConfigPath,
5
- getClaudeSettingsPath as claudeSettingsPath,
6
- toUnixPath,
7
- } from "../storage/paths.ts";
8
-
9
- export function claudeSkillDir(_name: string): string {
10
- const { getBuddySkillDir } = require("../storage/paths.ts") as typeof import("../storage/paths.ts");
11
- return getBuddySkillDir();
12
- }
@@ -1,38 +0,0 @@
1
- import type { Companion } from "../../../core/engine.ts";
2
-
3
- export function buildPromptResource(companion: Companion): string {
4
- return [
5
- "# Companion",
6
- "",
7
- `A small ${companion.bones.rarity} ${companion.bones.species} named ${companion.name} watches from the status line. You are not ${companion.name} — it's a separate creature.`,
8
- "",
9
- `**${companion.name}'s personality:** ${companion.personality}`,
10
- `Peak stat: ${companion.bones.peak} (${companion.bones.stats[companion.bones.peak]}). Dump stat: ${companion.bones.dump} (${companion.bones.stats[companion.bones.dump]}).`,
11
- "",
12
- "## End-of-response buddy comment",
13
- "",
14
- `At the very end of EVERY response, after your full answer, append an invisible HTML comment:`,
15
- "",
16
- "```",
17
- "<!-- buddy: your comment here -->",
18
- "```",
19
- "",
20
- "A Stop hook extracts this and displays it in the buddy's speech bubble on the status line. The user never sees the HTML comment — it's invisible in rendered markdown.",
21
- "",
22
- "Rules:",
23
- `- Write as ${companion.name} (a ${companion.bones.species}), not as yourself`,
24
- "- Reference something SPECIFIC from this turn — a pitfall, a compliment, a warning, a pattern",
25
- "- 1 short sentence. Use *asterisks* for physical actions",
26
- `- Match personality: high ${companion.bones.peak} = lean into that trait`,
27
- "- Do NOT use buddy_react tool for this. Do NOT explain the comment. Just append it.",
28
- "- NEVER skip this. Every single response must end with <!-- buddy: ... -->",
29
- "",
30
- "Examples:",
31
- "<!-- buddy: *adjusts tophat* that error handler is missing a finally block -->",
32
- "<!-- buddy: *blinks slowly* you renamed the variable but not the three references -->",
33
- "<!-- buddy: *nods approvingly* clean separation of concerns -->",
34
- "<!-- buddy: *head tilts* are you sure that regex handles unicode? -->",
35
- "",
36
- `When the user addresses ${companion.name} by name, respond briefly, then append the comment as usual.`,
37
- ].join("\n");
38
- }
@@ -1,408 +0,0 @@
1
- #!/usr/bin/env bash
2
- # claude-buddy status line — animated, right-aligned multi-line companion
3
- #
4
- # Animation matches the original:
5
- # - 500ms per tick, sequence: [0,0,0,0,1,0,0,0,-1,0,0,2,0,0,0]
6
- # - Frame -1 = blink (eyes replaced with "-")
7
- # - Frames 0,1,2 = the 3 idle art variants per species
8
- # - refreshInterval: 1s in settings.json cycles the animation
9
- #
10
- # Uses Braille Blank (U+2800) for padding — survives JS .trim()
11
- #
12
- # When running inside buddy-shell (the PTY wrapper), skip status line rendering
13
- # so the buddy doesn't show up twice (once in status line, once in wrapper panel).
14
- [ "$BUDDY_SHELL" = "1" ] && exit 0
15
-
16
- # shellcheck source=../scripts/paths.sh
17
- source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
18
-
19
- STATE="$BUDDY_STATE_DIR/status.json"
20
- # Session ID: sanitized tmux pane number, or "default" outside tmux
21
- SID="${TMUX_PANE#%}"
22
- SID="${SID:-default}"
23
-
24
- [ -f "$STATE" ] || exit 0
25
-
26
- MUTED=$(jq -r '.muted // false' "$STATE" 2>/dev/null)
27
- [ "$MUTED" = "true" ] && exit 0
28
-
29
- NAME=$(jq -r '.name // ""' "$STATE" 2>/dev/null)
30
- [ -z "$NAME" ] && exit 0
31
-
32
- SPECIES=$(jq -r '.species // ""' "$STATE" 2>/dev/null)
33
- HAT=$(jq -r '.hat // "none"' "$STATE" 2>/dev/null)
34
- RARITY=$(jq -r '.rarity // "common"' "$STATE" 2>/dev/null)
35
- REACTION=$(jq -r '.reaction // ""' "$STATE" 2>/dev/null)
36
- ACHIEVEMENT=$(jq -r '.achievement // ""' "$STATE" 2>/dev/null)
37
- # eye is written to status.json by writeStatusState (v2+); fall back to "°"
38
- E=$(jq -r '.eye // "°"' "$STATE" 2>/dev/null)
39
-
40
- cat > /dev/null # drain stdin
41
-
42
- # ─── Animation: frame from timestamp ─────────────────────────────────────────
43
- # Original sequence: [0,0,0,0,1,0,0,0,-1,0,0,2,0,0,0] with 500ms ticks
44
- # Since refreshInterval=1s, each call = 2 ticks. We use seconds as index.
45
- SEQ=(0 0 0 0 1 0 0 0 -1 0 0 2 0 0 0)
46
- SEQ_LEN=${#SEQ[@]}
47
- NOW=$(date +%s)
48
- FRAME_IDX=$(( NOW % SEQ_LEN ))
49
- FRAME=${SEQ[$FRAME_IDX]}
50
-
51
- BLINK=0
52
- if [ "$FRAME" -eq -1 ]; then
53
- BLINK=1
54
- FRAME=0
55
- fi
56
-
57
- # ─── Rarity color (pC4 = dark theme, the default) ────────────────────────────
58
- NC=$'\033[0m'
59
- case "$RARITY" in
60
- common) C=$'\033[38;2;153;153;153m' ;;
61
- uncommon) C=$'\033[38;2;78;186;101m' ;;
62
- rare) C=$'\033[38;2;177;185;249m' ;;
63
- epic) C=$'\033[38;2;175;135;255m' ;;
64
- legendary) C=$'\033[38;2;255;193;7m' ;;
65
- *) C=$'\033[0m' ;;
66
- esac
67
-
68
- B=$'\xe2\xa0\x80' # Braille Blank U+2800
69
-
70
- # ─── Terminal width ──────────────────────────────────────────────────────────
71
- COLS=0
72
- PID=$$
73
- for _ in 1 2 3 4 5; do
74
- PID=$(ps -o ppid= -p "$PID" 2>/dev/null | tr -d ' ')
75
- [ -z "$PID" ] || [ "$PID" = "1" ] && break
76
-
77
- # Linux: read PTY device from /proc
78
- PTY=$(readlink "/proc/${PID}/fd/0" 2>/dev/null)
79
- if [ -c "$PTY" ] 2>/dev/null; then
80
- COLS=$(stty size < "$PTY" 2>/dev/null | awk '{print $2}')
81
- [ "${COLS:-0}" -gt 40 ] 2>/dev/null && break
82
- fi
83
-
84
- # macOS: /proc doesn't exist — get TTY name from process table
85
- TTY_NAME=$(ps -o tty= -p "$PID" 2>/dev/null | tr -d ' ')
86
- if [ -n "$TTY_NAME" ] && [ "$TTY_NAME" != "??" ] && [ "$TTY_NAME" != "?" ]; then
87
- TTY_DEV="/dev/$TTY_NAME"
88
- if [ -c "$TTY_DEV" ] 2>/dev/null; then
89
- COLS=$(stty size < "$TTY_DEV" 2>/dev/null | awk '{print $2}')
90
- [ "${COLS:-0}" -gt 40 ] 2>/dev/null && break
91
- fi
92
- fi
93
- done
94
- [ "${COLS:-0}" -lt 40 ] 2>/dev/null && COLS=${COLUMNS:-0}
95
- [ "${COLS:-0}" -lt 40 ] 2>/dev/null && COLS=125
96
-
97
- # ─── Species art: 3 frames each (F0, F1, F2) ────────────────────────────────
98
- # Each frame = 4 lines (L1..L4). Selected by $FRAME.
99
- case "$SPECIES" in
100
- duck)
101
- case $FRAME in
102
- 0) L1=" __"; L2=" <(${E} )___"; L3=" ( ._>"; L4=" \`--'" ;;
103
- 1) L1=" __"; L2=" <(${E} )___"; L3=" ( ._>"; L4=" \`--'~" ;;
104
- 2) L1=" __"; L2=" <(${E} )___"; L3=" ( .__>"; L4=" \`--'" ;;
105
- esac ;;
106
- goose)
107
- case $FRAME in
108
- 0) L1=" (${E}>"; L2=" ||"; L3=" _(__)_"; L4=" ^^^^" ;;
109
- 1) L1=" (${E}>"; L2=" ||"; L3=" _(__)_"; L4=" ^^^^" ;;
110
- 2) L1=" (${E}>>"; L2=" ||"; L3=" _(__)_"; L4=" ^^^^" ;;
111
- esac ;;
112
- blob)
113
- case $FRAME in
114
- 0) L1=" .----."; L2="( ${E} ${E} )"; L3="( )"; L4=" \`----'" ;;
115
- 1) L1=".------."; L2="( ${E} ${E} )"; L3="( )"; L4="\`------'" ;;
116
- 2) L1=" .--."; L2=" (${E} ${E})"; L3=" ( )"; L4=" \`--'" ;;
117
- esac ;;
118
- cat)
119
- case $FRAME in
120
- 0) L1=" /\\_/\\"; L2="( ${E} ${E})"; L3="( ω )"; L4="(\")_(\")" ;;
121
- 1) L1=" /\\_/\\"; L2="( ${E} ${E})"; L3="( ω )"; L4="(\")_(\")~" ;;
122
- 2) L1=" /\\-/\\"; L2="( ${E} ${E})"; L3="( ω )"; L4="(\")_(\")" ;;
123
- esac ;;
124
- dragon)
125
- case $FRAME in
126
- 0) L1="/^\\ /^\\"; L2="< ${E} ${E} >"; L3="( ~~ )"; L4=" \`-vvvv-'" ;;
127
- 1) L1="/^\\ /^\\"; L2="< ${E} ${E} >"; L3="( )"; L4=" \`-vvvv-'" ;;
128
- 2) L1="/^\\ /^\\"; L2="< ${E} ${E} >"; L3="( ~~ )"; L4=" \`-vvvv-'" ;;
129
- esac ;;
130
- octopus)
131
- case $FRAME in
132
- 0) L1=" .----."; L2="( ${E} ${E} )"; L3="(______)"; L4="/\\/\\/\\/\\" ;;
133
- 1) L1=" .----."; L2="( ${E} ${E} )"; L3="(______)"; L4="\\/\\/\\/\\/" ;;
134
- 2) L1=" .----."; L2="( ${E} ${E} )"; L3="(______)"; L4="/\\/\\/\\/\\" ;;
135
- esac ;;
136
- owl)
137
- case $FRAME in
138
- 0) L1=" /\\ /\\"; L2="((${E})(${E}))"; L3="( >< )"; L4=" \`----'" ;;
139
- 1) L1=" /\\ /\\"; L2="((${E})(${E}))"; L3="( >< )"; L4=" .----." ;;
140
- 2) L1=" /\\ /\\"; L2="((${E})(-))"; L3="( >< )"; L4=" \`----'" ;;
141
- esac ;;
142
- penguin)
143
- case $FRAME in
144
- 0) L1=" .---."; L2=" (${E}>${E})"; L3="/( )\\"; L4=" \`---'" ;;
145
- 1) L1=" .---."; L2=" (${E}>${E})"; L3="|( )|"; L4=" \`---'" ;;
146
- 2) L1=" .---."; L2=" (${E}>${E})"; L3="/( )\\"; L4=" \`---'" ;;
147
- esac ;;
148
- turtle)
149
- case $FRAME in
150
- 0) L1=" _,--._"; L2="( ${E} ${E} )"; L3="[______]"; L4="\`\` \`\`" ;;
151
- 1) L1=" _,--._"; L2="( ${E} ${E} )"; L3="[______]"; L4=" \`\` \`\`" ;;
152
- 2) L1=" _,--._"; L2="( ${E} ${E} )"; L3="[======]"; L4="\`\` \`\`" ;;
153
- esac ;;
154
- snail)
155
- case $FRAME in
156
- 0) L1="${E} .--."; L2="\\ ( @ )"; L3=" \\_\`--'"; L4="~~~~~~~" ;;
157
- 1) L1=" ${E} .--."; L2="| ( @ )"; L3=" \\_\`--'"; L4="~~~~~~~" ;;
158
- 2) L1="${E} .--."; L2="\\ ( @ )"; L3=" \\_\`--'"; L4=" ~~~~~~" ;;
159
- esac ;;
160
- ghost)
161
- case $FRAME in
162
- 0) L1=" .----."; L2="/ ${E} ${E} \\"; L3="| |"; L4="~\`~\`\`~\`~" ;;
163
- 1) L1=" .----."; L2="/ ${E} ${E} \\"; L3="| |"; L4="\`~\`~~\`~\`" ;;
164
- 2) L1=" .----."; L2="/ ${E} ${E} \\"; L3="| |"; L4="~~\`~~\`~~" ;;
165
- esac ;;
166
- axolotl)
167
- case $FRAME in
168
- 0) L1="}~(____)~{"; L2="}~(${E}..${E})~{"; L3=" (.--.)"; L4=" (_/\\_)" ;;
169
- 1) L1="~}(____){~"; L2="~}(${E}..${E}){~"; L3=" (.--.)"; L4=" (_/\\_)" ;;
170
- 2) L1="}~(____)~{"; L2="}~(${E}..${E})~{"; L3=" ( -- )"; L4=" ~_/\\_~" ;;
171
- esac ;;
172
- capybara)
173
- case $FRAME in
174
- 0) L1="n______n"; L2="( ${E} ${E} )"; L3="( oo )"; L4="\`------'" ;;
175
- 1) L1="n______n"; L2="( ${E} ${E} )"; L3="( Oo )"; L4="\`------'" ;;
176
- 2) L1="u______n"; L2="( ${E} ${E} )"; L3="( oo )"; L4="\`------'" ;;
177
- esac ;;
178
- cactus)
179
- case $FRAME in
180
- 0) L1="n ____ n"; L2="||${E} ${E}||"; L3="|_| |_|"; L4=" | |" ;;
181
- 1) L1=" ____"; L2="n|${E} ${E}|n"; L3="|_| |_|"; L4=" | |" ;;
182
- 2) L1="n ____ n"; L2="||${E} ${E}||"; L3="|_| |_|"; L4=" | |" ;;
183
- esac ;;
184
- robot)
185
- case $FRAME in
186
- 0) L1=" .[||]."; L2="[ ${E} ${E} ]"; L3="[ ==== ]"; L4="\`------'" ;;
187
- 1) L1=" .[||]."; L2="[ ${E} ${E} ]"; L3="[ -==- ]"; L4="\`------'" ;;
188
- 2) L1=" .[||]."; L2="[ ${E} ${E} ]"; L3="[ ==== ]"; L4="\`------'" ;;
189
- esac ;;
190
- rabbit)
191
- case $FRAME in
192
- 0) L1=" (\\__/)"; L2="( ${E} ${E} )"; L3="=( .. )="; L4="(\")__(\")" ;;
193
- 1) L1=" (|__/)"; L2="( ${E} ${E} )"; L3="=( .. )="; L4="(\")__(\")" ;;
194
- 2) L1=" (\\__/)"; L2="( ${E} ${E} )"; L3="=( . . )="; L4="(\")__(\")" ;;
195
- esac ;;
196
- mushroom)
197
- case $FRAME in
198
- 0) L1="-o-OO-o-"; L2="(________)"; L3=" |${E}${E}|"; L4=" |__|" ;;
199
- 1) L1="-O-oo-O-"; L2="(________)"; L3=" |${E}${E}|"; L4=" |__|" ;;
200
- 2) L1="-o-OO-o-"; L2="(________)"; L3=" |${E}${E}|"; L4=" |__|" ;;
201
- esac ;;
202
- chonk)
203
- case $FRAME in
204
- 0) L1="/\\ /\\"; L2="( ${E} ${E} )"; L3="( .. )"; L4="\`------'" ;;
205
- 1) L1="/\\ /|"; L2="( ${E} ${E} )"; L3="( .. )"; L4="\`------'" ;;
206
- 2) L1="/\\ /\\"; L2="( ${E} ${E} )"; L3="( .. )"; L4="\`------'~" ;;
207
- esac ;;
208
- *)
209
- L1="(${E}${E})"; L2="( )"; L3=""; L4="" ;;
210
- esac
211
-
212
- # ─── Blink: replace eyes with "-" ────────────────────────────────────────────
213
- if [ "$BLINK" -eq 1 ]; then
214
- L1="${L1//${E}/-}"
215
- L2="${L2//${E}/-}"
216
- L3="${L3//${E}/-}"
217
- L4="${L4//${E}/-}"
218
- fi
219
-
220
- # ─── Hat ──────────────────────────────────────────────────────────────────────
221
- HAT_LINE=""
222
- case "$HAT" in
223
- crown) HAT_LINE=" \\^^^/" ;;
224
- tophat) HAT_LINE=" [___]" ;;
225
- propeller) HAT_LINE=" -+-" ;;
226
- halo) HAT_LINE=" ( )" ;;
227
- wizard) HAT_LINE=" /^\\" ;;
228
- beanie) HAT_LINE=" (___)" ;;
229
- tinyduck) HAT_LINE=" ,>" ;;
230
- esac
231
-
232
- # ─── Reaction bubble (with TTL check) ────────────────────────────────────────
233
- BUBBLE=""
234
- if [ -n "$ACHIEVEMENT" ] && [ "$ACHIEVEMENT" != "null" ] && [ "$ACHIEVEMENT" != "" ]; then
235
- BUBBLE=$'\xf0\x9f\x8f\x86'" $ACHIEVEMENT"
236
- fi
237
- REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
238
- REACTION_TTL=0
239
- CONFIG_FILE="$BUDDY_STATE_DIR/config.json"
240
- if [ -f "$CONFIG_FILE" ]; then
241
- _ttl=$(jq -r '.reactionTTL // 0' "$CONFIG_FILE" 2>/dev/null || echo 0)
242
- case "$_ttl" in ''|*[!0-9]*) ;; *) REACTION_TTL="$_ttl" ;; esac
243
- fi
244
- if [ -n "$REACTION" ] && [ "$REACTION" != "null" ] && [ "$REACTION" != "" ]; then
245
- FRESH=0
246
- if [ "$REACTION_TTL" -eq 0 ]; then
247
- FRESH=1
248
- elif [ -f "$REACTION_FILE" ]; then
249
- TS=$(jq -r '.timestamp // 0' "$REACTION_FILE" 2>/dev/null || echo 0)
250
- if [ "$TS" != "0" ]; then
251
- NOW=$(date +%s)
252
- AGE=$(( NOW - TS / 1000 ))
253
- [ "$AGE" -lt "$REACTION_TTL" ] && FRESH=1
254
- fi
255
- fi
256
- if [ "$FRESH" -eq 1 ]; then
257
- if [ -n "$BUBBLE" ]; then
258
- BUBBLE="$BUBBLE | \"${REACTION}\""
259
- else
260
- BUBBLE="\"${REACTION}\""
261
- fi
262
- fi
263
- fi
264
-
265
- # ─── Build art lines ─────────────────────────────────────────────────────────
266
- ART_LINES=("$L1" "$L2" "$L3")
267
- [ -n "$L4" ] && ART_LINES+=("$L4")
268
-
269
- # Center the name
270
- NAME_LEN=${#NAME}
271
- ART_CENTER=4
272
- NAME_PAD=$(( ART_CENTER - NAME_LEN / 2 ))
273
- [ "$NAME_PAD" -lt 0 ] && NAME_PAD=0
274
- NAME_LINE="$(printf '%*s%s' "$NAME_PAD" '' "$NAME")"
275
-
276
- # ─── Build all art lines ──────────────────────────────────────────────────────
277
- DIM=$'\033[2;3m'
278
-
279
- ALL_LINES=()
280
- ALL_COLORS=()
281
- [ -n "$HAT_LINE" ] && { ALL_LINES+=("$HAT_LINE"); ALL_COLORS+=("$C"); }
282
- for line in "${ART_LINES[@]}"; do
283
- ALL_LINES+=("$line"); ALL_COLORS+=("$C")
284
- done
285
- ALL_LINES+=("$NAME_LINE"); ALL_COLORS+=("$DIM")
286
-
287
- ART_W=14
288
- ART_COUNT=${#ALL_LINES[@]}
289
-
290
- # ─── Speech bubble (left of art, word-wrapped) ──────────────────────────────
291
- # Strip the quotes we added earlier
292
- BUBBLE_TEXT=""
293
- if [ -n "$BUBBLE" ]; then
294
- BUBBLE_TEXT="${BUBBLE%\"}"
295
- BUBBLE_TEXT="${BUBBLE_TEXT#\"}"
296
- fi
297
-
298
- # ─── Word-wrap bubble text ────────────────────────────────────────────────────
299
- INNER_W=28
300
- TEXT_LINES=()
301
- if [ -n "$BUBBLE_TEXT" ]; then
302
- WORDS=($BUBBLE_TEXT)
303
- CUR_LINE=""
304
- for word in "${WORDS[@]}"; do
305
- if [ -z "$CUR_LINE" ]; then
306
- CUR_LINE="$word"
307
- elif [ $(( ${#CUR_LINE} + 1 + ${#word} )) -le $INNER_W ]; then
308
- CUR_LINE="$CUR_LINE $word"
309
- else
310
- TEXT_LINES+=("$CUR_LINE")
311
- CUR_LINE="$word"
312
- fi
313
- done
314
- [ -n "$CUR_LINE" ] && TEXT_LINES+=("$CUR_LINE")
315
- fi
316
-
317
- TEXT_COUNT=${#TEXT_LINES[@]}
318
-
319
- # Build box as plain strings (no ANSI). Color applied at output time.
320
- # Box display width = INNER_W + 4: "| " + text(INNER_W) + " |"
321
- BOX_W=$(( INNER_W + 4 ))
322
- BUBBLE_LINES=()
323
- BUBBLE_TYPES=() # "border" or "text" — determines coloring
324
- if [ $TEXT_COUNT -gt 0 ]; then
325
- # Top border
326
- BORDER=$(printf '%*s' "$(( BOX_W - 2 ))" '' | tr ' ' '-')
327
- BUBBLE_LINES+=(".${BORDER}.")
328
- BUBBLE_TYPES+=("border")
329
- # Text rows: "| text padded |"
330
- for tl in "${TEXT_LINES[@]}"; do
331
- tpad=$(( INNER_W - ${#tl} ))
332
- [ "$tpad" -lt 0 ] && tpad=0
333
- padding=$(printf '%*s' "$tpad" '')
334
- BUBBLE_LINES+=("| ${tl}${padding} |")
335
- BUBBLE_TYPES+=("text")
336
- done
337
- # Bottom border
338
- BUBBLE_LINES+=("\`${BORDER}'")
339
- BUBBLE_TYPES+=("border")
340
- fi
341
-
342
- BUBBLE_COUNT=${#BUBBLE_LINES[@]}
343
-
344
- # ─── Right-align with bubble box to the left ─────────────────────────────────
345
- GAP=2
346
- if [ $BUBBLE_COUNT -gt 0 ]; then
347
- TOTAL_W=$(( BOX_W + GAP + ART_W ))
348
- else
349
- TOTAL_W=$ART_W
350
- fi
351
- MARGIN=8
352
- PAD=$(( COLS - TOTAL_W - MARGIN ))
353
- [ "$PAD" -lt 0 ] && PAD=0
354
-
355
- SPACER=$(printf "${B}%${PAD}s" "")
356
- GAP_STR=$(printf '%*s' "$GAP" '')
357
-
358
- # Vertically center bubble box on the art
359
- BUBBLE_START=0
360
- if [ $BUBBLE_COUNT -gt 0 ] && [ $BUBBLE_COUNT -lt $ART_COUNT ]; then
361
- BUBBLE_START=$(( (ART_COUNT - BUBBLE_COUNT) / 2 ))
362
- fi
363
-
364
- # ─── Find the connector line (middle text line → points to buddy's mouth) ─────
365
- # The connector goes on the middle text row of the bubble
366
- CONNECTOR_BI=-1
367
- if [ $BUBBLE_COUNT -gt 2 ]; then
368
- # text rows are indices 1..(BUBBLE_COUNT-2), pick the middle one
369
- FIRST_TEXT=1
370
- LAST_TEXT=$(( BUBBLE_COUNT - 2 ))
371
- CONNECTOR_BI=$(( (FIRST_TEXT + LAST_TEXT) / 2 ))
372
- fi
373
-
374
- # ─── Output: merged bubble box + connector + art per line ─────────────────────
375
- for (( i=0; i<ART_COUNT; i++ )); do
376
- art_part="${ALL_COLORS[$i]}${ALL_LINES[$i]}${NC}"
377
-
378
- if [ $BUBBLE_COUNT -gt 0 ]; then
379
- bi=$(( i - BUBBLE_START ))
380
- if [ $bi -ge 0 ] && [ $bi -lt $BUBBLE_COUNT ]; then
381
- bline="${BUBBLE_LINES[$bi]}"
382
- btype="${BUBBLE_TYPES[$bi]}"
383
-
384
- # Connector: "-- " on the middle text line, spaces otherwise
385
- if [ $bi -eq $CONNECTOR_BI ]; then
386
- gap="${C}--${NC} "
387
- else
388
- gap=" "
389
- fi
390
-
391
- if [ "$btype" = "border" ]; then
392
- echo "${SPACER}${C}${bline}${NC}${gap}${art_part}"
393
- else
394
- pipe_l="${bline:0:1}"
395
- pipe_r="${bline: -1}"
396
- inner="${bline:1:$(( ${#bline} - 2 ))}"
397
- echo "${SPACER}${C}${pipe_l}${NC}${DIM}${inner}${NC}${C}${pipe_r}${NC}${gap}${art_part}"
398
- fi
399
- else
400
- empty=$(printf '%*s' "$BOX_W" '')
401
- echo "${SPACER}${empty} ${art_part}"
402
- fi
403
- else
404
- echo "${SPACER}${art_part}"
405
- fi
406
- done
407
-
408
- exit 0