@codexstar/pi-pompom 1.0.0 → 1.1.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.
@@ -2,9 +2,7 @@
2
2
  * pi-pompom — Pompom Companion Extension for Pi CLI.
3
3
  *
4
4
  * A 3D raymarched virtual pet that lives above the editor.
5
- * Interactive commands: pet, feed, ball, music, color, sleep, wake, flip, hide.
6
- *
7
- * Install as a standalone extension — no dependencies on pi-voice.
5
+ * Hardened against conflicts with other extensions.
8
6
  */
9
7
 
10
8
  import type {
@@ -14,6 +12,9 @@ import type {
14
12
 
15
13
  import { renderPompom, resetPompom, pompomSetTalking, pompomKeypress, pompomStatus } from "./pompom";
16
14
 
15
+ // Namespaced widget ID — prevents collision with any other extension
16
+ const WIDGET_ID = "codexstar-pompom-companion";
17
+
17
18
  export default function (pi: ExtensionAPI) {
18
19
  let ctx: ExtensionContext | null = null;
19
20
  let companionTimer: ReturnType<typeof setInterval> | null = null;
@@ -22,31 +23,44 @@ export default function (pi: ExtensionAPI) {
22
23
  let terminalInputUnsub: (() => void) | null = null;
23
24
  let enabled = true;
24
25
 
25
- function showCompanion() {
26
- if (companionActive) return;
27
- if (!ctx?.hasUI) return;
28
- companionActive = true;
29
- lastRenderTime = Date.now();
26
+ // ─── Safe render wrapper — never lets an error crash the TUI ─────────
30
27
 
31
- const renderCompanion = (width: number): string[] => {
28
+ function safeRender(width: number): string[] {
29
+ try {
32
30
  const now = Date.now();
33
31
  const dt = Math.min(0.1, (now - lastRenderTime) / 1000);
34
32
  lastRenderTime = now;
35
33
  return renderPompom(Math.max(40, width), 0, dt);
36
- };
34
+ } catch {
35
+ // If rendering fails, return a minimal placeholder so the TUI doesn't crash
36
+ return [" ".repeat(Math.max(1, width))];
37
+ }
38
+ }
39
+
40
+ // ─── Widget management ──────────────────────────────────────────────
37
41
 
38
- ctx.ui.setWidget("pompom-companion", (_tui, _theme) => ({
39
- invalidate() {},
40
- render: renderCompanion,
41
- }), { placement: "aboveEditor" });
42
+ function showCompanion() {
43
+ if (companionActive) return;
44
+ if (!ctx?.hasUI) return;
45
+ companionActive = true;
46
+ lastRenderTime = Date.now();
42
47
 
43
- companionTimer = setInterval(() => {
48
+ const setWidget = () => {
44
49
  if (!ctx?.hasUI) return;
45
- ctx.ui.setWidget("pompom-companion", (_tui, _theme) => ({
46
- invalidate() {},
47
- render: renderCompanion,
48
- }), { placement: "aboveEditor" });
49
- }, 150);
50
+ try {
51
+ ctx.ui.setWidget(WIDGET_ID, (_tui, _theme) => ({
52
+ invalidate() {},
53
+ render: safeRender,
54
+ }), { placement: "aboveEditor" });
55
+ } catch {
56
+ // Widget slot may be unavailable — don't crash
57
+ }
58
+ };
59
+
60
+ setWidget();
61
+ // Re-set widget on interval for animation. Defensive: clear any stale timer first.
62
+ if (companionTimer) clearInterval(companionTimer);
63
+ companionTimer = setInterval(setWidget, 150);
50
64
  }
51
65
 
52
66
  function hideCompanion() {
@@ -56,74 +70,77 @@ export default function (pi: ExtensionAPI) {
56
70
  companionTimer = null;
57
71
  }
58
72
  pompomSetTalking(false);
59
- if (ctx?.hasUI) {
60
- ctx.ui.setWidget("pompom-companion", undefined);
73
+ try {
74
+ if (ctx?.hasUI) ctx.ui.setWidget(WIDGET_ID, undefined);
75
+ } catch {
76
+ // Ignore — widget may already be gone
61
77
  }
62
78
  }
63
79
 
64
- // macOS Option+key produces Unicode characters when macos-option-as-alt is off.
65
- // Windows/Linux don't use this — they send ESC prefix for Alt+key (handled below).
80
+ // ─── Keyboard input ─────────────────────────────────────────────────
81
+
82
+ // macOS Option+key Unicode map (only fires when macos-option-as-alt is off)
66
83
  const optionUnicodeMap: Record<string, string> = {
67
- "π": "p", // Option+p Pet
68
- "ƒ": "f", // Option+f Feed
69
- "": "b", // Option+b Ball
70
- "µ": "m", // Option+m → Music
71
- "ç": "c", // Option+c → Color
72
- "∂": "d", // Option+d → Flip
73
- "ß": "s", // Option+s → Sleep
74
- "∑": "w", // Option+w → Wake
75
- "ø": "o", // Option+o → Hide
76
- "≈": "x", // Option+x → Dance
77
- "†": "t", // Option+t → Treat
78
- "˙": "h", // Option+h → Hug
84
+ "π": "p", "ƒ": "f", "∫": "b", "µ": "m", "ç": "c",
85
+ "": "d", "ß": "s", "∑": "w", "ø": "o",
86
+ "": "x", "†": "t", "˙": "h",
79
87
  };
80
88
 
89
+ const POMPOM_KEYS = "pfbmcdswoxth";
90
+
81
91
  function setupKeyHandler() {
82
92
  if (!ctx?.hasUI) return;
93
+ // Always clean up previous handler first — prevents double-binding
83
94
  if (terminalInputUnsub) { terminalInputUnsub(); terminalInputUnsub = null; }
84
95
 
85
- terminalInputUnsub = ctx.ui.onTerminalInput((data: string) => {
86
- if (!enabled || !companionActive) return undefined;
96
+ try {
97
+ terminalInputUnsub = ctx.ui.onTerminalInput((data: string) => {
98
+ if (!enabled || !companionActive) return undefined;
87
99
 
88
- // 1. Ghostty keybind prefix \x1d + letter (macOS with Ghostty config)
89
- if (data.length === 2 && data[0] === "\x1d" && "pfbmcdswoxth".includes(data[1])) {
90
- pompomKeypress(data[1]);
91
- return { consume: true };
92
- }
93
-
94
- // 2. ESC prefix — Alt+key on Windows/Linux, Option-as-Meta on macOS
95
- // This is the primary method on Windows Terminal, CMD, PowerShell, WSL
96
- if (data.length === 2 && data[0] === "\x1b" && "pfbmcdswoxth".includes(data[1])) {
97
- pompomKeypress(data[1]);
98
- return { consume: true };
99
- }
100
+ try {
101
+ // 1. Ghostty keybind prefix \x1d + letter
102
+ if (data.length === 2 && data[0] === "\x1d" && POMPOM_KEYS.includes(data[1])) {
103
+ pompomKeypress(data[1]);
104
+ return { consume: true };
105
+ }
100
106
 
101
- // 3. macOS Unicode chars (Option key without macos-option-as-alt)
102
- const mapped = optionUnicodeMap[data];
103
- if (mapped) {
104
- pompomKeypress(mapped);
105
- return { consume: true };
106
- }
107
+ // 2. ESC prefix Alt+key on Windows/Linux, Option-as-Meta on macOS
108
+ if (data.length === 2 && data[0] === "\x1b" && POMPOM_KEYS.includes(data[1])) {
109
+ pompomKeypress(data[1]);
110
+ return { consume: true };
111
+ }
107
112
 
108
- // 4. Kitty keyboard protocol (Ghostty, Kitty, WezTerm, Windows Terminal 1.22+)
109
- // Format: \x1b[<codepoint>;{1+modifier}u — Alt modifier bit = 2
110
- const kittyMatch = data.match(/^\x1b\[(\d+);(\d+)u$/);
111
- if (kittyMatch) {
112
- const mod = parseInt(kittyMatch[2]);
113
- if ((mod - 1) & 2) {
114
- const char = String.fromCharCode(parseInt(kittyMatch[1]));
115
- if ("pfbmcdswoxth".includes(char)) {
116
- pompomKeypress(char);
113
+ // 3. macOS Unicode chars
114
+ const mapped = optionUnicodeMap[data];
115
+ if (mapped) {
116
+ pompomKeypress(mapped);
117
117
  return { consume: true };
118
118
  }
119
+
120
+ // 4. Kitty keyboard protocol
121
+ const kittyMatch = data.match(/^\x1b\[(\d+);(\d+)u$/);
122
+ if (kittyMatch) {
123
+ const mod = parseInt(kittyMatch[2]);
124
+ if ((mod - 1) & 2) {
125
+ const char = String.fromCharCode(parseInt(kittyMatch[1]));
126
+ if (POMPOM_KEYS.includes(char)) {
127
+ pompomKeypress(char);
128
+ return { consume: true };
129
+ }
130
+ }
131
+ }
132
+ } catch {
133
+ // Never let a key handler error propagate to the TUI
119
134
  }
120
- }
121
135
 
122
- return undefined;
123
- });
136
+ return undefined;
137
+ });
138
+ } catch {
139
+ // onTerminalInput may not be available — gracefully degrade (commands still work)
140
+ }
124
141
  }
125
142
 
126
- // ─── Lifecycle ───────────────────────────────────────────────────────────
143
+ // ─── Lifecycle — defensive against load-order issues ────────────────
127
144
 
128
145
  pi.on("session_start", async (_event, startCtx) => {
129
146
  ctx = startCtx;
@@ -149,7 +166,7 @@ export default function (pi: ExtensionAPI) {
149
166
  }
150
167
  });
151
168
 
152
- // ─── /pompom command ─────────────────────────────────────────────────────
169
+ // ─── /pompom command ────────────────────────────────────────────────
153
170
 
154
171
  const pompomCommands: Record<string, string> = {
155
172
  pet: "p", feed: "f", ball: "b", music: "m", color: "c", theme: "c",
@@ -229,7 +246,7 @@ export default function (pi: ExtensionAPI) {
229
246
  return;
230
247
  }
231
248
 
232
- // No args or unknown: toggle if active, else show help
249
+ // No args: toggle. Unknown: error.
233
250
  if (sub === "") {
234
251
  if (companionActive) {
235
252
  enabled = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexstar/pi-pompom",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Pompom — a 3D raymarched virtual pet companion for Pi CLI",
5
5
  "type": "module",
6
6
  "keywords": [