@alchemy.run/sigil 0.0.0-alpha.4 → 0.0.0-alpha.6
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 +21 -9
- package/THIRD_PARTY_NOTICES.md +39 -1
- package/dist/Text-BobFKi74.d.ts +452 -0
- package/dist/ansi.d.ts +122 -131
- package/dist/ansi.js +86 -6
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
- package/dist/color-policy-SVj1pYTA.js +560 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-Bh4yGP6w.d.ts +186 -0
- package/dist/detect-BuTXtY6e.js +373 -0
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-DZ88EXJv.d.ts +21 -0
- package/dist/index.d.ts +143 -498
- package/dist/index.js +1026 -2244
- package/dist/osc-CCH7xDoS.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-C19minOS.d.ts +81 -0
- package/dist/query-vaIeGOkH.d.ts +152 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +709 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-BOSLQ8fF.d.ts +49 -0
- package/dist/screen-CiPytswf.js +342 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-aZr9O8h3.js +665 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-CgrG9K4y.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +118 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-BzqAJi0n.js +1337 -0
- package/package.json +41 -9
- package/src/ansi/chalk.ts +5 -3
- package/src/ansi/escapes.ts +14 -0
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +3 -1
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/tokenize.ts +3 -4
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +594 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +657 -0
- package/src/capabilities/store.ts +379 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +98 -10
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +0 -8
- package/src/components/CursorContext.ts +1 -1
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +22 -45
- package/src/components/Transform.tsx +1 -1
- package/src/dom.ts +11 -2
- package/src/global.d.ts +3 -0
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +2 -2
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/index.ts +45 -1
- package/src/ink.tsx +213 -153
- package/src/{render-node-to-output.ts → paint-tree.ts} +75 -48
- package/src/reconciler.ts +24 -3
- package/src/render-background.ts +36 -15
- package/src/render-border.ts +94 -61
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +21 -6
- package/src/render.ts +15 -6
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +160 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +305 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/squash-text-nodes.ts +2 -5
- package/src/structured-text.ts +325 -0
- package/src/styles.ts +19 -14
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +86 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +188 -0
- package/src/terminal/session.ts +407 -0
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +349 -0
- package/src/testing/vitest.ts +157 -0
- package/src/transform-adapter.ts +14 -0
- package/src/wrap-text.ts +4 -0
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-Cr6xVFMa.js +0 -2330
- package/src/ansi/supports-color.ts +0 -207
- package/src/colorize.ts +0 -60
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- package/src/renderer.ts +0 -73
|
@@ -0,0 +1,325 @@
|
|
|
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 (wrap === "none") {
|
|
85
|
+
return input.map((line) => [...line]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (maxWidth < 1) {
|
|
89
|
+
return [[]];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return input.flatMap((line) => {
|
|
93
|
+
if (lineWidth(line) <= maxWidth) {
|
|
94
|
+
return [[...line]];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (wrap === "hard") {
|
|
98
|
+
return hardWrap(line, maxWidth);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (wrap === "wrap") {
|
|
102
|
+
return wordWrap(line, maxWidth);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [truncate(line, maxWidth, wrap, baseStyle)];
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function wordWrap(line: readonly Cell[], maxWidth: number): Cell[][] {
|
|
110
|
+
const words: Cell[][] = [[]];
|
|
111
|
+
const spaces: Cell[] = [];
|
|
112
|
+
|
|
113
|
+
for (const cell of line) {
|
|
114
|
+
if (cell.grapheme === " ") {
|
|
115
|
+
spaces.push(cell);
|
|
116
|
+
words.push([]);
|
|
117
|
+
} else {
|
|
118
|
+
words.at(-1)!.push(cell);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const output: Cell[][] = [[]];
|
|
123
|
+
appendHard(output, words[0]!, maxWidth);
|
|
124
|
+
|
|
125
|
+
for (let index = 1; index < words.length; index++) {
|
|
126
|
+
appendHard(output, [spaces[index - 1]!], maxWidth);
|
|
127
|
+
const word = words[index]!;
|
|
128
|
+
const current = output.at(-1)!;
|
|
129
|
+
const wordWidth = lineWidth(word);
|
|
130
|
+
const currentWidth = lineWidth(current);
|
|
131
|
+
|
|
132
|
+
if (wordWidth <= maxWidth && currentWidth + wordWidth > maxWidth) {
|
|
133
|
+
output.push([]);
|
|
134
|
+
} else if (wordWidth > maxWidth && currentWidth > 0) {
|
|
135
|
+
const remaining = maxWidth - currentWidth;
|
|
136
|
+
const breaksHere = 1 + Math.floor((wordWidth - remaining - 1) / maxWidth);
|
|
137
|
+
const breaksNext = Math.floor((wordWidth - 1) / maxWidth);
|
|
138
|
+
if (breaksNext < breaksHere) {
|
|
139
|
+
output.push([]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
appendHard(output, word, maxWidth);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function hardWrap(line: readonly Cell[], maxWidth: number): Cell[][] {
|
|
150
|
+
const output: Cell[][] = [[]];
|
|
151
|
+
appendHard(output, line, maxWidth);
|
|
152
|
+
return output;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function appendHard(output: Cell[][], cells: readonly Cell[], maxWidth: number): void {
|
|
156
|
+
for (const cell of cells) {
|
|
157
|
+
const current = output.at(-1)!;
|
|
158
|
+
if (current.length > 0 && lineWidth(current) + cell.width > maxWidth) {
|
|
159
|
+
output.push([]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
output.at(-1)!.push(cell);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function truncate(
|
|
167
|
+
line: readonly Cell[],
|
|
168
|
+
maxWidth: number,
|
|
169
|
+
wrap: Styles["textWrap"],
|
|
170
|
+
baseStyle: CellStyle | undefined,
|
|
171
|
+
): Cell[] {
|
|
172
|
+
if (maxWidth === 1) {
|
|
173
|
+
return [ellipsis(baseStyle ?? cellStyleIntersection(line.at(0), line.at(-1)))];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (wrap === "truncate-start") {
|
|
177
|
+
const right = sliceCells(line, lineWidth(line) - maxWidth + 1, lineWidth(line));
|
|
178
|
+
return [ellipsis(right[0]?.style), ...right];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (wrap === "truncate-middle") {
|
|
182
|
+
const half = Math.min(Math.floor(maxWidth / 2), maxWidth - 1);
|
|
183
|
+
const left = sliceCells(line, 0, half);
|
|
184
|
+
const rightWidth = maxWidth - half - 1;
|
|
185
|
+
const right = sliceCells(line, lineWidth(line) - rightWidth, lineWidth(line));
|
|
186
|
+
return [
|
|
187
|
+
...left,
|
|
188
|
+
ellipsis(baseStyle ?? cellStyleIntersection(left.at(-1), right.at(0))),
|
|
189
|
+
...right,
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const left = sliceCells(line, 0, maxWidth - 1);
|
|
194
|
+
return [...left, ellipsis(left.at(-1)?.style)];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sliceCells(line: readonly Cell[], start: number, end: number): Cell[] {
|
|
198
|
+
const output: Cell[] = [];
|
|
199
|
+
let column = 0;
|
|
200
|
+
|
|
201
|
+
for (const cell of line) {
|
|
202
|
+
const nextColumn = column + cell.width;
|
|
203
|
+
if (column >= start && nextColumn <= end) {
|
|
204
|
+
output.push(cell);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
column = nextColumn;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return output;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function ellipsis(style = semanticTextStyleToCellStyle(emptySemanticTextStyle)): Cell {
|
|
214
|
+
return createCell("…", 1, style);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function cellStyleIntersection(left: Cell | undefined, right: Cell | undefined): Cell["style"] {
|
|
218
|
+
if (!left) {
|
|
219
|
+
return right?.style ?? semanticTextStyleToCellStyle(emptySemanticTextStyle);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!right) {
|
|
223
|
+
return left.style;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
...(sameColor(left.style.foreground, right.style.foreground)
|
|
228
|
+
? { foreground: left.style.foreground }
|
|
229
|
+
: {}),
|
|
230
|
+
...(sameColor(left.style.background, right.style.background)
|
|
231
|
+
? { background: left.style.background }
|
|
232
|
+
: {}),
|
|
233
|
+
...(sameColor(left.style.underlineColor, right.style.underlineColor)
|
|
234
|
+
? { underlineColor: left.style.underlineColor }
|
|
235
|
+
: {}),
|
|
236
|
+
underline: left.style.underline === right.style.underline ? left.style.underline : "none",
|
|
237
|
+
// eslint-disable-next-line no-bitwise
|
|
238
|
+
attributes: left.style.attributes & right.style.attributes,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sameColor(left: Cell["style"]["foreground"], right: Cell["style"]["foreground"]): boolean {
|
|
243
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function lineWidth(line: readonly Cell[]): number {
|
|
247
|
+
return line.reduce((width, cell) => width + cell.width, 0);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function appendNode(
|
|
251
|
+
node: DOMNode,
|
|
252
|
+
inherited: SemanticTextStyle,
|
|
253
|
+
lines: SemanticCell[][],
|
|
254
|
+
ansi: boolean,
|
|
255
|
+
): void {
|
|
256
|
+
if (node.nodeName === "#text") {
|
|
257
|
+
if (ansi) appendAnsiText(node.nodeValue, inherited, lines);
|
|
258
|
+
else appendText(node.nodeValue, inherited, lines);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const style = mergeSemanticTextStyles(inherited, node.internal_textStyle);
|
|
263
|
+
for (const child of node.childNodes) {
|
|
264
|
+
appendNode(child, style, lines, ansi || node.internal_ansi === true);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function appendAnsiText(text: string, semantic: SemanticTextStyle, lines: SemanticCell[][]): void {
|
|
269
|
+
const base = semanticTextStyleToCellStyle(semantic);
|
|
270
|
+
for (const cell of cellsFromAnsi(text)) {
|
|
271
|
+
if (cell.grapheme === "\n" || cell.grapheme === "\r\n") {
|
|
272
|
+
lines.push([]);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const hasForeground = cell.style.foreground !== undefined;
|
|
277
|
+
const hasBackground = cell.style.background !== undefined;
|
|
278
|
+
lines.at(-1)!.push({
|
|
279
|
+
...cell,
|
|
280
|
+
style: {
|
|
281
|
+
...((cell.style.foreground ?? base.foreground)
|
|
282
|
+
? { foreground: cell.style.foreground ?? base.foreground }
|
|
283
|
+
: {}),
|
|
284
|
+
...((cell.style.background ?? base.background)
|
|
285
|
+
? { background: cell.style.background ?? base.background }
|
|
286
|
+
: {}),
|
|
287
|
+
...(cell.style.underlineColor ? { underlineColor: cell.style.underlineColor } : {}),
|
|
288
|
+
underline: cell.style.underline === "none" ? base.underline : cell.style.underline,
|
|
289
|
+
// eslint-disable-next-line no-bitwise
|
|
290
|
+
attributes: base.attributes | cell.style.attributes,
|
|
291
|
+
},
|
|
292
|
+
...(!hasForeground && semantic.foreground ? { foregroundPaint: semantic.foreground } : {}),
|
|
293
|
+
...(!hasBackground && semantic.background ? { backgroundPaint: semantic.background } : {}),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function appendText(text: string, style: SemanticTextStyle, lines: SemanticCell[][]): void {
|
|
299
|
+
const cellStyle = semanticTextStyleToCellStyle(style);
|
|
300
|
+
|
|
301
|
+
const plainText = tokenizeAnsi(text)
|
|
302
|
+
.filter((token) => token.type === "text")
|
|
303
|
+
.map((token) => token.value)
|
|
304
|
+
.join("");
|
|
305
|
+
for (const segment of graphemes(stripAnsi(plainText))) {
|
|
306
|
+
if (segment === "\n" || segment === "\r\n") {
|
|
307
|
+
lines.push([]);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
lines.at(-1)!.push({
|
|
312
|
+
...createCell(
|
|
313
|
+
segment,
|
|
314
|
+
Math.max(1, stringWidth(segment)),
|
|
315
|
+
cellStyle,
|
|
316
|
+
undefined,
|
|
317
|
+
style.resetForeground || style.resetBackground
|
|
318
|
+
? { foreground: style.resetForeground, background: style.resetBackground }
|
|
319
|
+
: undefined,
|
|
320
|
+
),
|
|
321
|
+
...(style.foreground ? { foregroundPaint: style.foreground } : {}),
|
|
322
|
+
...(style.background ? { backgroundPaint: style.background } : {}),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
package/src/styles.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Paint } from "#/color/paint.ts";
|
|
2
2
|
import { type BoxGlyphs, type BoxStyle } from "#/glyphs.ts";
|
|
3
|
-
import { type LiteralUnion } from "#/types.ts";
|
|
4
3
|
import { Yoga, type Node as YogaNode, type PositionType } from "#/yoga/index.ts";
|
|
5
4
|
|
|
6
5
|
export type Styles = {
|
|
7
6
|
/*
|
|
8
7
|
We keep this as a single enum so overflow is one complete choice and invalid combinations like wrap + truncate-middle are unrepresentable. In hindsight, `normal` would have been a clearer default value than `wrap`, since it describes the standard behavior instead of repeating the prop name.
|
|
8
|
+
|
|
9
|
+
`none` neither wraps nor truncates: the text overflows the container (and
|
|
10
|
+
the screen's nominal width). Meant for scrollback content (`<Static>`),
|
|
11
|
+
not live regions, where a physically soft-wrapped line breaks the
|
|
12
|
+
presenter's line accounting.
|
|
9
13
|
*/
|
|
10
14
|
readonly textWrap?:
|
|
11
15
|
| "wrap"
|
|
@@ -13,7 +17,8 @@ export type Styles = {
|
|
|
13
17
|
| "truncate-end"
|
|
14
18
|
| "truncate"
|
|
15
19
|
| "truncate-middle"
|
|
16
|
-
| "truncate-start"
|
|
20
|
+
| "truncate-start"
|
|
21
|
+
| "none";
|
|
17
22
|
|
|
18
23
|
/**
|
|
19
24
|
Controls how the element is positioned.
|
|
@@ -274,27 +279,27 @@ export type Styles = {
|
|
|
274
279
|
/**
|
|
275
280
|
Change border color. A shorthand for setting `borderTopColor`, `borderRightColor`, `borderBottomColor`, and `borderLeftColor`.
|
|
276
281
|
*/
|
|
277
|
-
readonly borderColor?:
|
|
282
|
+
readonly borderColor?: Paint;
|
|
278
283
|
|
|
279
284
|
/**
|
|
280
285
|
Change the top border color. Accepts the same values as `color` in `Text` component.
|
|
281
286
|
*/
|
|
282
|
-
readonly borderTopColor?:
|
|
287
|
+
readonly borderTopColor?: Paint;
|
|
283
288
|
|
|
284
289
|
/**
|
|
285
290
|
Change the bottom border color. Accepts the same values as `color` in `Text` component.
|
|
286
291
|
*/
|
|
287
|
-
readonly borderBottomColor?:
|
|
292
|
+
readonly borderBottomColor?: Paint;
|
|
288
293
|
|
|
289
294
|
/**
|
|
290
295
|
Change the left border color. Accepts the same values as `color` in `Text` component.
|
|
291
296
|
*/
|
|
292
|
-
readonly borderLeftColor?:
|
|
297
|
+
readonly borderLeftColor?: Paint;
|
|
293
298
|
|
|
294
299
|
/**
|
|
295
300
|
Change the right border color. Accepts the same values as `color` in `Text` component.
|
|
296
301
|
*/
|
|
297
|
-
readonly borderRightColor?:
|
|
302
|
+
readonly borderRightColor?: Paint;
|
|
298
303
|
|
|
299
304
|
/**
|
|
300
305
|
Dim the border color. A shorthand for setting `borderTopDimColor`, `borderBottomDimColor`, `borderLeftDimColor`, and `borderRightDimColor`.
|
|
@@ -334,27 +339,27 @@ export type Styles = {
|
|
|
334
339
|
/**
|
|
335
340
|
Change border background color. A shorthand for setting `borderTopBackgroundColor`, `borderRightBackgroundColor`, `borderBottomBackgroundColor`, and `borderLeftBackgroundColor`.
|
|
336
341
|
*/
|
|
337
|
-
readonly borderBackgroundColor?:
|
|
342
|
+
readonly borderBackgroundColor?: Paint;
|
|
338
343
|
|
|
339
344
|
/**
|
|
340
345
|
Change top border background color. Accepts the same values as `backgroundColor` in `Text` component.
|
|
341
346
|
*/
|
|
342
|
-
readonly borderTopBackgroundColor?:
|
|
347
|
+
readonly borderTopBackgroundColor?: Paint;
|
|
343
348
|
|
|
344
349
|
/**
|
|
345
350
|
Change bottom border background color. Accepts the same values as `backgroundColor` in `Text` component.
|
|
346
351
|
*/
|
|
347
|
-
readonly borderBottomBackgroundColor?:
|
|
352
|
+
readonly borderBottomBackgroundColor?: Paint;
|
|
348
353
|
|
|
349
354
|
/**
|
|
350
355
|
Change left border background color. Accepts the same values as `backgroundColor` in `Text` component.
|
|
351
356
|
*/
|
|
352
|
-
readonly borderLeftBackgroundColor?:
|
|
357
|
+
readonly borderLeftBackgroundColor?: Paint;
|
|
353
358
|
|
|
354
359
|
/**
|
|
355
360
|
Change right border background color. Accepts the same values as `backgroundColor` in `Text` component.
|
|
356
361
|
*/
|
|
357
|
-
readonly borderRightBackgroundColor?:
|
|
362
|
+
readonly borderRightBackgroundColor?: Paint;
|
|
358
363
|
|
|
359
364
|
/**
|
|
360
365
|
Behavior for an element's overflow in both directions.
|
|
@@ -382,7 +387,7 @@ export type Styles = {
|
|
|
382
387
|
|
|
383
388
|
Accepts the same values as `color` in the `<Text>` component.
|
|
384
389
|
*/
|
|
385
|
-
readonly backgroundColor?:
|
|
390
|
+
readonly backgroundColor?: Paint;
|
|
386
391
|
};
|
|
387
392
|
|
|
388
393
|
const positionEdges = [
|
|
@@ -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
|
+
}
|