@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
@@ -0,0 +1,321 @@
1
+ import { tokenizeAnsi } from "#/ansi-tokenizer.ts";
2
+ import { graphemes } from "#/ansi/graphemes.ts";
3
+ import { stringWidth } from "#/ansi/string-width.ts";
4
+ import { stripAnsi } from "#/ansi/strip.ts";
5
+ import { samplePaint, type PaintContext } from "#/color/index.ts";
6
+ import type { Paint } from "#/color/paint.ts";
7
+ import type { DOMElement, DOMNode } from "#/dom.ts";
8
+ import { cellsFromAnsi } from "#/screen/ansi.ts";
9
+ import { createCell, type Cell, type CellStyle } from "#/screen/cell.ts";
10
+ import type { Rect } from "#/screen/geometry.ts";
11
+ import {
12
+ emptySemanticTextStyle,
13
+ mergeSemanticTextStyles,
14
+ semanticTextStyleToCellStyle,
15
+ type SemanticTextStyle,
16
+ } from "#/semantic-text-style.ts";
17
+ import type { Styles } from "#/styles.ts";
18
+
19
+ type SemanticCell = Cell & {
20
+ readonly foregroundPaint?: Paint;
21
+ readonly backgroundPaint?: Paint;
22
+ };
23
+
24
+ /** Whether a text subtree must retain the ANSI compatibility pipeline. */
25
+ export function hasCompatibilityText(node: DOMElement): boolean {
26
+ if (node.internal_transform && !node.internal_textStyle) {
27
+ return true;
28
+ }
29
+
30
+ return node.childNodes.some((child) => child.nodeName !== "#text" && hasCompatibilityText(child));
31
+ }
32
+
33
+ /** Rasterizes an unwrapped native Text subtree into semantic cell lines. */
34
+ export function structuredTextLines(node: DOMElement): readonly (readonly SemanticCell[])[] {
35
+ const lines: SemanticCell[][] = [[]];
36
+ appendNode(node, emptySemanticTextStyle, lines, false);
37
+ return lines;
38
+ }
39
+
40
+ /** Samples semantic paints only after wrapping and final layout are known. */
41
+ export function sampleStructuredText(
42
+ lines: readonly (readonly SemanticCell[])[],
43
+ bounds: Rect,
44
+ context: PaintContext,
45
+ ): readonly (readonly Cell[])[] {
46
+ return lines.map((line, row) => {
47
+ let column = 0;
48
+ return line.map((cell) => {
49
+ const foreground = cell.foregroundPaint
50
+ ? samplePaint(cell.foregroundPaint, bounds.x + column, bounds.y + row, bounds, context)
51
+ : cell.style.foreground;
52
+ const background = cell.backgroundPaint
53
+ ? samplePaint(cell.backgroundPaint, bounds.x + column, bounds.y + row, bounds, context)
54
+ : cell.style.background;
55
+ column += cell.width;
56
+ return createCell(
57
+ cell.grapheme,
58
+ cell.width,
59
+ {
60
+ ...cell.style,
61
+ ...(foreground ? { foreground } : {}),
62
+ ...(background ? { background } : {}),
63
+ },
64
+ cell.hyperlink,
65
+ cell.reset,
66
+ );
67
+ });
68
+ });
69
+ }
70
+
71
+ export function structuredTextBaseStyle(node: DOMElement): CellStyle {
72
+ return semanticTextStyleToCellStyle(
73
+ mergeSemanticTextStyles(emptySemanticTextStyle, node.internal_textStyle),
74
+ );
75
+ }
76
+
77
+ /** Wraps or truncates semantic cells without converting them through ANSI. */
78
+ export function wrapStructuredText(
79
+ input: readonly (readonly Cell[])[],
80
+ maxWidth: number,
81
+ wrap: Styles["textWrap"],
82
+ baseStyle?: CellStyle,
83
+ ): readonly (readonly Cell[])[] {
84
+ if (maxWidth < 1) {
85
+ return [[]];
86
+ }
87
+
88
+ return input.flatMap((line) => {
89
+ if (lineWidth(line) <= maxWidth) {
90
+ return [[...line]];
91
+ }
92
+
93
+ if (wrap === "hard") {
94
+ return hardWrap(line, maxWidth);
95
+ }
96
+
97
+ if (wrap === "wrap") {
98
+ return wordWrap(line, maxWidth);
99
+ }
100
+
101
+ return [truncate(line, maxWidth, wrap, baseStyle)];
102
+ });
103
+ }
104
+
105
+ function wordWrap(line: readonly Cell[], maxWidth: number): Cell[][] {
106
+ const words: Cell[][] = [[]];
107
+ const spaces: Cell[] = [];
108
+
109
+ for (const cell of line) {
110
+ if (cell.grapheme === " ") {
111
+ spaces.push(cell);
112
+ words.push([]);
113
+ } else {
114
+ words.at(-1)!.push(cell);
115
+ }
116
+ }
117
+
118
+ const output: Cell[][] = [[]];
119
+ appendHard(output, words[0]!, maxWidth);
120
+
121
+ for (let index = 1; index < words.length; index++) {
122
+ appendHard(output, [spaces[index - 1]!], maxWidth);
123
+ const word = words[index]!;
124
+ const current = output.at(-1)!;
125
+ const wordWidth = lineWidth(word);
126
+ const currentWidth = lineWidth(current);
127
+
128
+ if (wordWidth <= maxWidth && currentWidth + wordWidth > maxWidth) {
129
+ output.push([]);
130
+ } else if (wordWidth > maxWidth && currentWidth > 0) {
131
+ const remaining = maxWidth - currentWidth;
132
+ const breaksHere = 1 + Math.floor((wordWidth - remaining - 1) / maxWidth);
133
+ const breaksNext = Math.floor((wordWidth - 1) / maxWidth);
134
+ if (breaksNext < breaksHere) {
135
+ output.push([]);
136
+ }
137
+ }
138
+
139
+ appendHard(output, word, maxWidth);
140
+ }
141
+
142
+ return output;
143
+ }
144
+
145
+ function hardWrap(line: readonly Cell[], maxWidth: number): Cell[][] {
146
+ const output: Cell[][] = [[]];
147
+ appendHard(output, line, maxWidth);
148
+ return output;
149
+ }
150
+
151
+ function appendHard(output: Cell[][], cells: readonly Cell[], maxWidth: number): void {
152
+ for (const cell of cells) {
153
+ const current = output.at(-1)!;
154
+ if (current.length > 0 && lineWidth(current) + cell.width > maxWidth) {
155
+ output.push([]);
156
+ }
157
+
158
+ output.at(-1)!.push(cell);
159
+ }
160
+ }
161
+
162
+ function truncate(
163
+ line: readonly Cell[],
164
+ maxWidth: number,
165
+ wrap: Styles["textWrap"],
166
+ baseStyle: CellStyle | undefined,
167
+ ): Cell[] {
168
+ if (maxWidth === 1) {
169
+ return [ellipsis(baseStyle ?? cellStyleIntersection(line.at(0), line.at(-1)))];
170
+ }
171
+
172
+ if (wrap === "truncate-start") {
173
+ const right = sliceCells(line, lineWidth(line) - maxWidth + 1, lineWidth(line));
174
+ return [ellipsis(right[0]?.style), ...right];
175
+ }
176
+
177
+ if (wrap === "truncate-middle") {
178
+ const half = Math.min(Math.floor(maxWidth / 2), maxWidth - 1);
179
+ const left = sliceCells(line, 0, half);
180
+ const rightWidth = maxWidth - half - 1;
181
+ const right = sliceCells(line, lineWidth(line) - rightWidth, lineWidth(line));
182
+ return [
183
+ ...left,
184
+ ellipsis(baseStyle ?? cellStyleIntersection(left.at(-1), right.at(0))),
185
+ ...right,
186
+ ];
187
+ }
188
+
189
+ const left = sliceCells(line, 0, maxWidth - 1);
190
+ return [...left, ellipsis(left.at(-1)?.style)];
191
+ }
192
+
193
+ function sliceCells(line: readonly Cell[], start: number, end: number): Cell[] {
194
+ const output: Cell[] = [];
195
+ let column = 0;
196
+
197
+ for (const cell of line) {
198
+ const nextColumn = column + cell.width;
199
+ if (column >= start && nextColumn <= end) {
200
+ output.push(cell);
201
+ }
202
+
203
+ column = nextColumn;
204
+ }
205
+
206
+ return output;
207
+ }
208
+
209
+ function ellipsis(style = semanticTextStyleToCellStyle(emptySemanticTextStyle)): Cell {
210
+ return createCell("…", 1, style);
211
+ }
212
+
213
+ function cellStyleIntersection(left: Cell | undefined, right: Cell | undefined): Cell["style"] {
214
+ if (!left) {
215
+ return right?.style ?? semanticTextStyleToCellStyle(emptySemanticTextStyle);
216
+ }
217
+
218
+ if (!right) {
219
+ return left.style;
220
+ }
221
+
222
+ return {
223
+ ...(sameColor(left.style.foreground, right.style.foreground)
224
+ ? { foreground: left.style.foreground }
225
+ : {}),
226
+ ...(sameColor(left.style.background, right.style.background)
227
+ ? { background: left.style.background }
228
+ : {}),
229
+ ...(sameColor(left.style.underlineColor, right.style.underlineColor)
230
+ ? { underlineColor: left.style.underlineColor }
231
+ : {}),
232
+ underline: left.style.underline === right.style.underline ? left.style.underline : "none",
233
+ // eslint-disable-next-line no-bitwise
234
+ attributes: left.style.attributes & right.style.attributes,
235
+ };
236
+ }
237
+
238
+ function sameColor(left: Cell["style"]["foreground"], right: Cell["style"]["foreground"]): boolean {
239
+ return JSON.stringify(left) === JSON.stringify(right);
240
+ }
241
+
242
+ function lineWidth(line: readonly Cell[]): number {
243
+ return line.reduce((width, cell) => width + cell.width, 0);
244
+ }
245
+
246
+ function appendNode(
247
+ node: DOMNode,
248
+ inherited: SemanticTextStyle,
249
+ lines: SemanticCell[][],
250
+ ansi: boolean,
251
+ ): void {
252
+ if (node.nodeName === "#text") {
253
+ if (ansi) appendAnsiText(node.nodeValue, inherited, lines);
254
+ else appendText(node.nodeValue, inherited, lines);
255
+ return;
256
+ }
257
+
258
+ const style = mergeSemanticTextStyles(inherited, node.internal_textStyle);
259
+ for (const child of node.childNodes) {
260
+ appendNode(child, style, lines, ansi || node.internal_ansi === true);
261
+ }
262
+ }
263
+
264
+ function appendAnsiText(text: string, semantic: SemanticTextStyle, lines: SemanticCell[][]): void {
265
+ const base = semanticTextStyleToCellStyle(semantic);
266
+ for (const cell of cellsFromAnsi(text)) {
267
+ if (cell.grapheme === "\n" || cell.grapheme === "\r\n") {
268
+ lines.push([]);
269
+ continue;
270
+ }
271
+
272
+ const hasForeground = cell.style.foreground !== undefined;
273
+ const hasBackground = cell.style.background !== undefined;
274
+ lines.at(-1)!.push({
275
+ ...cell,
276
+ style: {
277
+ ...((cell.style.foreground ?? base.foreground)
278
+ ? { foreground: cell.style.foreground ?? base.foreground }
279
+ : {}),
280
+ ...((cell.style.background ?? base.background)
281
+ ? { background: cell.style.background ?? base.background }
282
+ : {}),
283
+ ...(cell.style.underlineColor ? { underlineColor: cell.style.underlineColor } : {}),
284
+ underline: cell.style.underline === "none" ? base.underline : cell.style.underline,
285
+ // eslint-disable-next-line no-bitwise
286
+ attributes: base.attributes | cell.style.attributes,
287
+ },
288
+ ...(!hasForeground && semantic.foreground ? { foregroundPaint: semantic.foreground } : {}),
289
+ ...(!hasBackground && semantic.background ? { backgroundPaint: semantic.background } : {}),
290
+ });
291
+ }
292
+ }
293
+
294
+ function appendText(text: string, style: SemanticTextStyle, lines: SemanticCell[][]): void {
295
+ const cellStyle = semanticTextStyleToCellStyle(style);
296
+
297
+ const plainText = tokenizeAnsi(text)
298
+ .filter((token) => token.type === "text")
299
+ .map((token) => token.value)
300
+ .join("");
301
+ for (const segment of graphemes(stripAnsi(plainText))) {
302
+ if (segment === "\n" || segment === "\r\n") {
303
+ lines.push([]);
304
+ continue;
305
+ }
306
+
307
+ lines.at(-1)!.push({
308
+ ...createCell(
309
+ segment,
310
+ Math.max(1, stringWidth(segment)),
311
+ cellStyle,
312
+ undefined,
313
+ style.resetForeground || style.resetBackground
314
+ ? { foreground: style.resetForeground, background: style.resetBackground }
315
+ : undefined,
316
+ ),
317
+ ...(style.foreground ? { foregroundPaint: style.foreground } : {}),
318
+ ...(style.background ? { backgroundPaint: style.background } : {}),
319
+ });
320
+ }
321
+ }
package/src/styles.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { type ForegroundColorName } from "./ansi/sgr.ts"; // Note: We import directly from `ansi-styles` to avoid a bug in TypeScript.
2
- import { type Boxes, type BoxStyle } from "./boxes.ts";
3
- import { type LiteralUnion } from "./types.ts";
4
- import { Yoga, type Node as YogaNode, type PositionType } from "./yoga/index.ts";
1
+ import type { Paint } from "#/color/paint.ts";
2
+ import { type BoxGlyphs, type BoxStyle } from "#/glyphs.ts";
3
+ import { Yoga, type Node as YogaNode, type PositionType } from "#/yoga/index.ts";
5
4
 
6
5
  export type Styles = {
7
6
  /*
@@ -134,7 +133,7 @@ export type Styles = {
134
133
  readonly flexGrow?: number;
135
134
 
136
135
  /**
137
- It specifies the “flex shrink factor”, which determines how much the flex item will shrink relative to the rest of the flex items in the flex container when there isn’t enough space on the row.
136
+ It specifies the “flex shrink factor”, which determines how much the flex item will shrink relative to the rest of the flex items in the flex container when there isn't enough space on the row.
138
137
  See [flex-shrink](https://css-tricks.com/almanac/properties/f/flex-shrink/).
139
138
  */
140
139
  readonly flexShrink?: number;
@@ -241,7 +240,7 @@ export type Styles = {
241
240
  /**
242
241
  Add a border with a specified style. If `borderStyle` is `undefined` (the default), no border will be added.
243
242
  */
244
- readonly borderStyle?: keyof Boxes | BoxStyle;
243
+ readonly borderStyle?: BoxStyle | BoxGlyphs;
245
244
 
246
245
  /**
247
246
  Determines whether the top border is visible.
@@ -274,27 +273,27 @@ export type Styles = {
274
273
  /**
275
274
  Change border color. A shorthand for setting `borderTopColor`, `borderRightColor`, `borderBottomColor`, and `borderLeftColor`.
276
275
  */
277
- readonly borderColor?: LiteralUnion<ForegroundColorName, string>;
276
+ readonly borderColor?: Paint;
278
277
 
279
278
  /**
280
279
  Change the top border color. Accepts the same values as `color` in `Text` component.
281
280
  */
282
- readonly borderTopColor?: LiteralUnion<ForegroundColorName, string>;
281
+ readonly borderTopColor?: Paint;
283
282
 
284
283
  /**
285
284
  Change the bottom border color. Accepts the same values as `color` in `Text` component.
286
285
  */
287
- readonly borderBottomColor?: LiteralUnion<ForegroundColorName, string>;
286
+ readonly borderBottomColor?: Paint;
288
287
 
289
288
  /**
290
289
  Change the left border color. Accepts the same values as `color` in `Text` component.
291
290
  */
292
- readonly borderLeftColor?: LiteralUnion<ForegroundColorName, string>;
291
+ readonly borderLeftColor?: Paint;
293
292
 
294
293
  /**
295
294
  Change the right border color. Accepts the same values as `color` in `Text` component.
296
295
  */
297
- readonly borderRightColor?: LiteralUnion<ForegroundColorName, string>;
296
+ readonly borderRightColor?: Paint;
298
297
 
299
298
  /**
300
299
  Dim the border color. A shorthand for setting `borderTopDimColor`, `borderBottomDimColor`, `borderLeftDimColor`, and `borderRightDimColor`.
@@ -334,27 +333,27 @@ export type Styles = {
334
333
  /**
335
334
  Change border background color. A shorthand for setting `borderTopBackgroundColor`, `borderRightBackgroundColor`, `borderBottomBackgroundColor`, and `borderLeftBackgroundColor`.
336
335
  */
337
- readonly borderBackgroundColor?: LiteralUnion<ForegroundColorName, string>;
336
+ readonly borderBackgroundColor?: Paint;
338
337
 
339
338
  /**
340
339
  Change top border background color. Accepts the same values as `backgroundColor` in `Text` component.
341
340
  */
342
- readonly borderTopBackgroundColor?: LiteralUnion<ForegroundColorName, string>;
341
+ readonly borderTopBackgroundColor?: Paint;
343
342
 
344
343
  /**
345
344
  Change bottom border background color. Accepts the same values as `backgroundColor` in `Text` component.
346
345
  */
347
- readonly borderBottomBackgroundColor?: LiteralUnion<ForegroundColorName, string>;
346
+ readonly borderBottomBackgroundColor?: Paint;
348
347
 
349
348
  /**
350
349
  Change left border background color. Accepts the same values as `backgroundColor` in `Text` component.
351
350
  */
352
- readonly borderLeftBackgroundColor?: LiteralUnion<ForegroundColorName, string>;
351
+ readonly borderLeftBackgroundColor?: Paint;
353
352
 
354
353
  /**
355
354
  Change right border background color. Accepts the same values as `backgroundColor` in `Text` component.
356
355
  */
357
- readonly borderRightBackgroundColor?: LiteralUnion<ForegroundColorName, string>;
356
+ readonly borderRightBackgroundColor?: Paint;
358
357
 
359
358
  /**
360
359
  Behavior for an element's overflow in both directions.
@@ -382,7 +381,7 @@ export type Styles = {
382
381
 
383
382
  Accepts the same values as `color` in the `<Text>` component.
384
383
  */
385
- readonly backgroundColor?: LiteralUnion<ForegroundColorName, string>;
384
+ readonly backgroundColor?: Paint;
386
385
  };
387
386
 
388
387
  const positionEdges = [
@@ -0,0 +1,2 @@
1
+ export * from "#/terminal/input.ts";
2
+ export * from "#/terminal/session.ts";
@@ -0,0 +1,120 @@
1
+ import { cliCursor } from "#/ansi/cursor.ts";
2
+ import { ansiEscapes } from "#/ansi/escapes.ts";
3
+ import {
4
+ buildCursorOnlySequence,
5
+ buildCursorSuffix,
6
+ buildReturnToBottomPrefix,
7
+ cursorPositionChanged,
8
+ hideCursorEscape,
9
+ type CursorPosition,
10
+ } from "#/cursor-position.ts";
11
+
12
+ export type InlinePresenter = {
13
+ clear: () => void;
14
+ done: () => void;
15
+ reset: () => void;
16
+ sync: (output: string) => void;
17
+ setCursorPosition: (position: CursorPosition | undefined) => void;
18
+ isCursorDirty: () => boolean;
19
+ willRender: (output: string) => boolean;
20
+ (output: string): boolean;
21
+ };
22
+
23
+ /** Full-rewrite fallback and cursor state for the structured inline renderer. */
24
+ export function createInlinePresenter(
25
+ stream: NodeJS.WritableStream,
26
+ options: { readonly showCursor?: boolean } = {},
27
+ ): InlinePresenter {
28
+ let previousLineCount = 0;
29
+ let previousOutput = "";
30
+ let hasHiddenCursor = false;
31
+ let cursorPosition: CursorPosition | undefined;
32
+ let cursorDirty = false;
33
+ let previousCursorPosition: CursorPosition | undefined;
34
+ let cursorWasShown = false;
35
+ const showCursor = options.showCursor ?? false;
36
+
37
+ const activeCursor = () => (cursorDirty ? cursorPosition : undefined);
38
+ const hasChanges = (output: string, cursor: CursorPosition | undefined) =>
39
+ output !== previousOutput || cursorPositionChanged(cursor, previousCursorPosition);
40
+
41
+ const present = (output: string): boolean => {
42
+ if (!showCursor && !hasHiddenCursor) {
43
+ cliCursor.hide(stream);
44
+ hasHiddenCursor = true;
45
+ }
46
+ const cursor = activeCursor();
47
+ cursorDirty = false;
48
+ const cursorChanged = cursorPositionChanged(cursor, previousCursorPosition);
49
+ if (!hasChanges(output, cursor)) return false;
50
+ const lines = output.split("\n");
51
+ const suffix = buildCursorSuffix(lines.length - 1, cursor);
52
+ if (output === previousOutput && cursorChanged) {
53
+ stream.write(
54
+ buildCursorOnlySequence({
55
+ cursorWasShown,
56
+ previousLineCount,
57
+ previousCursorPosition,
58
+ cursorPosition: cursor,
59
+ }),
60
+ );
61
+ } else {
62
+ previousOutput = output;
63
+ stream.write(
64
+ buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition) +
65
+ ansiEscapes.eraseLines(previousLineCount) +
66
+ output +
67
+ suffix,
68
+ );
69
+ previousLineCount = lines.length;
70
+ }
71
+ previousCursorPosition = cursor ? { ...cursor } : undefined;
72
+ cursorWasShown = cursor !== undefined;
73
+ return true;
74
+ };
75
+
76
+ present.clear = () => {
77
+ stream.write(
78
+ buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition) +
79
+ ansiEscapes.eraseLines(previousLineCount),
80
+ );
81
+ previousOutput = "";
82
+ previousLineCount = 0;
83
+ previousCursorPosition = undefined;
84
+ cursorWasShown = false;
85
+ };
86
+ present.done = () => {
87
+ previousOutput = "";
88
+ previousLineCount = 0;
89
+ previousCursorPosition = undefined;
90
+ cursorWasShown = false;
91
+ if (!showCursor) {
92
+ cliCursor.show(stream);
93
+ hasHiddenCursor = false;
94
+ }
95
+ };
96
+ present.reset = () => {
97
+ previousOutput = "";
98
+ previousLineCount = 0;
99
+ previousCursorPosition = undefined;
100
+ cursorWasShown = false;
101
+ };
102
+ present.sync = (output: string) => {
103
+ const cursor = activeCursor();
104
+ cursorDirty = false;
105
+ const lines = output.split("\n");
106
+ previousOutput = output;
107
+ previousLineCount = lines.length;
108
+ if (!cursor && cursorWasShown) stream.write(hideCursorEscape);
109
+ if (cursor) stream.write(buildCursorSuffix(lines.length - 1, cursor));
110
+ previousCursorPosition = cursor ? { ...cursor } : undefined;
111
+ cursorWasShown = cursor !== undefined;
112
+ };
113
+ present.setCursorPosition = (position: CursorPosition | undefined) => {
114
+ cursorPosition = position;
115
+ cursorDirty = true;
116
+ };
117
+ present.isCursorDirty = () => cursorDirty;
118
+ present.willRender = (output: string) => hasChanges(output, activeCursor());
119
+ return present;
120
+ }
@@ -0,0 +1,86 @@
1
+ import type { CapabilitiesStore } from "#/capabilities/store.ts";
2
+ import { createInputParser, type InputEvent } from "#/input-parser.ts";
3
+
4
+ export type MouseButton = "left" | "middle" | "right" | "none" | "wheel-up" | "wheel-down";
5
+ export type TerminalMouseEvent = {
6
+ readonly type: "press" | "release" | "move" | "wheel";
7
+ readonly x: number;
8
+ readonly y: number;
9
+ readonly button: MouseButton;
10
+ readonly shift: boolean;
11
+ readonly alt: boolean;
12
+ readonly ctrl: boolean;
13
+ };
14
+
15
+ const sgrMouse = /^\u001B\[<(\d+);(\d+);(\d+)([Mm])$/;
16
+
17
+ /** Stateful stdin decoder that separates terminal reports from application input. */
18
+ export class TerminalInput {
19
+ readonly #parser = createInputParser();
20
+ readonly #capabilities: CapabilitiesStore;
21
+ readonly #mouseListeners = new Set<(event: TerminalMouseEvent) => void>();
22
+
23
+ constructor(capabilities: CapabilitiesStore) {
24
+ this.#capabilities = capabilities;
25
+ }
26
+
27
+ push(chunk: string): InputEvent[] {
28
+ return this.#parser.push(chunk).filter((event) => {
29
+ if (typeof event !== "string") return true;
30
+ if (this.#capabilities.ingest(event)) return false;
31
+ const mouse = parseMouseEvent(event);
32
+ if (!mouse) return true;
33
+ for (const listener of this.#mouseListeners) listener(mouse);
34
+ return false;
35
+ });
36
+ }
37
+
38
+ subscribeMouse(listener: (event: TerminalMouseEvent) => void): () => void {
39
+ this.#mouseListeners.add(listener);
40
+ return () => this.#mouseListeners.delete(listener);
41
+ }
42
+
43
+ hasPendingEscape(): boolean {
44
+ return this.#parser.hasPendingEscape();
45
+ }
46
+
47
+ flushPendingEscape(): string | undefined {
48
+ return this.#parser.flushPendingEscape();
49
+ }
50
+
51
+ reset(): void {
52
+ this.#parser.reset();
53
+ }
54
+ }
55
+
56
+ export function parseMouseEvent(sequence: string): TerminalMouseEvent | undefined {
57
+ const match = sequence.match(sgrMouse);
58
+ if (!match) return;
59
+ const code = Number(match[1]);
60
+ const column = Number(match[2]);
61
+ const row = Number(match[3]);
62
+ if (column < 1 || row < 1) return;
63
+ const wheel = (code & 64) !== 0;
64
+ const move = (code & 32) !== 0;
65
+ const buttonCode = code & 3;
66
+ const button: MouseButton = wheel
67
+ ? buttonCode === 0
68
+ ? "wheel-up"
69
+ : "wheel-down"
70
+ : (["left", "middle", "right", "none"] as const)[buttonCode]!;
71
+ return {
72
+ type: wheel
73
+ ? "wheel"
74
+ : move
75
+ ? "move"
76
+ : match[4] === "m" || buttonCode === 3
77
+ ? "release"
78
+ : "press",
79
+ x: column - 1,
80
+ y: row - 1,
81
+ button,
82
+ shift: (code & 4) !== 0,
83
+ alt: (code & 8) !== 0,
84
+ ctrl: (code & 16) !== 0,
85
+ };
86
+ }
@@ -0,0 +1,37 @@
1
+ import { throttle, type Throttled } from "#/throttle.ts";
2
+
3
+ export type RenderScheduler = {
4
+ readonly intervalMs: number;
5
+ readonly throttled: Throttled<never[]> | undefined;
6
+ readonly pending: boolean;
7
+ readonly schedule: () => void;
8
+ readonly immediate: () => void;
9
+ markRendered: () => void;
10
+ };
11
+
12
+ /** Owns frame cadence independently of React reconciliation and terminal presentation. */
13
+ export function createRenderScheduler(
14
+ render: () => void,
15
+ options: { readonly unthrottled: boolean; readonly maxFps: number },
16
+ ): RenderScheduler {
17
+ const frameInterval = options.maxFps > 0 ? Math.max(1, Math.ceil(1000 / options.maxFps)) : 0;
18
+ let pending = false;
19
+ const throttled = options.unthrottled ? undefined : throttle(render, frameInterval);
20
+ return {
21
+ intervalMs: options.unthrottled ? 0 : frameInterval,
22
+ throttled,
23
+ get pending() {
24
+ return pending;
25
+ },
26
+ schedule: options.unthrottled
27
+ ? render
28
+ : () => {
29
+ pending = true;
30
+ throttled!();
31
+ },
32
+ immediate: render,
33
+ markRendered() {
34
+ pending = false;
35
+ },
36
+ };
37
+ }