@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.
- package/README.md +23 -13
- package/THIRD_PARTY_NOTICES.md +70 -23
- package/dist/Text-D5HUf3Fj.d.ts +452 -0
- package/dist/ansi.d.ts +137 -143
- package/dist/ansi.js +87 -2
- 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/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
- 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-Bmc2tRPk.d.ts +21 -0
- package/dist/index.d.ts +235 -754
- package/dist/index.js +1362 -3001
- 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-BOh__-65.js +324 -0
- package/dist/screen-BReKIheE.d.ts +40 -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-BJmzX2NJ.js +664 -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-C2sciZOo.js +1335 -0
- package/package.json +43 -17
- package/src/ansi/chalk.ts +12 -53
- package/src/ansi/cursor.ts +2 -4
- package/src/ansi/east-asian-width.ts +44 -0
- package/src/ansi/escapes.ts +18 -26
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +14 -14
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/sgr.ts +2 -5
- package/src/ansi/slice.ts +1 -1
- package/src/ansi/string-width.ts +91 -206
- package/src/ansi/strip.ts +13 -33
- package/src/ansi/tokenize.ts +96 -169
- package/src/ansi/truncate.ts +5 -3
- package/src/ansi/wrap.ts +13 -14
- package/src/ansi-tokenizer.ts +1 -1
- 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 +109 -22
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +43 -60
- package/src/components/CursorContext.ts +1 -1
- package/src/components/ErrorBoundary.tsx +1 -1
- package/src/components/ErrorOverview.tsx +8 -7
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/Spacer.tsx +1 -1
- package/src/components/Static.tsx +1 -1
- package/src/components/StderrContext.ts +0 -2
- package/src/components/StdinContext.ts +0 -1
- package/src/components/StdoutContext.ts +1 -3
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +23 -46
- package/src/components/Transform.tsx +2 -2
- package/src/cursor-position.ts +1 -1
- package/src/devtools.ts +84 -24
- package/src/dom.ts +11 -8
- package/src/env.ts +12 -0
- package/src/get-max-width.ts +1 -1
- package/src/global.d.ts +6 -4
- package/src/{boxes.ts → glyphs.ts} +17 -18
- package/src/hooks/use-animation.ts +1 -1
- package/src/hooks/use-app.ts +1 -1
- package/src/hooks/use-box-metrics.ts +1 -1
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +3 -3
- package/src/hooks/use-focus-manager.ts +1 -1
- package/src/hooks/use-focus.ts +5 -6
- package/src/hooks/use-input.ts +4 -4
- package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
- package/src/hooks/use-paste.ts +2 -2
- package/src/hooks/use-stderr.ts +1 -1
- package/src/hooks/use-stdin.ts +1 -1
- package/src/hooks/use-stdout.ts +1 -1
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/hooks/use-window-size.ts +2 -2
- package/src/index.ts +88 -44
- package/src/ink.tsx +620 -745
- package/src/input-parser.ts +1 -1
- package/src/instances.ts +1 -1
- package/src/kitty-keyboard.ts +128 -0
- package/src/measure-element.ts +1 -1
- package/src/measure-text.ts +2 -2
- package/src/{render-node-to-output.ts → paint-tree.ts} +78 -53
- package/src/parse-keypress.ts +4 -5
- package/src/parse-stack-line.ts +0 -1
- package/src/patch-console.ts +44 -0
- package/src/reconciler.ts +36 -62
- package/src/render-background.ts +37 -16
- package/src/render-border.ts +97 -64
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +24 -9
- package/src/render.ts +24 -16
- 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/sanitize-ansi.ts +1 -1
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +137 -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 +272 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/signal-exit.ts +4 -5
- package/src/squash-text-nodes.ts +4 -7
- package/src/stream.ts +2 -4
- package/src/structured-text.ts +321 -0
- package/src/styles.ts +16 -17
- 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-size.ts +9 -8
- 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/types.ts +1 -6
- package/src/utils.ts +2 -2
- package/src/wrap-text.ts +4 -4
- package/src/yoga/config.ts +2 -2
- package/src/yoga/core/absoluteLayout.ts +15 -15
- package/src/yoga/core/baseline.ts +3 -3
- package/src/yoga/core/cache.ts +4 -4
- package/src/yoga/core/calculateLayout.ts +33 -27
- package/src/yoga/core/config.ts +1 -1
- package/src/yoga/core/flexLine.ts +3 -3
- package/src/yoga/core/helpers.ts +3 -3
- package/src/yoga/core/layoutResults.ts +4 -4
- package/src/yoga/core/node.ts +16 -16
- package/src/yoga/core/pixelGrid.ts +4 -4
- package/src/yoga/core/style.ts +11 -11
- package/src/yoga/core/types.ts +2 -2
- package/src/yoga/index.ts +6 -6
- package/src/yoga/node.ts +8 -8
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-CBiyyZzw.js +0 -2156
- package/src/ansi/supports-color.ts +0 -213
- package/src/ansi/widest-line.ts +0 -12
- package/src/auto-bind.ts +0 -41
- package/src/colorize.ts +0 -60
- package/src/devtools-window-polyfill.ts +0 -73
- package/src/indent-string.ts +0 -16
- package/src/is-in-ci.ts +0 -7
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- package/src/renderer.ts +0 -73
- 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 {
|
|
2
|
-
import { type
|
|
3
|
-
import { type
|
|
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
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
384
|
+
readonly backgroundColor?: Paint;
|
|
386
385
|
};
|
|
387
386
|
|
|
388
387
|
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
|
+
}
|