@alchemy.run/sigil 0.0.0-alpha.3 → 0.0.0-alpha.5

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 (207) hide show
  1. package/README.md +23 -13
  2. package/THIRD_PARTY_NOTICES.md +70 -23
  3. package/dist/Text-D5HUf3Fj.d.ts +452 -0
  4. package/dist/ansi.d.ts +137 -143
  5. package/dist/ansi.js +87 -2
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
  11. package/dist/color-policy-SVj1pYTA.js +560 -0
  12. package/dist/color-profile-DHhQHY55.js +36 -0
  13. package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-Bh4yGP6w.d.ts +186 -0
  18. package/dist/detect-BuTXtY6e.js +373 -0
  19. package/dist/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-Bmc2tRPk.d.ts +21 -0
  24. package/dist/index.d.ts +235 -754
  25. package/dist/index.js +1362 -3001
  26. package/dist/osc-CCH7xDoS.js +71 -0
  27. package/dist/osc-Cn0fw77g.d.ts +23 -0
  28. package/dist/paint-C19minOS.d.ts +81 -0
  29. package/dist/query-vaIeGOkH.d.ts +152 -0
  30. package/dist/router.d.ts +392 -0
  31. package/dist/router.js +709 -0
  32. package/dist/sample-Cqw1bjUL.js +445 -0
  33. package/dist/screen-BOh__-65.js +324 -0
  34. package/dist/screen-BReKIheE.d.ts +40 -0
  35. package/dist/screen.d.ts +5 -0
  36. package/dist/screen.js +5 -0
  37. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  38. package/dist/serialize-BTkAZgw1.js +79 -0
  39. package/dist/session-BJmzX2NJ.js +664 -0
  40. package/dist/sgr-BhwaWAJB.js +246 -0
  41. package/dist/store-CgrG9K4y.d.ts +72 -0
  42. package/dist/string-width-CijQwpIk.js +69 -0
  43. package/dist/strip-BvU4toXG.js +6 -0
  44. package/dist/terminal.d.ts +118 -0
  45. package/dist/terminal.js +2 -0
  46. package/dist/tokenize-AjqbvtiT.js +1242 -0
  47. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  48. package/dist/truncate-D31fhU6i.js +562 -0
  49. package/dist/use-focus-C2sciZOo.js +1335 -0
  50. package/package.json +43 -17
  51. package/src/ansi/chalk.ts +12 -53
  52. package/src/ansi/cursor.ts +2 -4
  53. package/src/ansi/east-asian-width.ts +44 -0
  54. package/src/ansi/escapes.ts +18 -26
  55. package/src/ansi/graphemes.ts +8 -0
  56. package/src/ansi/hyperlink.ts +44 -0
  57. package/src/ansi/index.ts +14 -14
  58. package/src/ansi/osc.ts +77 -0
  59. package/src/ansi/sgr.ts +2 -5
  60. package/src/ansi/slice.ts +1 -1
  61. package/src/ansi/string-width.ts +91 -206
  62. package/src/ansi/strip.ts +13 -33
  63. package/src/ansi/tokenize.ts +96 -169
  64. package/src/ansi/truncate.ts +5 -3
  65. package/src/ansi/wrap.ts +13 -14
  66. package/src/ansi-tokenizer.ts +1 -1
  67. package/src/capabilities/color-policy.ts +34 -0
  68. package/src/capabilities/detect.ts +594 -0
  69. package/src/capabilities/index.ts +37 -0
  70. package/src/capabilities/query.ts +657 -0
  71. package/src/capabilities/store.ts +379 -0
  72. package/src/color/index.ts +3 -0
  73. package/src/color/paint.ts +169 -0
  74. package/src/color/palette.ts +48 -0
  75. package/src/color/sample.ts +323 -0
  76. package/src/color.ts +1 -0
  77. package/src/components/AnsiText.tsx +42 -0
  78. package/src/components/App.tsx +109 -22
  79. package/src/components/BackgroundContext.ts +2 -3
  80. package/src/components/Box.tsx +43 -60
  81. package/src/components/CursorContext.ts +1 -1
  82. package/src/components/ErrorBoundary.tsx +1 -1
  83. package/src/components/ErrorOverview.tsx +8 -7
  84. package/src/components/Hyperlink.tsx +56 -0
  85. package/src/components/Spacer.tsx +1 -1
  86. package/src/components/Static.tsx +1 -1
  87. package/src/components/StderrContext.ts +0 -2
  88. package/src/components/StdinContext.ts +0 -1
  89. package/src/components/StdoutContext.ts +1 -3
  90. package/src/components/TerminalOscContext.ts +25 -0
  91. package/src/components/Text.tsx +23 -46
  92. package/src/components/Transform.tsx +2 -2
  93. package/src/cursor-position.ts +1 -1
  94. package/src/devtools.ts +84 -24
  95. package/src/dom.ts +11 -8
  96. package/src/env.ts +12 -0
  97. package/src/get-max-width.ts +1 -1
  98. package/src/global.d.ts +6 -4
  99. package/src/{boxes.ts → glyphs.ts} +17 -18
  100. package/src/hooks/use-animation.ts +1 -1
  101. package/src/hooks/use-app.ts +1 -1
  102. package/src/hooks/use-box-metrics.ts +1 -1
  103. package/src/hooks/use-capabilities.ts +73 -0
  104. package/src/hooks/use-cursor.ts +3 -3
  105. package/src/hooks/use-focus-manager.ts +1 -1
  106. package/src/hooks/use-focus.ts +5 -6
  107. package/src/hooks/use-input.ts +4 -4
  108. package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
  109. package/src/hooks/use-paste.ts +2 -2
  110. package/src/hooks/use-stderr.ts +1 -1
  111. package/src/hooks/use-stdin.ts +1 -1
  112. package/src/hooks/use-stdout.ts +1 -1
  113. package/src/hooks/use-terminal-osc.ts +59 -0
  114. package/src/hooks/use-window-size.ts +2 -2
  115. package/src/index.ts +88 -44
  116. package/src/ink.tsx +620 -745
  117. package/src/input-parser.ts +1 -1
  118. package/src/instances.ts +1 -1
  119. package/src/kitty-keyboard.ts +128 -0
  120. package/src/measure-element.ts +1 -1
  121. package/src/measure-text.ts +2 -2
  122. package/src/{render-node-to-output.ts → paint-tree.ts} +78 -53
  123. package/src/parse-keypress.ts +4 -5
  124. package/src/parse-stack-line.ts +0 -1
  125. package/src/patch-console.ts +44 -0
  126. package/src/reconciler.ts +36 -62
  127. package/src/render-background.ts +37 -16
  128. package/src/render-border.ts +97 -64
  129. package/src/render-frame.ts +83 -0
  130. package/src/render-to-string.ts +24 -9
  131. package/src/render.ts +24 -16
  132. package/src/router/components.tsx +343 -0
  133. package/src/router/context.ts +41 -0
  134. package/src/router/history.ts +194 -0
  135. package/src/router/hooks.tsx +391 -0
  136. package/src/router/index.ts +34 -0
  137. package/src/router/matcher.ts +571 -0
  138. package/src/sanitize-ansi.ts +1 -1
  139. package/src/screen/ansi.ts +184 -0
  140. package/src/screen/canvas.ts +137 -0
  141. package/src/screen/cell.ts +138 -0
  142. package/src/screen/color-profile.ts +47 -0
  143. package/src/screen/geometry.ts +9 -0
  144. package/src/screen/index.ts +6 -0
  145. package/src/screen/screen.ts +272 -0
  146. package/src/screen/serialize.ts +129 -0
  147. package/src/screen.ts +1 -0
  148. package/src/semantic-text-style.ts +118 -0
  149. package/src/signal-exit.ts +4 -5
  150. package/src/squash-text-nodes.ts +4 -7
  151. package/src/stream.ts +2 -4
  152. package/src/structured-text.ts +321 -0
  153. package/src/styles.ts +16 -17
  154. package/src/terminal/index.ts +2 -0
  155. package/src/terminal/inline-presenter.ts +120 -0
  156. package/src/terminal/input.ts +86 -0
  157. package/src/terminal/render-scheduler.ts +37 -0
  158. package/src/terminal/screen-presenter.ts +188 -0
  159. package/src/terminal/session.ts +407 -0
  160. package/src/terminal-size.ts +9 -8
  161. package/src/terminal.ts +1 -0
  162. package/src/testing/browser.ts +588 -0
  163. package/src/testing/emulators.ts +205 -0
  164. package/src/testing/explorer-app/index.html +12 -0
  165. package/src/testing/explorer-app/main.ts +381 -0
  166. package/src/testing/explorer-app/style.css +194 -0
  167. package/src/testing/explorer-app/tsconfig.json +15 -0
  168. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  169. package/src/testing/index.ts +26 -0
  170. package/src/testing/keys.ts +56 -0
  171. package/src/testing/live.ts +85 -0
  172. package/src/testing/matchers.ts +70 -0
  173. package/src/testing/public.ts +94 -0
  174. package/src/testing/terminal.ts +349 -0
  175. package/src/testing/vitest.ts +157 -0
  176. package/src/transform-adapter.ts +14 -0
  177. package/src/types.ts +1 -6
  178. package/src/utils.ts +2 -2
  179. package/src/wrap-text.ts +4 -4
  180. package/src/yoga/config.ts +2 -2
  181. package/src/yoga/core/absoluteLayout.ts +15 -15
  182. package/src/yoga/core/baseline.ts +3 -3
  183. package/src/yoga/core/cache.ts +4 -4
  184. package/src/yoga/core/calculateLayout.ts +33 -27
  185. package/src/yoga/core/config.ts +1 -1
  186. package/src/yoga/core/flexLine.ts +3 -3
  187. package/src/yoga/core/helpers.ts +3 -3
  188. package/src/yoga/core/layoutResults.ts +4 -4
  189. package/src/yoga/core/node.ts +16 -16
  190. package/src/yoga/core/pixelGrid.ts +4 -4
  191. package/src/yoga/core/style.ts +11 -11
  192. package/src/yoga/core/types.ts +2 -2
  193. package/src/yoga/index.ts +6 -6
  194. package/src/yoga/node.ts +8 -8
  195. package/dist/sgr-CMfEpjSk.d.ts +0 -91
  196. package/dist/truncate-CBiyyZzw.js +0 -2156
  197. package/src/ansi/supports-color.ts +0 -213
  198. package/src/ansi/widest-line.ts +0 -12
  199. package/src/auto-bind.ts +0 -41
  200. package/src/colorize.ts +0 -60
  201. package/src/devtools-window-polyfill.ts +0 -73
  202. package/src/indent-string.ts +0 -16
  203. package/src/is-in-ci.ts +0 -7
  204. package/src/log-update.ts +0 -370
  205. package/src/output.ts +0 -308
  206. package/src/renderer.ts +0 -73
  207. package/src/write-synchronized.ts +0 -9
@@ -1,4 +1,4 @@
1
- import { ESC, pasteStart, pasteEnd } from "./ansi/escapes.ts";
1
+ import { ESC, pasteStart, pasteEnd } from "#/ansi/escapes.ts";
2
2
 
3
3
  const escape = ESC;
4
4
 
package/src/instances.ts CHANGED
@@ -4,6 +4,6 @@
4
4
  // This map has to be stored in a separate file, because render.js creates instances,
5
5
  // but instance.js should delete itself from the map on unmount
6
6
 
7
- import type { Ink } from "./ink.tsx";
7
+ import type { Ink } from "#/ink.tsx";
8
8
 
9
9
  export const instances = new WeakMap<NodeJS.WritableStream, Ink>();
@@ -1,3 +1,5 @@
1
+ import { kittyQuery } from "#/ansi/escapes.ts";
2
+
1
3
  // Kitty keyboard protocol flags.
2
4
  // @see https://sw.kovidgoyal.net/kitty/keyboard-protocol/
3
5
  export const kittyFlags = {
@@ -55,3 +57,129 @@ export type KittyKeyboardOptions = {
55
57
  // - 'reportAssociatedText' - Report associated text with key events
56
58
  flags?: KittyFlagName[];
57
59
  };
60
+
61
+ const textEncoder = new TextEncoder();
62
+
63
+ const kittyQueryEscapeByte = 0x1b;
64
+ const kittyQueryOpenBracketByte = 0x5b;
65
+ const kittyQueryQuestionMarkByte = 0x3f;
66
+ const kittyQueryLetterByte = 0x75;
67
+ const zeroByte = 0x30;
68
+ const nineByte = 0x39;
69
+
70
+ type KittyQueryResponseMatch = { state: "complete"; endIndex: number } | { state: "partial" };
71
+
72
+ const isDigitByte = (byte: number): boolean => byte >= zeroByte && byte <= nineByte;
73
+
74
+ const matchKittyQueryResponse = (
75
+ buffer: number[],
76
+ startIndex: number,
77
+ ): KittyQueryResponseMatch | undefined => {
78
+ if (
79
+ buffer[startIndex] !== kittyQueryEscapeByte ||
80
+ buffer[startIndex + 1] !== kittyQueryOpenBracketByte ||
81
+ buffer[startIndex + 2] !== kittyQueryQuestionMarkByte
82
+ ) {
83
+ return;
84
+ }
85
+
86
+ let index = startIndex + 3;
87
+ const digitsStartIndex = index;
88
+ while (index < buffer.length && isDigitByte(buffer[index]!)) {
89
+ index++;
90
+ }
91
+
92
+ if (index === digitsStartIndex) {
93
+ return;
94
+ }
95
+
96
+ if (index === buffer.length) {
97
+ return { state: "partial" };
98
+ }
99
+
100
+ if (buffer[index] === kittyQueryLetterByte) {
101
+ return { state: "complete", endIndex: index };
102
+ }
103
+
104
+ return;
105
+ };
106
+
107
+ const hasCompleteKittyQueryResponse = (buffer: number[]): boolean => {
108
+ for (let index = 0; index < buffer.length; index++) {
109
+ const match = matchKittyQueryResponse(buffer, index);
110
+ if (match?.state === "complete") {
111
+ return true;
112
+ }
113
+ }
114
+
115
+ return false;
116
+ };
117
+
118
+ const stripKittyQueryResponsesAndTrailingPartial = (buffer: number[]): number[] => {
119
+ const keptBytes: number[] = [];
120
+ let index = 0;
121
+ while (index < buffer.length) {
122
+ const match = matchKittyQueryResponse(buffer, index);
123
+ if (match?.state === "complete") {
124
+ index = match.endIndex + 1;
125
+ continue;
126
+ }
127
+
128
+ if (match?.state === "partial") {
129
+ break;
130
+ }
131
+
132
+ keptBytes.push(buffer[index]!);
133
+ index++;
134
+ }
135
+
136
+ return keptBytes;
137
+ };
138
+
139
+ // Query the terminal for kitty keyboard protocol support. The CSI ? u query is
140
+ // safe to send to any terminal — unsupporting terminals simply won't respond,
141
+ // and the 200ms timeout handles that. Stdin bytes that aren't part of the
142
+ // protocol response are re-emitted so they aren't lost from the caller's input
143
+ // pipeline. Returns a cancel function; cancelling (or the timeout firing)
144
+ // means `onSupported` never runs.
145
+ export const detectKittySupport = (
146
+ stdin: NodeJS.ReadableStream,
147
+ stdout: { write: (data: string) => unknown },
148
+ onSupported: () => void,
149
+ ): (() => void) => {
150
+ let responseBuffer: number[] = [];
151
+
152
+ const cleanup = (): void => {
153
+ clearTimeout(timer);
154
+ stdin.removeListener("data", onData);
155
+
156
+ // Re-emit any buffered data that wasn't the protocol response.
157
+ // Clear responseBuffer afterwards to make cleanup idempotent.
158
+ const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
159
+ responseBuffer = [];
160
+ if (remaining.length > 0) {
161
+ stdin.unshift(Uint8Array.from(remaining));
162
+ }
163
+ };
164
+
165
+ const onData = (data: Uint8Array | string): void => {
166
+ const chunk = typeof data === "string" ? textEncoder.encode(data) : data;
167
+ for (const byte of chunk) {
168
+ responseBuffer.push(byte);
169
+ }
170
+
171
+ if (hasCompleteKittyQueryResponse(responseBuffer)) {
172
+ cleanup();
173
+ onSupported();
174
+ }
175
+ };
176
+
177
+ // Attach listener before writing the query so that synchronous
178
+ // or immediate responses are not missed.
179
+ stdin.on("data", onData);
180
+ const timer = setTimeout(cleanup, 200);
181
+
182
+ stdout.write(kittyQuery);
183
+
184
+ return cleanup;
185
+ };
@@ -1,4 +1,4 @@
1
- import { type DOMElement } from "./dom.ts";
1
+ import { type DOMElement } from "#/dom.ts";
2
2
 
3
3
  type Output = {
4
4
  /**
@@ -1,5 +1,5 @@
1
- import { widestLine } from "./ansi/widest-line.ts";
2
- import { QuickLru as QuickLRU } from "./quick-lru.ts";
1
+ import { widestLine } from "#/ansi/string-width.ts";
2
+ import { QuickLru as QuickLRU } from "#/quick-lru.ts";
3
3
 
4
4
  const cache = new QuickLRU<string, Output>({ maxSize: 4096 });
5
5
 
@@ -1,35 +1,21 @@
1
- import { widestLine } from "./ansi/widest-line.ts";
2
- import { type DOMElement } from "./dom.ts";
3
- import { getMaxWidth } from "./get-max-width.ts";
4
- import { indentString } from "./indent-string.ts";
5
- import type { Output } from "./output.ts";
6
- import { renderBackground } from "./render-background.ts";
7
- import { renderBorder } from "./render-border.ts";
8
- import { squashTextNodes } from "./squash-text-nodes.ts";
9
- import { wrapText } from "./wrap-text.ts";
10
- import { Yoga } from "./yoga/index.ts";
11
-
12
- // If parent container is `<Box>`, text nodes will be treated as separate nodes in
13
- // the tree and will have their own coordinates in the layout.
14
- // To ensure text nodes are aligned correctly, take X and Y of the first text node
15
- // and use it as offset for the rest of the nodes
16
- // Only first node is taken into account, because other text nodes can't have margin or padding,
17
- // so their coordinates will be relative to the first node anyway
18
- const applyPaddingToText = (node: DOMElement, text: string): string => {
19
- const yogaNode = node.childNodes[0]?.yogaNode;
20
-
21
- if (yogaNode) {
22
- const offsetX = yogaNode.getComputedLeft();
23
- const offsetY = yogaNode.getComputedTop();
24
- text = "\n".repeat(offsetY) + indentString(text, offsetX);
25
- }
26
-
27
- return text;
28
- };
29
-
30
- export type OutputTransformer = (s: string, index: number) => string;
31
-
32
- export const renderNodeToScreenReaderOutput = (
1
+ import { type DOMElement } from "#/dom.ts";
2
+ import { getMaxWidth } from "#/get-max-width.ts";
3
+ import { renderBackground } from "#/render-background.ts";
4
+ import { renderBorder } from "#/render-border.ts";
5
+ import type { Canvas } from "#/screen/canvas.ts";
6
+ import { serializeLine } from "#/screen/serialize.ts";
7
+ import { squashTextNodes } from "#/squash-text-nodes.ts";
8
+ import {
9
+ hasCompatibilityText,
10
+ structuredTextBaseStyle,
11
+ structuredTextLines,
12
+ wrapStructuredText,
13
+ sampleStructuredText,
14
+ } from "#/structured-text.ts";
15
+ import type { AnsiTransformer } from "#/transform-adapter.ts";
16
+ import { Yoga } from "#/yoga/index.ts";
17
+
18
+ export const renderAccessibleText = (
33
19
  node: DOMElement,
34
20
  options: {
35
21
  parentRole?: string;
@@ -59,7 +45,7 @@ export const renderNodeToScreenReaderOutput = (
59
45
 
60
46
  output = childNodes
61
47
  .map((childNode) => {
62
- const screenReaderOutput = renderNodeToScreenReaderOutput(childNode as DOMElement, {
48
+ const screenReaderOutput = renderAccessibleText(childNode as DOMElement, {
63
49
  parentRole: node.internal_accessibility?.role,
64
50
  skipStaticElements: options.skipStaticElements,
65
51
  });
@@ -90,13 +76,13 @@ export const renderNodeToScreenReaderOutput = (
90
76
  };
91
77
 
92
78
  // After nodes are laid out, render each to output object, which later gets rendered to terminal
93
- export const renderNodeToOutput = (
79
+ export const paintTree = (
94
80
  node: DOMElement,
95
- output: Output,
81
+ output: Canvas,
96
82
  options: {
97
83
  offsetX?: number;
98
84
  offsetY?: number;
99
- transformers?: OutputTransformer[];
85
+ transformers?: AnsiTransformer[];
100
86
  skipStaticElements: boolean;
101
87
  },
102
88
  ) => {
@@ -118,28 +104,54 @@ export const renderNodeToOutput = (
118
104
  const y = offsetY + yogaNode.getComputedTop();
119
105
 
120
106
  // Transformers are functions that transform final text output of each component
121
- // See Output class for logic that applies transformers
122
- let newTransformers = transformers;
123
-
124
- if (typeof node.internal_transform === "function") {
125
- newTransformers = [node.internal_transform, ...transformers];
126
- }
107
+ // The canvas applies explicit compatibility transformers at this boundary.
108
+ const newTransformers = node.internal_transform
109
+ ? [node.internal_transform, ...transformers]
110
+ : transformers;
127
111
 
128
112
  if (node.nodeName === "ink-text") {
129
- let text = squashTextNodes(node);
113
+ const text = squashTextNodes(node);
130
114
 
131
115
  if (text.length > 0) {
132
- const currentWidth = widestLine(text);
133
116
  const maxWidth = getMaxWidth(yogaNode);
134
-
135
- if (currentWidth > maxWidth) {
136
- const textWrap = node.style.textWrap ?? "wrap";
137
- text = wrapText(text, maxWidth, textWrap);
117
+ const firstChildYoga = node.childNodes[0]?.yogaNode;
118
+ const paddingX = firstChildYoga?.getComputedLeft() ?? 0;
119
+ const paddingY = firstChildYoga?.getComputedTop() ?? 0;
120
+ const lines = wrapStructuredText(
121
+ structuredTextLines(node),
122
+ maxWidth,
123
+ node.style.textWrap ?? "wrap",
124
+ structuredTextBaseStyle(node),
125
+ );
126
+ const paintBounds = {
127
+ x: x + paddingX,
128
+ y: y + paddingY,
129
+ width: Math.max(
130
+ 1,
131
+ ...lines.map((line) => line.reduce((width, cell) => width + cell.width, 0)),
132
+ ),
133
+ height: lines.length,
134
+ };
135
+ const sampled = sampleStructuredText(lines, paintBounds, output.paintContext);
136
+
137
+ if (hasCompatibilityText(node)) {
138
+ const textTransformers = collectTransformers(node, transformers);
139
+ output.writeAnsi(
140
+ paintBounds.x,
141
+ paintBounds.y,
142
+ sampled
143
+ .map((line) =>
144
+ serializeLine(line, {
145
+ colorProfile: output.paintContext.profile ?? "truecolor",
146
+ trimEnd: false,
147
+ }),
148
+ )
149
+ .join("\n"),
150
+ { transformers: textTransformers },
151
+ );
152
+ } else {
153
+ output.writeCells(paintBounds.x, paintBounds.y, sampled);
138
154
  }
139
-
140
- text = applyPaddingToText(node, text);
141
-
142
- output.write(x, y, text, { transformers: newTransformers });
143
155
  }
144
156
 
145
157
  return;
@@ -175,7 +187,7 @@ export const renderNodeToOutput = (
175
187
 
176
188
  if (node.nodeName === "ink-root" || node.nodeName === "ink-box") {
177
189
  for (const childNode of node.childNodes) {
178
- renderNodeToOutput(childNode as DOMElement, output, {
190
+ paintTree(childNode as DOMElement, output, {
179
191
  offsetX: x,
180
192
  offsetY: y,
181
193
  transformers: newTransformers,
@@ -189,3 +201,16 @@ export const renderNodeToOutput = (
189
201
  }
190
202
  }
191
203
  };
204
+
205
+ function collectTransformers(
206
+ node: DOMElement,
207
+ inherited: readonly AnsiTransformer[],
208
+ ): AnsiTransformer[] {
209
+ return [
210
+ ...node.childNodes.flatMap((child) =>
211
+ child.nodeName === "#text" ? [] : collectTransformers(child, []),
212
+ ),
213
+ ...(node.internal_transform ? [node.internal_transform] : []),
214
+ ...inherited,
215
+ ];
216
+ }
@@ -1,6 +1,6 @@
1
1
  // Copied from https://github.com/enquirer/enquirer/blob/36785f3399a41cd61e9d28d1eb9c2fcd73d69b4c/lib/keypress.js
2
- import { DEL, ESC } from "./ansi/escapes.ts";
3
- import { kittyModifiers } from "./kitty-keyboard.ts";
2
+ import { DEL, ESC } from "#/ansi/escapes.ts";
3
+ import { kittyModifiers } from "#/kitty-keyboard.ts";
4
4
 
5
5
  const textDecoder = new TextDecoder();
6
6
 
@@ -396,8 +396,7 @@ export const parseKeypress = (s: Uint8Array | string = ""): ParsedKey => {
396
396
 
397
397
  if (s instanceof Uint8Array) {
398
398
  if (s[0]! > 127 && s[1] === undefined) {
399
- (s[0] as unknown as number) -= 128;
400
- s = ESC + textDecoder.decode(s);
399
+ s = ESC + textDecoder.decode(Uint8Array.of(s[0]! - 128));
401
400
  } else {
402
401
  s = textDecoder.decode(s);
403
402
  }
@@ -499,7 +498,7 @@ export const parseKeypress = (s: Uint8Array | string = ""): ParsedKey => {
499
498
  // the modifier key bitflag and any meaningless "1;" sequence
500
499
  const code = [parts[1], parts[2], parts[4], parts[6]].filter(Boolean).join("");
501
500
 
502
- const modifier = ((parts[3] || parts[5] || 1) as number) - 1;
501
+ const modifier = Number(parts[3] || parts[5] || 1) - 1;
503
502
 
504
503
  // Parse the key modifier
505
504
  key.ctrl = !!(modifier & 4);
@@ -1,6 +1,5 @@
1
1
  // Derived from `stack-utils` (MIT, Isaac Z. Schlueter, James Talmage),
2
2
  // reduced to the `parseLine()` surface Ink's error overview uses.
3
- import process from "node:process";
4
3
 
5
4
  export type ParsedStackLine = {
6
5
  line?: number;
@@ -60,3 +60,47 @@ export const patchConsole = (callback: Callback): Restore => {
60
60
  originalMethods.clear();
61
61
  };
62
62
  };
63
+
64
+ // Intercept direct `write` calls on a stream. The write is swallowed and the
65
+ // decoded chunk is handed to `onData`; write callbacks still fire so callers
66
+ // awaiting flushes don't hang.
67
+ export const patchStreamWrite = (
68
+ stream: NodeJS.WritableStream,
69
+ onData: (data: string) => void,
70
+ ): Restore => {
71
+ // Keep the unbound reference: restore below must reassign the exact
72
+ // original function object, not a bound copy.
73
+ // oxlint-disable-next-line typescript/unbound-method
74
+ const originalWrite = stream.write;
75
+
76
+ const patchedWrite = (
77
+ chunk: unknown,
78
+ encodingOrCallback?: unknown,
79
+ callback?: unknown,
80
+ ): boolean => {
81
+ const data =
82
+ typeof chunk === "string"
83
+ ? chunk
84
+ : chunk instanceof Uint8Array
85
+ ? Buffer.from(chunk).toString()
86
+ : String(chunk);
87
+
88
+ onData(data);
89
+
90
+ const done =
91
+ typeof encodingOrCallback === "function"
92
+ ? encodingOrCallback
93
+ : typeof callback === "function"
94
+ ? callback
95
+ : undefined;
96
+ done?.();
97
+
98
+ return true;
99
+ };
100
+
101
+ stream.write = patchedWrite;
102
+
103
+ return () => {
104
+ stream.write = originalWrite;
105
+ };
106
+ };
package/src/reconciler.ts CHANGED
@@ -1,6 +1,4 @@
1
- import process from "node:process";
2
-
3
- import { createContext, version as reactVersion } from "react";
1
+ import { createContext } from "react";
4
2
  import createReconciler, { type ReactContext } from "react-reconciler";
5
3
  import { DefaultEventPriority, NoEventPriority } from "react-reconciler/constants.js";
6
4
  import * as Scheduler from "scheduler";
@@ -20,27 +18,23 @@ import {
20
18
  type TextNode,
21
19
  type ElementNames,
22
20
  type DOMElement,
23
- } from "./dom.ts";
24
- import { type OutputTransformer } from "./render-node-to-output.ts";
25
- import { styles as applyStyles, type Styles } from "./styles.ts";
26
- import { Yoga } from "./yoga/index.ts";
21
+ } from "#/dom.ts";
22
+ import { isSigilDev } from "#/env.ts";
23
+ import type { SemanticTextStyle } from "#/semantic-text-style.ts";
24
+ import { styles as applyStyles, type Styles } from "#/styles.ts";
25
+ import { type AnsiTransformer } from "#/transform-adapter.ts";
26
+ import { Yoga } from "#/yoga/index.ts";
27
+
28
+ import pkg from "../package.json" with { type: "json" };
27
29
 
28
30
  // We need to conditionally perform devtools connection to avoid
29
31
  // accidentally breaking other third-party code.
30
32
  // See https://github.com/vadimdemedes/ink/issues/384
31
33
  // See https://github.com/vadimdemedes/ink/issues/648
32
- if (process.env["SIGIL_DEV"] === "true") {
34
+ if (isSigilDev) {
33
35
  // Intentionally no warning when the package is missing.
34
36
  // SIGIL_DEV may be set for other reasons; devtools is opt-in via installing the package.
35
- let isDevtoolsInstalled = false;
36
- try {
37
- import.meta.resolve("react-devtools-core");
38
- isDevtoolsInstalled = true;
39
- } catch {}
40
-
41
- if (isDevtoolsInstalled) {
42
- await import("./devtools.ts");
43
- }
37
+ await import("#/devtools.ts").catch(() => {});
44
38
  }
45
39
 
46
40
  type AnyObject = Record<string, unknown>;
@@ -138,46 +132,6 @@ type HostContext = {
138
132
 
139
133
  let currentUpdatePriority = NoEventPriority;
140
134
 
141
- async function loadPackageJson() {
142
- const fs = await import("node:fs");
143
- const content = fs.readFileSync(new URL("../package.json", import.meta.url), "utf8");
144
-
145
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion
146
- const parsedContent = JSON.parse(content) as
147
- | {
148
- name?: string;
149
- version?: string;
150
- }
151
- | undefined;
152
-
153
- return {
154
- name: parsedContent?.name,
155
- version: parsedContent?.version,
156
- };
157
- }
158
-
159
- let packageInfo = {
160
- name: "ink",
161
- version: reactVersion,
162
- };
163
-
164
- if (process.env["DEV"] === "true") {
165
- try {
166
- const loaded = await loadPackageJson();
167
- packageInfo = {
168
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
169
- name: loaded.name || packageInfo.name,
170
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
171
- version: loaded.version || packageInfo.version,
172
- };
173
- } catch (error) {
174
- console.warn(
175
- "Failed to load package.json in development mode. Falling back to default renderer metadata.",
176
- error,
177
- );
178
- }
179
- }
180
-
181
135
  export const reconciler = createReconciler<
182
136
  ElementNames,
183
137
  Props,
@@ -246,7 +200,7 @@ export const reconciler = createReconciler<
246
200
  shouldSetTextContent: () => false,
247
201
  createInstance(originalType, newProps, rootNode, hostContext) {
248
202
  if (hostContext.isInsideText && originalType === "ink-box") {
249
- throw new Error(`<Box> cant be nested inside <Text> component`);
203
+ throw new Error(`<Box> can't be nested inside <Text> component`);
250
204
  }
251
205
 
252
206
  const type =
@@ -273,7 +227,17 @@ export const reconciler = createReconciler<
273
227
 
274
228
  if (key === "internal_transform") {
275
229
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
276
- node.internal_transform = value as OutputTransformer;
230
+ node.internal_transform = value as AnsiTransformer;
231
+ continue;
232
+ }
233
+
234
+ if (key === "internal_ansi") {
235
+ node.internal_ansi = value === true;
236
+ continue;
237
+ }
238
+
239
+ if (key === "internal_textStyle") {
240
+ node.internal_textStyle = value as SemanticTextStyle;
277
241
  continue;
278
242
  }
279
243
 
@@ -378,7 +342,17 @@ export const reconciler = createReconciler<
378
342
 
379
343
  if (key === "internal_transform") {
380
344
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
381
- node.internal_transform = value as OutputTransformer;
345
+ node.internal_transform = value as AnsiTransformer;
346
+ continue;
347
+ }
348
+
349
+ if (key === "internal_ansi") {
350
+ node.internal_ansi = value === true;
351
+ continue;
352
+ }
353
+
354
+ if (key === "internal_textStyle") {
355
+ node.internal_textStyle = value as SemanticTextStyle | undefined;
382
356
  continue;
383
357
  }
384
358
 
@@ -446,6 +420,6 @@ export const reconciler = createReconciler<
446
420
  waitForCommitToBeReady() {
447
421
  return null;
448
422
  },
449
- rendererPackageName: packageInfo.name,
450
- rendererVersion: packageInfo.version,
423
+ rendererPackageName: pkg.name,
424
+ rendererVersion: pkg.version,
451
425
  });
@@ -1,9 +1,10 @@
1
- import { colorize } from "./colorize.ts";
2
- import { type DOMNode } from "./dom.ts";
3
- import type { Output } from "./output.ts";
1
+ import { samplePaint } from "#/color/sample.ts";
2
+ import { type DOMNode } from "#/dom.ts";
3
+ import type { Canvas } from "#/screen/canvas.ts";
4
+ import { cellAttributes, createCell } from "#/screen/index.ts";
4
5
 
5
- export const renderBackground = (x: number, y: number, node: DOMNode, output: Output): void => {
6
- if (!node.style.backgroundColor) {
6
+ export const renderBackground = (x: number, y: number, node: DOMNode, output: Canvas): void => {
7
+ if (node.style.backgroundColor === undefined) {
7
8
  return;
8
9
  }
9
10
 
@@ -23,16 +24,36 @@ export const renderBackground = (x: number, y: number, node: DOMNode, output: Ou
23
24
  return;
24
25
  }
25
26
 
26
- // Create background fill for each row
27
- const backgroundLine = colorize(
28
- " ".repeat(contentWidth),
29
- node.style.backgroundColor,
30
- "background",
27
+ const bounds = {
28
+ x: x + leftBorderWidth,
29
+ y: y + topBorderHeight,
30
+ width: contentWidth,
31
+ height: contentHeight,
32
+ };
33
+ const lines = Array.from({ length: contentHeight }, (_unused, row) =>
34
+ Array.from({ length: contentWidth }, (_empty, column) => {
35
+ const resetBackground = node.style.backgroundColor === "";
36
+ const background = resetBackground
37
+ ? undefined
38
+ : samplePaint(
39
+ node.style.backgroundColor!,
40
+ bounds.x + column,
41
+ bounds.y + row,
42
+ bounds,
43
+ output.paintContext,
44
+ );
45
+ return createCell(
46
+ " ",
47
+ 1,
48
+ {
49
+ ...(background ? { background } : {}),
50
+ underline: "none",
51
+ attributes: cellAttributes.none,
52
+ },
53
+ undefined,
54
+ resetBackground ? { background: true } : undefined,
55
+ );
56
+ }),
31
57
  );
32
-
33
- for (let row = 0; row < contentHeight; row++) {
34
- output.write(x + leftBorderWidth, y + topBorderHeight + row, backgroundLine, {
35
- transformers: [],
36
- });
37
- }
58
+ output.writeCells(bounds.x, bounds.y, lines);
38
59
  };