giggles 0.2.3 → 0.3.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/README.md CHANGED
@@ -1,5 +1,5 @@
1
- [![npm version](https://img.shields.io/npm/v/giggles)](https://www.npmjs.com/package/giggles)
2
- [![npm downloads](https://img.shields.io/npm/dm/giggles)](https://www.npmjs.com/package/giggles)
1
+ [![CI](https://github.com/zion-off/giggles/actions/workflows/giggles-lint.yml/badge.svg)](https://github.com/zion-off/giggles/actions/workflows/giggles-lint.yml)
2
+ [![CD](https://github.com/zion-off/giggles/actions/workflows/giggles-cd.yml/badge.svg)](https://github.com/zion-off/giggles/actions/workflows/giggles-cd.yml)
3
3
  [![docs](https://img.shields.io/badge/docs-giggles.zzzzion.com-blue)](https://giggles.zzzzion.com)
4
4
 
5
5
  # giggles
@@ -0,0 +1,23 @@
1
+ // src/terminal/components/AlternateScreen.tsx
2
+ import { useEffect, useState } from "react";
3
+ import { Fragment, jsx } from "react/jsx-runtime";
4
+ var _a;
5
+ var isTTY = typeof process !== "undefined" && ((_a = process.stdout) == null ? void 0 : _a.write);
6
+ function AlternateScreen({ children }) {
7
+ const [ready, setReady] = useState(!isTTY);
8
+ useEffect(() => {
9
+ if (!isTTY) return;
10
+ process.stdout.write("\x1B[?1049h");
11
+ process.stdout.write("\x1B[2J");
12
+ process.stdout.write("\x1B[H");
13
+ setReady(true);
14
+ return () => {
15
+ process.stdout.write("\x1B[?1049l");
16
+ };
17
+ }, []);
18
+ return ready ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
19
+ }
20
+
21
+ export {
22
+ AlternateScreen
23
+ };
package/dist/index.d.ts CHANGED
@@ -13,14 +13,32 @@ type GigglesProviderProps = {
13
13
  };
14
14
  declare function GigglesProvider({ children }: GigglesProviderProps): react_jsx_runtime.JSX.Element;
15
15
 
16
+ type KeyHandler = (input: string, key: Key) => void;
17
+ type SpecialKey = 'up' | 'down' | 'left' | 'right' | 'enter' | 'escape' | 'tab' | 'backspace' | 'delete' | 'pageup' | 'pagedown' | 'home' | 'end';
18
+ type KeyName = SpecialKey | (string & {});
19
+ type Keybindings = Partial<Record<KeyName, KeyHandler>>;
20
+ type KeybindingOptions = {
21
+ capture?: boolean;
22
+ onKeypress?: (input: string, key: Key) => void;
23
+ layer?: string;
24
+ };
25
+
26
+ declare function useKeybindings(focus: FocusHandle, bindings: Keybindings, options?: KeybindingOptions): void;
27
+
28
+ type FocusTrapProps = {
29
+ children: React.ReactNode;
30
+ };
31
+ declare function FocusTrap({ children }: FocusTrapProps): react_jsx_runtime.JSX.Element;
32
+
16
33
  type FocusGroupProps = {
17
34
  children: React__default.ReactNode;
18
35
  direction?: 'vertical' | 'horizontal';
19
36
  value?: string;
20
37
  wrap?: boolean;
21
38
  navigable?: boolean;
39
+ keybindings?: Keybindings;
22
40
  };
23
- declare function FocusGroup({ children, direction, value, wrap, navigable }: FocusGroupProps): react_jsx_runtime.JSX.Element;
41
+ declare function FocusGroup({ children, direction, value, wrap, navigable, keybindings: customBindings }: FocusGroupProps): react_jsx_runtime.JSX.Element;
24
42
 
25
43
  type FocusHandle = {
26
44
  id: string;
@@ -32,23 +50,6 @@ declare const useFocus: (id?: string) => FocusHandle;
32
50
 
33
51
  declare function useFocusState<T extends string>(initial: T): readonly [T, React$1.Dispatch<React$1.SetStateAction<T>>];
34
52
 
35
- type KeyHandler = (input: string, key: Key) => void;
36
- type SpecialKey = 'up' | 'down' | 'left' | 'right' | 'enter' | 'escape' | 'tab' | 'backspace' | 'delete' | 'pageup' | 'pagedown' | 'home' | 'end';
37
- type KeyName = SpecialKey | (string & {});
38
- type Keybindings = Partial<Record<KeyName, KeyHandler>>;
39
- type KeybindingOptions = {
40
- capture?: boolean;
41
- onKeypress?: (input: string, key: Key) => void;
42
- layer?: string;
43
- };
44
-
45
- declare function useKeybindings(focus: FocusHandle, bindings: Keybindings, options?: KeybindingOptions): void;
46
-
47
- type FocusTrapProps = {
48
- children: React.ReactNode;
49
- };
50
- declare function FocusTrap({ children }: FocusTrapProps): react_jsx_runtime.JSX.Element;
51
-
52
53
  type RouterProps = {
53
54
  children: React__default.ReactNode;
54
55
  initialScreen: string;
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ AlternateScreen
3
+ } from "./chunk-7PDVDYFB.js";
4
+
1
5
  // src/core/GigglesError.ts
2
6
  var GigglesError = class extends Error {
3
7
  constructor(message) {
@@ -14,24 +18,11 @@ var FocusProvider = ({ children }) => {
14
18
  const nodesRef = useRef(/* @__PURE__ */ new Map());
15
19
  const pendingFocusFirstChildRef = useRef(/* @__PURE__ */ new Set());
16
20
  const [focusedId, setFocusedId] = useState(null);
17
- const [activeBranchNodes, setActiveBranchNodes] = useState(/* @__PURE__ */ new Set());
18
- const [activeBranchPath, setActiveBranchPath] = useState([]);
19
21
  const focusNode = useCallback((id) => {
20
22
  const nodes = nodesRef.current;
21
23
  if (!nodes.has(id)) return;
22
24
  setFocusedId((current) => {
23
25
  if (current === id) return current;
24
- const pathSet = /* @__PURE__ */ new Set();
25
- const pathArray = [];
26
- let currentNode = id;
27
- while (currentNode) {
28
- pathSet.add(currentNode);
29
- pathArray.push(currentNode);
30
- const node = nodes.get(currentNode);
31
- currentNode = (node == null ? void 0 : node.parentId) ?? null;
32
- }
33
- setActiveBranchNodes(pathSet);
34
- setActiveBranchPath(pathArray);
35
26
  return id;
36
27
  });
37
28
  }, []);
@@ -92,23 +83,7 @@ var FocusProvider = ({ children }) => {
92
83
  pendingFocusFirstChildRef.current.delete(id);
93
84
  setFocusedId((current) => {
94
85
  if (current !== id) return current;
95
- if (node.parentId) {
96
- const pathSet = /* @__PURE__ */ new Set();
97
- const pathArray = [];
98
- let currentNode = node.parentId;
99
- while (currentNode) {
100
- pathSet.add(currentNode);
101
- pathArray.push(currentNode);
102
- const n = nodes.get(currentNode);
103
- currentNode = (n == null ? void 0 : n.parentId) ?? null;
104
- }
105
- setActiveBranchNodes(pathSet);
106
- setActiveBranchPath(pathArray);
107
- return node.parentId;
108
- }
109
- setActiveBranchNodes(/* @__PURE__ */ new Set());
110
- setActiveBranchPath([]);
111
- return null;
86
+ return node.parentId ?? null;
112
87
  });
113
88
  }, []);
114
89
  const isFocused = useCallback(
@@ -120,15 +95,18 @@ var FocusProvider = ({ children }) => {
120
95
  const getFocusedId = useCallback(() => {
121
96
  return focusedId;
122
97
  }, [focusedId]);
123
- const isInActiveBranch = useCallback(
124
- (id) => {
125
- return activeBranchNodes.has(id);
126
- },
127
- [activeBranchNodes]
128
- );
129
98
  const getActiveBranchPath = useCallback(() => {
130
- return activeBranchPath;
131
- }, [activeBranchPath]);
99
+ if (!focusedId) return [];
100
+ const nodes = nodesRef.current;
101
+ const pathArray = [];
102
+ let node = focusedId;
103
+ while (node) {
104
+ pathArray.push(node);
105
+ const n = nodes.get(node);
106
+ node = (n == null ? void 0 : n.parentId) ?? null;
107
+ }
108
+ return pathArray;
109
+ }, [focusedId]);
132
110
  const navigateSibling = useCallback(
133
111
  (direction, wrap = true) => {
134
112
  const currentId = focusedId;
@@ -161,7 +139,6 @@ var FocusProvider = ({ children }) => {
161
139
  focusFirstChild,
162
140
  isFocused,
163
141
  getFocusedId,
164
- isInActiveBranch,
165
142
  getActiveBranchPath,
166
143
  navigateSibling
167
144
  },
@@ -179,7 +156,7 @@ var useFocusContext = () => {
179
156
  };
180
157
 
181
158
  // src/core/focus/FocusGroup.tsx
182
- import { useCallback as useCallback3, useEffect as useEffect4, useMemo, useRef as useRef3 } from "react";
159
+ import { useCallback as useCallback3, useEffect as useEffect4, useMemo, useRef as useRef4 } from "react";
183
160
 
184
161
  // src/core/input/InputContext.tsx
185
162
  import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useRef as useRef2 } from "react";
@@ -257,6 +234,7 @@ import { useInput } from "ink";
257
234
 
258
235
  // src/core/input/normalizeKey.ts
259
236
  function normalizeKey(input, key) {
237
+ if (input === "\x1B[I" || input === "\x1B[O") return "";
260
238
  if (key.downArrow) return "down";
261
239
  if (key.upArrow) return "up";
262
240
  if (key.leftArrow) return "left";
@@ -284,6 +262,7 @@ function InputRouter({ children }) {
284
262
  const path = getActiveBranchPath();
285
263
  const trapNodeId = getTrapNodeId();
286
264
  const keyName = normalizeKey(input, key);
265
+ if (!keyName) return;
287
266
  for (const nodeId of path) {
288
267
  const nodeBindings = getNodeBindings(nodeId);
289
268
  if (!nodeBindings) continue;
@@ -315,16 +294,25 @@ function useKeybindings(focus, bindings, options) {
315
294
  }
316
295
 
317
296
  // src/core/input/FocusTrap.tsx
318
- import { useEffect as useEffect2 } from "react";
319
- import { Fragment as Fragment2, jsx as jsx4 } from "react/jsx-runtime";
297
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
298
+ import { jsx as jsx4 } from "react/jsx-runtime";
320
299
  function FocusTrap({ children }) {
321
300
  const { id } = useFocus();
322
301
  const { setTrap, clearTrap } = useInputContext();
302
+ const { focusFirstChild, getFocusedId, focusNode } = useFocusContext();
303
+ const previousFocusRef = useRef3(getFocusedId());
323
304
  useEffect2(() => {
305
+ const previousFocus = previousFocusRef.current;
324
306
  setTrap(id);
325
- return () => clearTrap(id);
326
- }, [id, setTrap, clearTrap]);
327
- return /* @__PURE__ */ jsx4(Fragment2, { children });
307
+ focusFirstChild(id);
308
+ return () => {
309
+ clearTrap(id);
310
+ if (previousFocus) {
311
+ focusNode(previousFocus);
312
+ }
313
+ };
314
+ }, [id]);
315
+ return /* @__PURE__ */ jsx4(FocusNodeContext.Provider, { value: id, children });
328
316
  }
329
317
 
330
318
  // src/core/focus/FocusBindContext.tsx
@@ -364,11 +352,12 @@ function FocusGroup({
364
352
  direction = "vertical",
365
353
  value,
366
354
  wrap = true,
367
- navigable = true
355
+ navigable = true,
356
+ keybindings: customBindings
368
357
  }) {
369
358
  const focus = useFocus();
370
359
  const { focusNode, navigateSibling } = useFocusContext();
371
- const bindMapRef = useRef3(/* @__PURE__ */ new Map());
360
+ const bindMapRef = useRef4(/* @__PURE__ */ new Map());
372
361
  const register = useCallback3((logicalId, nodeId) => {
373
362
  if (bindMapRef.current.has(logicalId)) {
374
363
  throw new GigglesError(`FocusGroup: Duplicate id "${logicalId}". Each child must have a unique id.`);
@@ -386,7 +375,7 @@ function FocusGroup({
386
375
  }
387
376
  }
388
377
  }, [value, focusNode]);
389
- const bindContextValue = value ? { register, unregister } : null;
378
+ const bindContextValue = useMemo(() => value ? { register, unregister } : null, [value, register, unregister]);
390
379
  const navigationKeys = useMemo(() => {
391
380
  if (!navigable) return {};
392
381
  const next = () => navigateSibling("next", wrap);
@@ -403,7 +392,11 @@ function FocusGroup({
403
392
  left: prev
404
393
  };
405
394
  }, [navigable, direction, wrap, navigateSibling]);
406
- useKeybindings(focus, navigationKeys);
395
+ const mergedBindings = useMemo(
396
+ () => ({ ...navigationKeys, ...customBindings }),
397
+ [navigationKeys, customBindings]
398
+ );
399
+ useKeybindings(focus, mergedBindings);
407
400
  return /* @__PURE__ */ jsx5(FocusNodeContext.Provider, { value: focus.id, children: /* @__PURE__ */ jsx5(FocusBindContext.Provider, { value: bindContextValue, children }) });
408
401
  }
409
402
 
@@ -417,11 +410,11 @@ function useFocusState(initial) {
417
410
  // src/core/GigglesProvider.tsx
418
411
  import { jsx as jsx6 } from "react/jsx-runtime";
419
412
  function GigglesProvider({ children }) {
420
- return /* @__PURE__ */ jsx6(FocusProvider, { children: /* @__PURE__ */ jsx6(InputProvider, { children: /* @__PURE__ */ jsx6(InputRouter, { children }) }) });
413
+ return /* @__PURE__ */ jsx6(AlternateScreen, { children: /* @__PURE__ */ jsx6(FocusProvider, { children: /* @__PURE__ */ jsx6(InputProvider, { children: /* @__PURE__ */ jsx6(InputRouter, { children }) }) }) });
421
414
  }
422
415
 
423
416
  // src/core/router/Router.tsx
424
- import React4, { useCallback as useCallback4, useReducer, useRef as useRef5 } from "react";
417
+ import React4, { useCallback as useCallback4, useReducer, useRef as useRef6 } from "react";
425
418
 
426
419
  // src/core/router/Screen.tsx
427
420
  function Screen(_props) {
@@ -429,7 +422,7 @@ function Screen(_props) {
429
422
  }
430
423
 
431
424
  // src/core/router/ScreenEntry.tsx
432
- import React3, { useEffect as useEffect5, useId as useId2, useMemo as useMemo2, useRef as useRef4 } from "react";
425
+ import React3, { useEffect as useEffect5, useId as useId2, useMemo as useMemo2, useRef as useRef5 } from "react";
433
426
  import { Box } from "ink";
434
427
 
435
428
  // src/core/router/NavigationContext.tsx
@@ -459,8 +452,8 @@ function ScreenEntry({
459
452
  const screenNodeId = useId2();
460
453
  const parentId = React3.useContext(FocusNodeContext);
461
454
  const { registerNode, unregisterNode, focusFirstChild, focusNode, getFocusedId } = useFocusContext();
462
- const lastFocusedChildRef = useRef4(null);
463
- const wasTopRef = useRef4(isTop);
455
+ const lastFocusedChildRef = useRef5(null);
456
+ const wasTopRef = useRef5(isTop);
464
457
  useEffect5(() => {
465
458
  registerNode(screenNodeId, parentId);
466
459
  return () => {
@@ -490,7 +483,7 @@ function ScreenEntry({
490
483
  }
491
484
 
492
485
  // src/core/router/Router.tsx
493
- import { Fragment as Fragment3, jsx as jsx8 } from "react/jsx-runtime";
486
+ import { Fragment as Fragment2, jsx as jsx8 } from "react/jsx-runtime";
494
487
  function routerReducer(stack, action) {
495
488
  switch (action.type) {
496
489
  case "push":
@@ -504,9 +497,9 @@ function routerReducer(stack, action) {
504
497
  }
505
498
  }
506
499
  function Router({ children, initialScreen, initialParams, restoreFocus = true }) {
507
- const screenId = useRef5(0);
500
+ const screenId = useRef6(0);
508
501
  const routes = React4.Children.toArray(children).filter((child) => React4.isValidElement(child) && child.type === Screen).map((child) => child.props);
509
- const screenNamesRef = useRef5(/* @__PURE__ */ new Set());
502
+ const screenNamesRef = useRef6(/* @__PURE__ */ new Set());
510
503
  screenNamesRef.current = new Set(routes.map((r) => r.name));
511
504
  const assertScreen = useCallback4((name) => {
512
505
  if (!screenNamesRef.current.has(name)) {
@@ -552,7 +545,7 @@ function Router({ children, initialScreen, initialParams, restoreFocus = true })
552
545
  components.set(route.name, route.component);
553
546
  }
554
547
  const canGoBack = stack.length > 1;
555
- return /* @__PURE__ */ jsx8(Fragment3, { children: stack.map((entry, i) => {
548
+ return /* @__PURE__ */ jsx8(Fragment2, { children: stack.map((entry, i) => {
556
549
  const Component = components.get(entry.name);
557
550
  if (!Component) return null;
558
551
  return /* @__PURE__ */ jsx8(
@@ -0,0 +1,23 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type TerminalSize = {
5
+ rows: number;
6
+ columns: number;
7
+ };
8
+
9
+ declare function useTerminalSize(): TerminalSize;
10
+
11
+ declare function useTerminalFocus(callback: (focused: boolean) => void): void;
12
+
13
+ declare function AlternateScreen({ children }: {
14
+ children: ReactNode;
15
+ }): react_jsx_runtime.JSX.Element | null;
16
+
17
+ declare function useShellOut(): {
18
+ run: (command: string) => Promise<{
19
+ exitCode: number;
20
+ }>;
21
+ };
22
+
23
+ export { AlternateScreen, type TerminalSize, useShellOut, useTerminalFocus, useTerminalSize };
@@ -0,0 +1,83 @@
1
+ import {
2
+ AlternateScreen
3
+ } from "../chunk-7PDVDYFB.js";
4
+
5
+ // src/terminal/hooks/useTerminalSize.ts
6
+ import { useEffect, useState } from "react";
7
+ function useTerminalSize() {
8
+ const [size, setSize] = useState({
9
+ rows: process.stdout.rows,
10
+ columns: process.stdout.columns
11
+ });
12
+ useEffect(() => {
13
+ const handleResize = () => {
14
+ setSize({
15
+ rows: process.stdout.rows,
16
+ columns: process.stdout.columns
17
+ });
18
+ };
19
+ process.stdout.on("resize", handleResize);
20
+ return () => {
21
+ process.stdout.off("resize", handleResize);
22
+ };
23
+ }, []);
24
+ return size;
25
+ }
26
+
27
+ // src/terminal/hooks/useTerminalFocus.ts
28
+ import { useEffect as useEffect2, useRef } from "react";
29
+ function useTerminalFocus(callback) {
30
+ const callbackRef = useRef(callback);
31
+ callbackRef.current = callback;
32
+ useEffect2(() => {
33
+ const handler = (data) => {
34
+ const str = data.toString();
35
+ if (str.includes("\x1B[I")) callbackRef.current(true);
36
+ if (str.includes("\x1B[O")) callbackRef.current(false);
37
+ };
38
+ process.stdin.on("data", handler);
39
+ const timer = setTimeout(() => {
40
+ process.stdout.write("\x1B[?1004h");
41
+ }, 0);
42
+ return () => {
43
+ clearTimeout(timer);
44
+ process.stdout.write("\x1B[?1004l");
45
+ process.stdin.off("data", handler);
46
+ };
47
+ }, []);
48
+ }
49
+
50
+ // src/terminal/hooks/useShellout.ts
51
+ import { execa } from "execa";
52
+ import { useCallback, useState as useState2 } from "react";
53
+ import { useStdin } from "ink";
54
+ function useShellOut() {
55
+ const [, setRedrawCount] = useState2(0);
56
+ const { setRawMode } = useStdin();
57
+ const run = useCallback(
58
+ async (command) => {
59
+ process.stdout.write("\x1B[?1049l");
60
+ setRawMode(false);
61
+ try {
62
+ const result = await execa(command, { stdio: "inherit", shell: true, reject: false });
63
+ return { exitCode: result.exitCode ?? 0 };
64
+ } finally {
65
+ setRawMode(true);
66
+ process.stdout.write("\x1B[?1049h");
67
+ process.stdout.write("\x1B[2J");
68
+ process.stdout.write("\x1B[H");
69
+ setRedrawCount((c) => c + 1);
70
+ }
71
+ },
72
+ [setRawMode]
73
+ );
74
+ return {
75
+ run
76
+ };
77
+ }
78
+ export {
79
+ AlternateScreen,
80
+ useShellOut,
81
+ useTerminalFocus,
82
+ useTerminalSize
83
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "giggles",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,6 +13,10 @@
13
13
  ".": {
14
14
  "import": "./dist/index.js",
15
15
  "types": "./dist/index.d.ts"
16
+ },
17
+ "./terminal": {
18
+ "import": "./dist/terminal/index.js",
19
+ "types": "./dist/terminal/index.d.ts"
16
20
  }
17
21
  },
18
22
  "keywords": [
@@ -54,5 +58,8 @@
54
58
  "tsx": "^4.21.0",
55
59
  "typescript": "^5.0.3",
56
60
  "typescript-eslint": "^8.0.0"
61
+ },
62
+ "dependencies": {
63
+ "execa": "^9.6.1"
57
64
  }
58
65
  }