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 CHANGED
@@ -12,9 +12,9 @@ inspired by the [charmbracelet](https://github.com/charmbracelet) ecosystem, it
12
12
 
13
13
  ## features
14
14
 
15
- - no `useInput` hooks scattered across your app focus, input routing, and keyboard navigation are handled for you
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 — `useFocus`, `useFocusNode`, `FocusGroup`, `FocusTrap`, `useNavigation`, and more — for building any interaction pattern without reimplementing the plumbing
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
@@ -4,6 +4,7 @@ import { jsx } from "react/jsx-runtime";
4
4
  var defaultTheme = {
5
5
  accentColor: "#6B9FD4",
6
6
  borderColor: "#5C5C5C",
7
+ borderStyle: "round",
7
8
  selectedColor: "#8FBF7F",
8
9
  hintColor: "#8A8A8A",
9
10
  hintDimColor: "#5C5C5C",
@@ -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-R5I4YOOP.js";
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: "round", borderColor: theme.borderColor, ...boxProps, children: /* @__PURE__ */ jsx(Text, { children: content }) });
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
+ };