@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.
- package/extensions/pompom-extension.ts +88 -71
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
function showCompanion() {
|
|
43
|
+
if (companionActive) return;
|
|
44
|
+
if (!ctx?.hasUI) return;
|
|
45
|
+
companionActive = true;
|
|
46
|
+
lastRenderTime = Date.now();
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
const setWidget = () => {
|
|
44
49
|
if (!ctx?.hasUI) return;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
60
|
-
ctx.ui.setWidget(
|
|
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
|
-
//
|
|
65
|
-
|
|
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",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
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
|
-
|
|
86
|
-
|
|
96
|
+
try {
|
|
97
|
+
terminalInputUnsub = ctx.ui.onTerminalInput((data: string) => {
|
|
98
|
+
if (!enabled || !companionActive) return undefined;
|
|
87
99
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
249
|
+
// No args: toggle. Unknown: error.
|
|
233
250
|
if (sub === "") {
|
|
234
251
|
if (companionActive) {
|
|
235
252
|
enabled = false;
|