@valyrianjs/terminal 0.2.0 → 0.2.2

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.
Files changed (102) hide show
  1. package/dist/ansi.d.ts +2 -0
  2. package/dist/ansi.d.ts.map +1 -1
  3. package/dist/ansi.js +23 -13
  4. package/dist/ansi.js.map +1 -1
  5. package/dist/events.d.ts.map +1 -1
  6. package/dist/events.js +10 -2
  7. package/dist/events.js.map +1 -1
  8. package/dist/frame-style.d.ts +7 -0
  9. package/dist/frame-style.d.ts.map +1 -0
  10. package/dist/frame-style.js +27 -0
  11. package/dist/frame-style.js.map +1 -0
  12. package/dist/keymap.d.ts.map +1 -1
  13. package/dist/keymap.js +4 -2
  14. package/dist/keymap.js.map +1 -1
  15. package/dist/layout.d.ts +5 -1
  16. package/dist/layout.d.ts.map +1 -1
  17. package/dist/layout.js +55 -24
  18. package/dist/layout.js.map +1 -1
  19. package/dist/mouse.d.ts +6 -0
  20. package/dist/mouse.d.ts.map +1 -1
  21. package/dist/mouse.js +38 -17
  22. package/dist/mouse.js.map +1 -1
  23. package/dist/primitives.d.ts.map +1 -1
  24. package/dist/primitives.js +8 -1
  25. package/dist/primitives.js.map +1 -1
  26. package/dist/render.d.ts.map +1 -1
  27. package/dist/render.js +266 -70
  28. package/dist/render.js.map +1 -1
  29. package/dist/runtime.d.ts.map +1 -1
  30. package/dist/runtime.js +13 -5
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/session.d.ts.map +1 -1
  33. package/dist/session.js +325 -83
  34. package/dist/session.js.map +1 -1
  35. package/dist/text.d.ts +7 -0
  36. package/dist/text.d.ts.map +1 -1
  37. package/dist/text.js +114 -0
  38. package/dist/text.js.map +1 -1
  39. package/dist/theme.d.ts.map +1 -1
  40. package/dist/theme.js +3 -0
  41. package/dist/theme.js.map +1 -1
  42. package/dist/tree.d.ts.map +1 -1
  43. package/dist/tree.js +18 -4
  44. package/dist/tree.js.map +1 -1
  45. package/dist/types.d.ts +41 -4
  46. package/dist/types.d.ts.map +1 -1
  47. package/docs/api-reference.md +18 -8
  48. package/docs/cookbook.md +1 -1
  49. package/docs/interaction-model.md +10 -8
  50. package/docs/primitive-gallery.md +9 -5
  51. package/examples/basic.tsx +22 -0
  52. package/examples/cli.tsx +55 -0
  53. package/examples/demo.tsx +98 -0
  54. package/examples/docs/background-fill.tsx +107 -0
  55. package/examples/docs/component-composition.tsx +140 -0
  56. package/examples/docs/cursor.tsx +121 -0
  57. package/examples/docs/employees-list.tsx +138 -0
  58. package/examples/docs/hello.tsx +98 -0
  59. package/examples/docs/interactive-note.tsx +111 -0
  60. package/examples/docs/module-api-dashboard.tsx +307 -0
  61. package/examples/docs/module-flux-store.tsx +181 -0
  62. package/examples/docs/module-form-workflow.tsx +339 -0
  63. package/examples/docs/module-forms.tsx +218 -0
  64. package/examples/docs/module-money.tsx +175 -0
  65. package/examples/docs/module-native-store.tsx +188 -0
  66. package/examples/docs/module-pulses.tsx +142 -0
  67. package/examples/docs/module-query.tsx +209 -0
  68. package/examples/docs/module-request.tsx +194 -0
  69. package/examples/docs/module-state-workbench.tsx +283 -0
  70. package/examples/docs/module-tasks.tsx +223 -0
  71. package/examples/docs/module-translate.tsx +194 -0
  72. package/examples/docs/module-utils.tsx +168 -0
  73. package/examples/docs/module-valyrian-core.tsx +159 -0
  74. package/examples/docs/pizza-builder.tsx +463 -0
  75. package/examples/docs/primitive-activity-console.tsx +113 -0
  76. package/examples/docs/primitive-command-panel.tsx +186 -0
  77. package/examples/docs/primitive-data-explorer.tsx +155 -0
  78. package/examples/docs/primitive-input-workbench.tsx +128 -0
  79. package/examples/docs/primitive-layout-shell.tsx +115 -0
  80. package/examples/docs/responsive-split.tsx +186 -0
  81. package/examples/docs/style-system.tsx +209 -0
  82. package/examples/docs/theme-colors.tsx +225 -0
  83. package/examples/docs/virtualized-list-workbench.tsx +232 -0
  84. package/examples/opencode-dogfood-app.tsx +215 -0
  85. package/examples/opencode-dogfood-lifecycle.tsx +194 -0
  86. package/examples/opencode-dogfood.tsx +11 -0
  87. package/llms-full.txt +38 -22
  88. package/package.json +3 -2
  89. package/src/ansi.ts +23 -13
  90. package/src/events.ts +6 -2
  91. package/src/frame-style.ts +36 -0
  92. package/src/keymap.ts +4 -2
  93. package/src/layout.ts +59 -25
  94. package/src/mouse.ts +41 -16
  95. package/src/primitives.ts +8 -1
  96. package/src/render.ts +286 -71
  97. package/src/runtime.ts +13 -5
  98. package/src/session.ts +343 -79
  99. package/src/text.ts +148 -0
  100. package/src/theme.ts +3 -0
  101. package/src/tree.ts +19 -4
  102. package/src/types.ts +48 -3
package/src/text.ts CHANGED
@@ -5,6 +5,20 @@ const C1_CSI_TERMINAL_CONTROL = /\u009b[0-?]*[ -/]*[@-~]/g;
5
5
  const ESC_TERMINAL_CONTROL = /\u001b[ -/]*[0-~]/g;
6
6
  const C1_TERMINAL_CONTROL = /[\u0080-\u009f]/g;
7
7
  const C0_TERMINAL_CONTROL = /[\u0000-\u0009\u000b-\u001f\u007f]/g;
8
+ const COMBINING_MARK = /\p{Mark}/u;
9
+ const EMOJI_PRESENTATION = /\p{Extended_Pictographic}/u;
10
+
11
+ type GraphemeSegmenter = {
12
+ segment(value: string): Iterable<{ segment: string }>;
13
+ };
14
+
15
+ const Segmenter = (Intl as unknown as {
16
+ Segmenter?: new (locale?: string, options?: { granularity: "grapheme" }) => GraphemeSegmenter;
17
+ }).Segmenter;
18
+
19
+ const GRAPHEME_SEGMENTER = typeof Segmenter === "function"
20
+ ? new Segmenter(undefined, { granularity: "grapheme" })
21
+ : null;
8
22
 
9
23
  export function stripTerminalControls(value: unknown) {
10
24
  return String(value)
@@ -18,3 +32,137 @@ export function stripTerminalControls(value: unknown) {
18
32
  }
19
33
 
20
34
  export const plainText = stripTerminalControls;
35
+
36
+ export function terminalGraphemes(value: string): string[] {
37
+ if (GRAPHEME_SEGMENTER !== null) {
38
+ return Array.from(GRAPHEME_SEGMENTER.segment(value), (part) => part.segment);
39
+ }
40
+
41
+ return Array.from(value);
42
+ }
43
+
44
+ function isWideCodePoint(codePoint: number) {
45
+ return (
46
+ codePoint >= 0x1100 && (
47
+ codePoint <= 0x115f
48
+ || codePoint === 0x2329
49
+ || codePoint === 0x232a
50
+ || (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f)
51
+ || (codePoint >= 0xac00 && codePoint <= 0xd7a3)
52
+ || (codePoint >= 0xf900 && codePoint <= 0xfaff)
53
+ || (codePoint >= 0xfe10 && codePoint <= 0xfe19)
54
+ || (codePoint >= 0xfe30 && codePoint <= 0xfe6f)
55
+ || (codePoint >= 0xff00 && codePoint <= 0xff60)
56
+ || (codePoint >= 0xffe0 && codePoint <= 0xffe6)
57
+ || (codePoint >= 0x20000 && codePoint <= 0x3fffd)
58
+ )
59
+ );
60
+ }
61
+
62
+ function isZeroWidthCodePoint(codePoint: number, char: string) {
63
+ return codePoint === 0x200d
64
+ || (codePoint >= 0xfe00 && codePoint <= 0xfe0f)
65
+ || (codePoint >= 0xe0100 && codePoint <= 0xe01ef)
66
+ || COMBINING_MARK.test(char);
67
+ }
68
+
69
+ function graphemeCellWidth(grapheme: string) {
70
+ if (grapheme.length === 0) {
71
+ return 0;
72
+ }
73
+
74
+ if (grapheme.includes("\u200d") || EMOJI_PRESENTATION.test(grapheme)) {
75
+ return 2;
76
+ }
77
+
78
+ let width = 0;
79
+ for (const char of Array.from(grapheme)) {
80
+ const codePoint = char.codePointAt(0);
81
+ if (typeof codePoint !== "number") {
82
+ continue;
83
+ }
84
+ if (isZeroWidthCodePoint(codePoint, char)) {
85
+ continue;
86
+ }
87
+ width = Math.max(width, isWideCodePoint(codePoint) ? 2 : 1);
88
+ }
89
+
90
+ return width;
91
+ }
92
+
93
+ export function terminalCellWidth(value: unknown) {
94
+ const text = stripTerminalControls(value);
95
+ return terminalGraphemes(text).reduce((width, grapheme) => width + graphemeCellWidth(grapheme), 0);
96
+ }
97
+
98
+ export function sliceTerminalCells(value: string, maxCells: number) {
99
+ const limit = Math.max(0, Math.trunc(Number(maxCells) || 0));
100
+ let width = 0;
101
+ let output = "";
102
+
103
+ for (const grapheme of terminalGraphemes(value)) {
104
+ const graphemeWidth = graphemeCellWidth(grapheme);
105
+ if (graphemeWidth > 0 && width + graphemeWidth > limit) {
106
+ break;
107
+ }
108
+ output += grapheme;
109
+ width += graphemeWidth;
110
+ }
111
+
112
+ return output;
113
+ }
114
+
115
+ export function dropTerminalCells(value: string, cells: number) {
116
+ const limit = Math.max(0, Math.trunc(Number(cells) || 0));
117
+ let width = 0;
118
+ let output = "";
119
+ let dropping = true;
120
+
121
+ for (const grapheme of terminalGraphemes(value)) {
122
+ if (dropping) {
123
+ const graphemeWidth = graphemeCellWidth(grapheme);
124
+ if (width < limit || (graphemeWidth === 0 && width <= limit)) {
125
+ width += graphemeWidth;
126
+ continue;
127
+ }
128
+ dropping = false;
129
+ }
130
+ output += grapheme;
131
+ }
132
+
133
+ return output;
134
+ }
135
+
136
+ export function padEndTerminalCells(value: string, width: number) {
137
+ const size = Math.max(0, Math.trunc(Number(width) || 0));
138
+ const visibleWidth = terminalCellWidth(value);
139
+ return visibleWidth >= size ? value : `${value}${" ".repeat(size - visibleWidth)}`;
140
+ }
141
+
142
+ export function terminalCellToStringIndex(value: string) {
143
+ const indexes: number[] = [0];
144
+ let cellOffset = 0;
145
+ let stringIndex = 0;
146
+
147
+ for (const grapheme of terminalGraphemes(value)) {
148
+ const graphemeWidth = graphemeCellWidth(grapheme);
149
+ const nextStringIndex = stringIndex + grapheme.length;
150
+
151
+ if (graphemeWidth > 0) {
152
+ for (let cell = 1; cell < graphemeWidth; cell += 1) {
153
+ indexes[cellOffset + cell] = nextStringIndex;
154
+ }
155
+ cellOffset += graphemeWidth;
156
+ indexes[cellOffset] = nextStringIndex;
157
+ }
158
+
159
+ stringIndex = nextStringIndex;
160
+ }
161
+
162
+ indexes[cellOffset] = stringIndex;
163
+ return indexes;
164
+ }
165
+
166
+ export function cursorCellOffset(value: string, cursor: number) {
167
+ return terminalCellWidth(value.slice(0, Math.max(0, Math.trunc(Number(cursor) || 0))));
168
+ }
package/src/theme.ts CHANGED
@@ -101,6 +101,9 @@ export const defaultTerminalTheme: TerminalTheme = {
101
101
  "list.current": {
102
102
  plainPrefix: "> "
103
103
  },
104
+ "list.selected": {
105
+ plainPrefix: "* "
106
+ },
104
107
  focus: {
105
108
  background: "#1f2328"
106
109
  },
package/src/tree.ts CHANGED
@@ -54,12 +54,27 @@ export function resolveToTerminalNodes(input: any): TerminalNode[] {
54
54
  return resolveToTerminalNodes(resolveComponent(input.tag, input.props || {}, input.children || []));
55
55
  }
56
56
 
57
+ const tag = input.tag as TerminalPrimitiveTag;
58
+ const props = { ...(input.props || {}) } as Record<string, any>;
59
+ const vnodeChildren = input.children || [];
60
+ if (tag === "terminal-list" && vnodeChildren.length === 1 && typeof vnodeChildren[0] === "function") {
61
+ props.__childrenRenderer = vnodeChildren[0];
62
+ return [
63
+ {
64
+ type: "element",
65
+ tag,
66
+ props,
67
+ children: []
68
+ }
69
+ ];
70
+ }
71
+
57
72
  return [
58
73
  {
59
74
  type: "element",
60
- tag: input.tag as TerminalPrimitiveTag,
61
- props: (input.props || {}) as Record<string, any>,
62
- children: normalizeChildren(input.children || [])
75
+ tag,
76
+ props,
77
+ children: normalizeChildren(vnodeChildren)
63
78
  }
64
79
  ];
65
80
  }
@@ -106,7 +121,7 @@ export function collectDirectOverlayFocusableNodes(nodes: TerminalNode[], out: T
106
121
 
107
122
  const overlayChildren = node.children.filter((child) => child.type === "element" && child.tag === "terminal-overlay" && child.props.trapFocus !== false);
108
123
  if (overlayChildren.length) {
109
- collectFocusableNodes(overlayChildren, out);
124
+ collectFocusableNodes(overlayChildren.slice().reverse(), out);
110
125
  continue;
111
126
  }
112
127
 
package/src/types.ts CHANGED
@@ -28,6 +28,10 @@ export type TerminalCommandId =
28
28
  | "button.press"
29
29
  | "list.prev"
30
30
  | "list.next"
31
+ | "list.pageUp"
32
+ | "list.pageDown"
33
+ | "list.home"
34
+ | "list.end"
31
35
  | "list.press"
32
36
  | "scroll.up"
33
37
  | "scroll.down"
@@ -125,8 +129,13 @@ export interface TerminalHitbox {
125
129
  y2: number;
126
130
  textStartX?: number;
127
131
  textLength?: number;
132
+ textCellToStringIndex?: number[];
128
133
  itemOffset?: number;
134
+ itemIndexes?: number[];
135
+ contentY?: number;
129
136
  pointerLayer?: number;
137
+ __listItemIndex?: number;
138
+ __pressHandler?: (event: TerminalButtonPressEventPayload) => void;
130
139
  }
131
140
 
132
141
  export interface CursorPosition {
@@ -290,14 +299,29 @@ export interface TerminalButtonPressEventPayload extends TerminalEventPayloadBas
290
299
  type: "press" | "doublepress" | "contextpress";
291
300
  }
292
301
 
293
- export interface TerminalListChangeEventPayload<T = any> extends TerminalValueEventPayloadBase<T> {
302
+ export interface TerminalListStateEventPayload {
303
+ activeIndex: number;
304
+ selectedIndex: number | null;
305
+ viewportOffset: number;
306
+ viewportRows: number;
307
+ }
308
+
309
+ export interface TerminalListChangeEventPayload<T = any> extends TerminalValueEventPayloadBase<T>, TerminalListStateEventPayload {
294
310
  type: "change";
295
311
  index: number;
312
+ key?: string;
296
313
  }
297
314
 
298
- export interface TerminalListPressEventPayload<T = any> extends TerminalValueEventPayloadBase<T> {
315
+ export interface TerminalListViewportChangeEventPayload extends TerminalEventPayloadBase, TerminalListStateEventPayload {
316
+ type: "viewportchange";
317
+ offset: number;
318
+ rows: number;
319
+ }
320
+
321
+ export interface TerminalListPressEventPayload<T = any> extends TerminalValueEventPayloadBase<T>, TerminalListStateEventPayload {
299
322
  type: "press" | "doublepress" | "contextpress";
300
323
  index: number;
324
+ key?: string;
301
325
  }
302
326
 
303
327
  export type TerminalChangeEventPayload =
@@ -334,6 +358,7 @@ export interface TerminalRowPointerEventPayloadBase extends TerminalPointerCoord
334
358
  export interface TerminalListPointerEventPayload<T = any> extends TerminalRowPointerEventPayloadBase {
335
359
  type: TerminalMouseEventType;
336
360
  index: number;
361
+ key?: string;
337
362
  value: T;
338
363
  }
339
364
 
@@ -363,7 +388,9 @@ export interface TerminalInputProps extends TerminalFocusableProps, TerminalStyl
363
388
  export interface TerminalEditorProps extends TerminalFocusableProps, TerminalStyleProps {
364
389
  value?: string;
365
390
  placeholder?: string;
391
+ width?: number;
366
392
  height?: number;
393
+ fill?: boolean;
367
394
  onchange?(event: TerminalEditorChangeEventPayload): void;
368
395
  oninput?(event: TerminalEditorChangeEventPayload): void;
369
396
  onsubmit?(event: TerminalEditorSubmitEventPayload): void;
@@ -377,13 +404,31 @@ export interface TerminalButtonProps extends TerminalFocusableProps, TerminalSty
377
404
  oncontextpress?(event: TerminalButtonPressEventPayload): void;
378
405
  }
379
406
 
407
+ export interface TerminalListRenderContext<T = any> {
408
+ index: number;
409
+ key: string;
410
+ active: boolean;
411
+ selected: boolean;
412
+ viewportIndex: number;
413
+ item: T;
414
+ }
415
+
416
+ export type TerminalListItemKey<T = any> = (item: T, index: number) => string | number;
417
+ export type TerminalListItemRenderer<T = any> = (item: T, ctx: TerminalListRenderContext<T>) => any;
418
+
380
419
  export interface TerminalListProps<T = any> extends TerminalFocusableProps, TerminalPointerCaptureProps, TerminalStyleProps {
381
420
  items?: readonly T[];
382
421
  virtualized?: boolean;
422
+ height?: number;
383
423
  itemHeight?: 1;
384
424
  overscan?: number;
385
- renderItem?(item: T, index: number): string;
425
+ wrap?: boolean;
426
+ itemKey?: TerminalListItemKey<T>;
427
+ showActive?: boolean;
428
+ renderItem?(item: T, index: number): any;
429
+ children?: TerminalListItemRenderer<T>;
386
430
  onchange?(event: TerminalListChangeEventPayload<T>): void;
431
+ onviewportchange?(event: TerminalListViewportChangeEventPayload): void;
387
432
  onpress?(event: TerminalListPressEventPayload<T>): void;
388
433
  ondoublepress?(event: TerminalListPressEventPayload<T>): void;
389
434
  oncontextpress?(event: TerminalListPressEventPayload<T>): void;