giggles 0.4.0 → 0.5.1
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 +2 -2
- package/dist/{chunk-R5I4YOOP.js → chunk-C77VBSPK.js} +1 -0
- package/dist/chunk-LXNKSYJT.js +540 -0
- package/dist/{chunk-DFB7V4OK.js → chunk-SKSDNDQF.js} +2 -2
- package/dist/chunk-WNGBTD67.js +25 -0
- package/dist/index.d.ts +48 -34
- package/dist/index.js +69 -45
- package/dist/markdown/index.js +2 -2
- package/dist/terminal/index.d.ts +1 -9
- package/dist/terminal/index.js +1 -3
- package/dist/ui/index.d.ts +18 -18
- package/dist/ui/index.js +67 -36
- package/package.json +5 -4
- package/dist/chunk-ET2WSMEF.js +0 -520
- package/dist/chunk-N2MMNJV3.js +0 -52
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ inspired by the [charmbracelet](https://github.com/charmbracelet) ecosystem, it
|
|
|
12
12
|
|
|
13
13
|
## features
|
|
14
14
|
|
|
15
|
-
-
|
|
15
|
+
- each component owns its keys — a text input inside a list inside a panel all work independently, with unhandled keys naturally passing up to the right parent. no global input handler, no coordination code
|
|
16
16
|
- navigate between views with a simple API; the previously focused component is restored when you return
|
|
17
|
-
- a full set of hooks and components — `
|
|
17
|
+
- a full set of hooks and components — `useFocusScope`, `useFocusNode`, `FocusTrap`, `useNavigation`, and more — for building any interaction pattern without reimplementing the plumbing
|
|
18
18
|
- built-in keybinding registry so your app can always show users what keys do what, in the current context — context-aware and accessible via a hook
|
|
19
19
|
- a component library covering most TUI use cases, from text inputs and autocomplete to virtual lists for large datasets — with sensible defaults and render props for full customization
|
|
20
20
|
- render markdown in the terminal, with full formatting and syntax-highlighted code block and diff support
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
// src/core/GigglesError.ts
|
|
2
|
+
var GigglesError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(`[giggles] ${message}`);
|
|
5
|
+
this.name = "GigglesError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/core/input/useKeybindings.tsx
|
|
10
|
+
import { useEffect, useId, useRef } from "react";
|
|
11
|
+
|
|
12
|
+
// src/core/focus/StoreContext.ts
|
|
13
|
+
import { createContext, useContext } from "react";
|
|
14
|
+
var StoreContext = createContext(null);
|
|
15
|
+
var ScopeIdContext = createContext(null);
|
|
16
|
+
function useStore() {
|
|
17
|
+
const store = useContext(StoreContext);
|
|
18
|
+
if (!store) {
|
|
19
|
+
throw new GigglesError("useStore must be used within a GigglesProvider");
|
|
20
|
+
}
|
|
21
|
+
return store;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/core/input/useKeybindings.tsx
|
|
25
|
+
function useKeybindings(focus, bindings, options) {
|
|
26
|
+
const store = useStore();
|
|
27
|
+
const registrationId = useId();
|
|
28
|
+
const nodeIdRef = useRef(focus.id);
|
|
29
|
+
nodeIdRef.current = focus.id;
|
|
30
|
+
store.registerKeybindings(nodeIdRef.current, registrationId, bindings, options);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
return () => {
|
|
33
|
+
store.unregisterKeybindings(nodeIdRef.current, registrationId);
|
|
34
|
+
};
|
|
35
|
+
}, [registrationId, store]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/core/input/useKeybindingRegistry.ts
|
|
39
|
+
function useKeybindingRegistry(focus) {
|
|
40
|
+
const store = useStore();
|
|
41
|
+
const all = store.getAllBindings().filter((b) => b.name != null);
|
|
42
|
+
const branchPath = store.getActiveBranchPath();
|
|
43
|
+
const branchSet = new Set(branchPath);
|
|
44
|
+
const trapNodeId = store.getTrapNodeId();
|
|
45
|
+
const withinTrapSet = (() => {
|
|
46
|
+
if (!trapNodeId) return null;
|
|
47
|
+
const trapIndex = branchPath.indexOf(trapNodeId);
|
|
48
|
+
return trapIndex >= 0 ? new Set(branchPath.slice(0, trapIndex + 1)) : null;
|
|
49
|
+
})();
|
|
50
|
+
const available = all.filter((b) => {
|
|
51
|
+
if (b.when === "mounted") return withinTrapSet ? withinTrapSet.has(b.nodeId) : true;
|
|
52
|
+
return (withinTrapSet ?? branchSet).has(b.nodeId);
|
|
53
|
+
});
|
|
54
|
+
const local = focus ? all.filter((b) => b.nodeId === focus.id) : [];
|
|
55
|
+
return { all, available, local };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/core/focus/useFocusNode.ts
|
|
59
|
+
import { useContext as useContext2, useEffect as useEffect2, useId as useId2, useMemo, useSyncExternalStore } from "react";
|
|
60
|
+
function useFocusNode(options) {
|
|
61
|
+
var _a;
|
|
62
|
+
const id = useId2();
|
|
63
|
+
const store = useStore();
|
|
64
|
+
const contextParentId = useContext2(ScopeIdContext);
|
|
65
|
+
const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
|
|
66
|
+
const subscribe = useMemo(() => store.subscribe.bind(store), [store]);
|
|
67
|
+
useEffect2(() => {
|
|
68
|
+
store.registerNode(id, parentId);
|
|
69
|
+
return () => {
|
|
70
|
+
store.unregisterNode(id);
|
|
71
|
+
};
|
|
72
|
+
}, [id, parentId, store]);
|
|
73
|
+
const hasFocus = useSyncExternalStore(subscribe, () => store.isFocused(id));
|
|
74
|
+
return { id, hasFocus };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/core/input/FocusTrap.tsx
|
|
78
|
+
import { useEffect as useEffect3, useRef as useRef2 } from "react";
|
|
79
|
+
import { jsx } from "react/jsx-runtime";
|
|
80
|
+
function FocusTrap({ children }) {
|
|
81
|
+
const { id } = useFocusNode();
|
|
82
|
+
const store = useStore();
|
|
83
|
+
const previousFocusRef = useRef2(store.getFocusedId());
|
|
84
|
+
useEffect3(() => {
|
|
85
|
+
const previousFocus = previousFocusRef.current;
|
|
86
|
+
store.setTrap(id);
|
|
87
|
+
store.focusFirstChild(id);
|
|
88
|
+
return () => {
|
|
89
|
+
store.clearTrap(id);
|
|
90
|
+
if (previousFocus) {
|
|
91
|
+
store.focusNode(previousFocus);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}, [id]);
|
|
95
|
+
return /* @__PURE__ */ jsx(ScopeIdContext.Provider, { value: id, children });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/core/input/InputRouter.tsx
|
|
99
|
+
import { useInput } from "ink";
|
|
100
|
+
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
101
|
+
function InputRouter({ children }) {
|
|
102
|
+
const store = useStore();
|
|
103
|
+
useInput((input, key) => {
|
|
104
|
+
store.dispatch(input, key);
|
|
105
|
+
});
|
|
106
|
+
return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/core/focus/useFocusScope.ts
|
|
110
|
+
import { useCallback, useContext as useContext3, useEffect as useEffect4, useId as useId3, useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
111
|
+
function useFocusScope(options) {
|
|
112
|
+
var _a;
|
|
113
|
+
const id = useId3();
|
|
114
|
+
const keybindingRegistrationId = useId3();
|
|
115
|
+
const store = useStore();
|
|
116
|
+
const contextParentId = useContext3(ScopeIdContext);
|
|
117
|
+
const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
|
|
118
|
+
const subscribe = useMemo2(() => store.subscribe.bind(store), [store]);
|
|
119
|
+
useEffect4(() => {
|
|
120
|
+
store.registerNode(id, parentId);
|
|
121
|
+
return () => {
|
|
122
|
+
store.unregisterNode(id);
|
|
123
|
+
};
|
|
124
|
+
}, [id, parentId, store]);
|
|
125
|
+
const hasFocus = useSyncExternalStore2(subscribe, () => store.isFocused(id));
|
|
126
|
+
const isPassive = useSyncExternalStore2(subscribe, () => store.isPassive(id));
|
|
127
|
+
const next = useCallback(() => store.navigateSibling("next", true, id), [store, id]);
|
|
128
|
+
const prev = useCallback(() => store.navigateSibling("prev", true, id), [store, id]);
|
|
129
|
+
const nextShallow = useCallback(() => store.navigateSibling("next", true, id, true), [store, id]);
|
|
130
|
+
const prevShallow = useCallback(() => store.navigateSibling("prev", true, id, true), [store, id]);
|
|
131
|
+
const escape = useCallback(() => store.makePassive(id), [store, id]);
|
|
132
|
+
const drillIn = useCallback(() => store.focusFirstChild(id), [store, id]);
|
|
133
|
+
const resolvedBindings = typeof (options == null ? void 0 : options.keybindings) === "function" ? options.keybindings({ next, prev, nextShallow, prevShallow, escape, drillIn }) : (options == null ? void 0 : options.keybindings) ?? {};
|
|
134
|
+
store.registerKeybindings(id, keybindingRegistrationId, resolvedBindings);
|
|
135
|
+
useEffect4(() => {
|
|
136
|
+
return () => {
|
|
137
|
+
store.unregisterKeybindings(id, keybindingRegistrationId);
|
|
138
|
+
};
|
|
139
|
+
}, [id, keybindingRegistrationId, store]);
|
|
140
|
+
return { id, hasFocus, isPassive };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/core/focus/FocusScope.tsx
|
|
144
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
145
|
+
function FocusScope({ handle, children }) {
|
|
146
|
+
return /* @__PURE__ */ jsx3(ScopeIdContext.Provider, { value: handle.id, children });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/core/input/normalizeKey.ts
|
|
150
|
+
function normalizeKey(input, key) {
|
|
151
|
+
if (input === "\x1B[I" || input === "\x1B[O") return "";
|
|
152
|
+
if (key.downArrow) return "down";
|
|
153
|
+
if (key.upArrow) return "up";
|
|
154
|
+
if (key.leftArrow) return "left";
|
|
155
|
+
if (key.rightArrow) return "right";
|
|
156
|
+
if (key.return) return "enter";
|
|
157
|
+
if (key.escape) return "escape";
|
|
158
|
+
if (key.tab && key.shift) return "shift+tab";
|
|
159
|
+
if (key.tab) return "tab";
|
|
160
|
+
if (input === "\x1B[3~") return "delete";
|
|
161
|
+
if (key.backspace || key.delete) return "backspace";
|
|
162
|
+
if (key.pageUp) return "pageup";
|
|
163
|
+
if (key.pageDown) return "pagedown";
|
|
164
|
+
if (key.home) return "home";
|
|
165
|
+
if (key.end) return "end";
|
|
166
|
+
if (key.ctrl && input.length === 1) {
|
|
167
|
+
return `ctrl+${input}`;
|
|
168
|
+
}
|
|
169
|
+
return input;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/core/focus/FocusStore.ts
|
|
173
|
+
var FocusStore = class {
|
|
174
|
+
nodes = /* @__PURE__ */ new Map();
|
|
175
|
+
// Persistent parent record — never deleted from, used for ancestor-walk during unregistration
|
|
176
|
+
parentMap = /* @__PURE__ */ new Map();
|
|
177
|
+
focusedId = null;
|
|
178
|
+
passiveSet = /* @__PURE__ */ new Set();
|
|
179
|
+
pendingFocusFirstChild = /* @__PURE__ */ new Set();
|
|
180
|
+
trapNodeId = null;
|
|
181
|
+
listeners = /* @__PURE__ */ new Set();
|
|
182
|
+
// nodeId → registrationId → BindingRegistration
|
|
183
|
+
// Keybindings register synchronously during render; nodes register in useEffect.
|
|
184
|
+
// A keybinding may exist for a node that has not yet appeared in the node tree —
|
|
185
|
+
// this is safe because dispatch only walks nodes in the active branch path.
|
|
186
|
+
keybindings = /* @__PURE__ */ new Map();
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Subscription
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
subscribe(listener) {
|
|
191
|
+
this.listeners.add(listener);
|
|
192
|
+
return () => this.listeners.delete(listener);
|
|
193
|
+
}
|
|
194
|
+
notify() {
|
|
195
|
+
for (const listener of this.listeners) {
|
|
196
|
+
listener();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Registration
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
registerNode(id, parentId) {
|
|
203
|
+
const node = { id, parentId, childrenIds: [] };
|
|
204
|
+
this.nodes.set(id, node);
|
|
205
|
+
this.parentMap.set(id, parentId);
|
|
206
|
+
if (parentId) {
|
|
207
|
+
const parent = this.nodes.get(parentId);
|
|
208
|
+
if (parent && !parent.childrenIds.includes(id)) {
|
|
209
|
+
const wasEmpty = parent.childrenIds.length === 0;
|
|
210
|
+
parent.childrenIds.push(id);
|
|
211
|
+
if (wasEmpty && this.pendingFocusFirstChild.has(parentId)) {
|
|
212
|
+
this.pendingFocusFirstChild.delete(parentId);
|
|
213
|
+
this.focusNode(id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const [existingId, existingNode] of this.nodes) {
|
|
218
|
+
if (existingNode.parentId === id && !node.childrenIds.includes(existingId)) {
|
|
219
|
+
node.childrenIds.push(existingId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (this.nodes.size === 1) {
|
|
223
|
+
this.focusNode(id);
|
|
224
|
+
}
|
|
225
|
+
this.notify();
|
|
226
|
+
}
|
|
227
|
+
unregisterNode(id) {
|
|
228
|
+
const node = this.nodes.get(id);
|
|
229
|
+
if (!node) return;
|
|
230
|
+
if (node.parentId) {
|
|
231
|
+
const parent = this.nodes.get(node.parentId);
|
|
232
|
+
if (parent) {
|
|
233
|
+
parent.childrenIds = parent.childrenIds.filter((c) => c !== id);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.nodes.delete(id);
|
|
237
|
+
this.passiveSet.delete(id);
|
|
238
|
+
this.pendingFocusFirstChild.delete(id);
|
|
239
|
+
if (this.focusedId === id) {
|
|
240
|
+
let candidate = node.parentId;
|
|
241
|
+
while (candidate !== null) {
|
|
242
|
+
if (this.nodes.has(candidate)) {
|
|
243
|
+
this.focusNode(candidate);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
candidate = this.parentMap.get(candidate) ?? null;
|
|
247
|
+
}
|
|
248
|
+
this.focusedId = null;
|
|
249
|
+
}
|
|
250
|
+
this.notify();
|
|
251
|
+
}
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Focus
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
focusNode(id) {
|
|
256
|
+
if (!this.nodes.has(id)) return;
|
|
257
|
+
const oldFocusedId = this.focusedId;
|
|
258
|
+
if (oldFocusedId === id) return;
|
|
259
|
+
this.focusedId = id;
|
|
260
|
+
for (const passiveId of this.passiveSet) {
|
|
261
|
+
const wasAncestor = this.isAncestorOf(passiveId, oldFocusedId);
|
|
262
|
+
const isAncestor = this.isAncestorOf(passiveId, id);
|
|
263
|
+
if (wasAncestor && !isAncestor) {
|
|
264
|
+
this.passiveSet.delete(passiveId);
|
|
265
|
+
}
|
|
266
|
+
if (isAncestor && id !== passiveId) {
|
|
267
|
+
this.passiveSet.delete(passiveId);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this.notify();
|
|
271
|
+
}
|
|
272
|
+
focusFirstChild(parentId) {
|
|
273
|
+
const parent = this.nodes.get(parentId);
|
|
274
|
+
if (parent && parent.childrenIds.length > 0) {
|
|
275
|
+
let target = parent.childrenIds[0];
|
|
276
|
+
let targetNode = this.nodes.get(target);
|
|
277
|
+
while (targetNode && targetNode.childrenIds.length > 0) {
|
|
278
|
+
target = targetNode.childrenIds[0];
|
|
279
|
+
targetNode = this.nodes.get(target);
|
|
280
|
+
}
|
|
281
|
+
this.focusNode(target);
|
|
282
|
+
} else {
|
|
283
|
+
this.pendingFocusFirstChild.add(parentId);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Navigation
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
navigateSibling(direction, wrap = true, groupId, shallow = false) {
|
|
290
|
+
var _a;
|
|
291
|
+
if (!this.focusedId) return;
|
|
292
|
+
let currentChildId;
|
|
293
|
+
let siblings;
|
|
294
|
+
if (groupId) {
|
|
295
|
+
const group = this.nodes.get(groupId);
|
|
296
|
+
if (!group || group.childrenIds.length === 0) return;
|
|
297
|
+
siblings = group.childrenIds;
|
|
298
|
+
let cursor = this.focusedId;
|
|
299
|
+
while (cursor) {
|
|
300
|
+
const node = this.nodes.get(cursor);
|
|
301
|
+
if ((node == null ? void 0 : node.parentId) === groupId) {
|
|
302
|
+
currentChildId = cursor;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
cursor = (node == null ? void 0 : node.parentId) ?? null;
|
|
306
|
+
}
|
|
307
|
+
if (!currentChildId) {
|
|
308
|
+
const targetId2 = direction === "next" ? siblings[0] : siblings[siblings.length - 1];
|
|
309
|
+
if (!shallow && ((_a = this.nodes.get(targetId2)) == null ? void 0 : _a.childrenIds.length)) {
|
|
310
|
+
this.focusFirstChild(targetId2);
|
|
311
|
+
} else {
|
|
312
|
+
this.focusNode(targetId2);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
const currentNode = this.nodes.get(this.focusedId);
|
|
318
|
+
if (!(currentNode == null ? void 0 : currentNode.parentId)) return;
|
|
319
|
+
const parent = this.nodes.get(currentNode.parentId);
|
|
320
|
+
if (!parent || parent.childrenIds.length === 0) return;
|
|
321
|
+
siblings = parent.childrenIds;
|
|
322
|
+
currentChildId = this.focusedId;
|
|
323
|
+
}
|
|
324
|
+
const currentIndex = siblings.indexOf(currentChildId);
|
|
325
|
+
if (currentIndex === -1) return;
|
|
326
|
+
let nextIndex;
|
|
327
|
+
if (wrap) {
|
|
328
|
+
nextIndex = direction === "next" ? (currentIndex + 1) % siblings.length : (currentIndex - 1 + siblings.length) % siblings.length;
|
|
329
|
+
} else {
|
|
330
|
+
nextIndex = direction === "next" ? Math.min(currentIndex + 1, siblings.length - 1) : Math.max(currentIndex - 1, 0);
|
|
331
|
+
}
|
|
332
|
+
const targetId = siblings[nextIndex];
|
|
333
|
+
const target = this.nodes.get(targetId);
|
|
334
|
+
if (!shallow && target && target.childrenIds.length > 0) {
|
|
335
|
+
this.focusFirstChild(targetId);
|
|
336
|
+
} else {
|
|
337
|
+
this.focusNode(targetId);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Passive scopes
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
makePassive(id) {
|
|
344
|
+
if (!this.nodes.has(id)) return;
|
|
345
|
+
this.passiveSet.add(id);
|
|
346
|
+
this.focusNode(id);
|
|
347
|
+
}
|
|
348
|
+
isPassive(id) {
|
|
349
|
+
return this.passiveSet.has(id);
|
|
350
|
+
}
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// Queries
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
isFocused(id) {
|
|
355
|
+
if (!this.focusedId) return false;
|
|
356
|
+
let cursor = this.focusedId;
|
|
357
|
+
while (cursor) {
|
|
358
|
+
if (cursor === id) return true;
|
|
359
|
+
const node = this.nodes.get(cursor);
|
|
360
|
+
cursor = (node == null ? void 0 : node.parentId) ?? null;
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
getFocusedId() {
|
|
365
|
+
return this.focusedId;
|
|
366
|
+
}
|
|
367
|
+
getActiveBranchPath() {
|
|
368
|
+
if (!this.focusedId) return [];
|
|
369
|
+
const path = [];
|
|
370
|
+
let cursor = this.focusedId;
|
|
371
|
+
while (cursor) {
|
|
372
|
+
path.push(cursor);
|
|
373
|
+
const node = this.nodes.get(cursor);
|
|
374
|
+
cursor = (node == null ? void 0 : node.parentId) ?? null;
|
|
375
|
+
}
|
|
376
|
+
return path;
|
|
377
|
+
}
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// Trap
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
setTrap(nodeId) {
|
|
382
|
+
this.trapNodeId = nodeId;
|
|
383
|
+
}
|
|
384
|
+
clearTrap(nodeId) {
|
|
385
|
+
if (this.trapNodeId === nodeId) {
|
|
386
|
+
this.trapNodeId = null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
getTrapNodeId() {
|
|
390
|
+
return this.trapNodeId;
|
|
391
|
+
}
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// Keybinding registry
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
registerKeybindings(nodeId, registrationId, bindings, options) {
|
|
396
|
+
const entries = Object.entries(bindings).filter((entry) => entry[1] != null).map(([key, def]) => {
|
|
397
|
+
if (typeof def === "function") {
|
|
398
|
+
return [key, { handler: def }];
|
|
399
|
+
}
|
|
400
|
+
return [key, { handler: def.action, name: def.name, when: def.when }];
|
|
401
|
+
});
|
|
402
|
+
const registration = {
|
|
403
|
+
bindings: new Map(entries),
|
|
404
|
+
capture: (options == null ? void 0 : options.capture) ?? false,
|
|
405
|
+
onKeypress: options == null ? void 0 : options.onKeypress,
|
|
406
|
+
passthrough: (options == null ? void 0 : options.passthrough) ? new Set(options.passthrough) : void 0,
|
|
407
|
+
layer: options == null ? void 0 : options.layer
|
|
408
|
+
};
|
|
409
|
+
if (!this.keybindings.has(nodeId)) {
|
|
410
|
+
this.keybindings.set(nodeId, /* @__PURE__ */ new Map());
|
|
411
|
+
}
|
|
412
|
+
this.keybindings.get(nodeId).set(registrationId, registration);
|
|
413
|
+
}
|
|
414
|
+
unregisterKeybindings(nodeId, registrationId) {
|
|
415
|
+
const nodeRegistrations = this.keybindings.get(nodeId);
|
|
416
|
+
if (nodeRegistrations) {
|
|
417
|
+
nodeRegistrations.delete(registrationId);
|
|
418
|
+
if (nodeRegistrations.size === 0) {
|
|
419
|
+
this.keybindings.delete(nodeId);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
getNodeBindings(nodeId) {
|
|
424
|
+
const nodeRegistrations = this.keybindings.get(nodeId);
|
|
425
|
+
if (!nodeRegistrations || nodeRegistrations.size === 0) return void 0;
|
|
426
|
+
const mergedBindings = /* @__PURE__ */ new Map();
|
|
427
|
+
let finalCapture = false;
|
|
428
|
+
let finalOnKeypress;
|
|
429
|
+
let finalPassthrough;
|
|
430
|
+
let finalLayer;
|
|
431
|
+
for (const registration of nodeRegistrations.values()) {
|
|
432
|
+
const isCaptureRegistration = registration.onKeypress !== void 0;
|
|
433
|
+
const shouldIncludeBindings = !isCaptureRegistration || registration.capture;
|
|
434
|
+
if (shouldIncludeBindings) {
|
|
435
|
+
for (const [key, entry] of registration.bindings) {
|
|
436
|
+
mergedBindings.set(key, entry);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (registration.capture) {
|
|
440
|
+
finalCapture = true;
|
|
441
|
+
finalOnKeypress = registration.onKeypress;
|
|
442
|
+
finalPassthrough = registration.passthrough;
|
|
443
|
+
}
|
|
444
|
+
if (registration.layer) {
|
|
445
|
+
finalLayer = registration.layer;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
bindings: mergedBindings,
|
|
450
|
+
capture: finalCapture,
|
|
451
|
+
onKeypress: finalOnKeypress,
|
|
452
|
+
passthrough: finalPassthrough,
|
|
453
|
+
layer: finalLayer
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
getAllBindings() {
|
|
457
|
+
const all = [];
|
|
458
|
+
for (const [nodeId, nodeRegistrations] of this.keybindings) {
|
|
459
|
+
for (const registration of nodeRegistrations.values()) {
|
|
460
|
+
for (const [key, entry] of registration.bindings) {
|
|
461
|
+
all.push({
|
|
462
|
+
nodeId,
|
|
463
|
+
key,
|
|
464
|
+
handler: entry.handler,
|
|
465
|
+
name: entry.name,
|
|
466
|
+
when: entry.when,
|
|
467
|
+
layer: registration.layer
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return all;
|
|
473
|
+
}
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// Input dispatch
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
// Bridge target for InputRouter. Walks the active branch path with passive-scope
|
|
478
|
+
// skipping, capture mode, and trap boundary — the full dispatch algorithm.
|
|
479
|
+
dispatch(input, key) {
|
|
480
|
+
var _a;
|
|
481
|
+
const keyName = normalizeKey(input, key);
|
|
482
|
+
if (!keyName) return;
|
|
483
|
+
const path = this.getActiveBranchPath();
|
|
484
|
+
const trapNodeId = this.trapNodeId;
|
|
485
|
+
for (const nodeId of path) {
|
|
486
|
+
if (this.passiveSet.has(nodeId)) continue;
|
|
487
|
+
const nodeBindings = this.getNodeBindings(nodeId);
|
|
488
|
+
if (nodeBindings) {
|
|
489
|
+
if (nodeBindings.capture && nodeBindings.onKeypress) {
|
|
490
|
+
if (!((_a = nodeBindings.passthrough) == null ? void 0 : _a.has(keyName))) {
|
|
491
|
+
nodeBindings.onKeypress(input, key);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const entry = nodeBindings.bindings.get(keyName);
|
|
496
|
+
if (entry && entry.when !== "mounted") {
|
|
497
|
+
entry.handler(input, key);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (nodeId === trapNodeId) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
for (const binding of this.getAllBindings()) {
|
|
506
|
+
if (binding.key === keyName && binding.when === "mounted") {
|
|
507
|
+
binding.handler(input, key);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// Private helpers
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
// Is `ancestor` an ancestor of `descendant`? (or equal)
|
|
516
|
+
isAncestorOf(ancestor, descendant) {
|
|
517
|
+
let cursor = descendant;
|
|
518
|
+
while (cursor) {
|
|
519
|
+
if (cursor === ancestor) return true;
|
|
520
|
+
const node = this.nodes.get(cursor);
|
|
521
|
+
cursor = (node == null ? void 0 : node.parentId) ?? null;
|
|
522
|
+
}
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
export {
|
|
528
|
+
GigglesError,
|
|
529
|
+
FocusStore,
|
|
530
|
+
StoreContext,
|
|
531
|
+
ScopeIdContext,
|
|
532
|
+
useStore,
|
|
533
|
+
InputRouter,
|
|
534
|
+
useKeybindings,
|
|
535
|
+
useKeybindingRegistry,
|
|
536
|
+
useFocusNode,
|
|
537
|
+
FocusTrap,
|
|
538
|
+
useFocusScope,
|
|
539
|
+
FocusScope
|
|
540
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useTheme
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-C77VBSPK.js";
|
|
4
4
|
|
|
5
5
|
// src/ui/CodeBlock.tsx
|
|
6
6
|
import { Box, Text } from "ink";
|
|
@@ -27,7 +27,7 @@ function CodeBlock({ children, language, tokenColors, ...boxProps }) {
|
|
|
27
27
|
const colors = { ...defaultTokenColors, ...tokenColors };
|
|
28
28
|
const grammar = language ? Prism.languages[language] : void 0;
|
|
29
29
|
const content = grammar ? renderTokens(Prism.tokenize(children, grammar), colors) : /* @__PURE__ */ jsx(Text, { children });
|
|
30
|
-
return /* @__PURE__ */ jsx(Box, { paddingX: 1, borderStyle:
|
|
30
|
+
return /* @__PURE__ */ jsx(Box, { paddingX: 1, borderStyle: theme.borderStyle, borderColor: theme.borderColor, ...boxProps, children: /* @__PURE__ */ jsx(Text, { children: content }) });
|
|
31
31
|
}
|
|
32
32
|
function renderTokens(tokens, colors) {
|
|
33
33
|
return tokens.map((token, idx) => {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/terminal/hooks/useTerminalSize.ts
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
function useTerminalSize() {
|
|
4
|
+
const [size, setSize] = useState({
|
|
5
|
+
rows: process.stdout.rows,
|
|
6
|
+
columns: process.stdout.columns
|
|
7
|
+
});
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const handleResize = () => {
|
|
10
|
+
setSize({
|
|
11
|
+
rows: process.stdout.rows,
|
|
12
|
+
columns: process.stdout.columns
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
process.stdout.on("resize", handleResize);
|
|
16
|
+
return () => {
|
|
17
|
+
process.stdout.off("resize", handleResize);
|
|
18
|
+
};
|
|
19
|
+
}, []);
|
|
20
|
+
return size;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
useTerminalSize
|
|
25
|
+
};
|