@effected/cli 0.13.0 → 0.15.0
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/Doc.js +9 -2
- package/Render.js +3 -3
- package/index.d.ts +16 -6
- package/internal/renderDoc.js +9 -6
- package/package.json +5 -5
- package/ui/CliUiLive.js +1 -8
- package/ui/UiTheme.js +38 -9
- package/ui/testing/CliUiTest.js +37 -12
- package/ui/testing/terminalModel.js +13 -4
- package/ui-testing.d.ts +4 -1
- package/ui.d.ts +3 -7
- package/ui/internal/perfDrain.js +0 -33
package/Doc.js
CHANGED
|
@@ -379,12 +379,19 @@ var Doc = class {
|
|
|
379
379
|
/**
|
|
380
380
|
* Lines, one per entry, in every renderer: markdown joins them with hard breaks so they never collapse into one.
|
|
381
381
|
*
|
|
382
|
+
* @remarks
|
|
383
|
+
* `truncate` and `wrap: false` hold for every entry as they do for {@link Doc.line}: each entry is cut to the width,
|
|
384
|
+
* or kept whole on one line, rather than wrapped. Markdown keeps every entry whole either way.
|
|
385
|
+
*
|
|
382
386
|
* @param lines - the entries; each takes a string, an inline or an array of either
|
|
387
|
+
* @param options - `truncate`, to cut each entry to the width; `wrap: false`, to keep each whole
|
|
383
388
|
*/
|
|
384
|
-
static lines(lines) {
|
|
389
|
+
static lines(lines, options) {
|
|
385
390
|
return freeze({
|
|
386
391
|
_tag: "Lines",
|
|
387
|
-
lines: frozenArray(lines.map(inlines))
|
|
392
|
+
lines: frozenArray(lines.map(inlines)),
|
|
393
|
+
...options?.truncate === void 0 ? {} : { truncate: options.truncate },
|
|
394
|
+
...options?.wrap === void 0 ? {} : { wrap: options.wrap }
|
|
388
395
|
});
|
|
389
396
|
}
|
|
390
397
|
/**
|
package/Render.js
CHANGED
|
@@ -186,7 +186,7 @@ var Render = class {
|
|
|
186
186
|
*
|
|
187
187
|
* Under GitHub Actions (`neutralizeWorkflowCommands`) the same neutralizing applies, since markdown can be printed
|
|
188
188
|
* to the log. Markdown escapes `[` in text, so `##[` cannot appear outside code and the headings are untouched
|
|
189
|
-
* (a bare `##` is not a command); code spans and blocks, which are not escaped, get the
|
|
189
|
+
* (a bare `##` is not a command); code spans and blocks, which are not escaped, get the blank marker, which can
|
|
190
190
|
* also land inside code or table text where it would otherwise have formed a command.
|
|
191
191
|
*
|
|
192
192
|
* GitHub also turns `@user`, `@org/team`, `#123` and commit SHAs in rendered markdown into mentions and references.
|
|
@@ -238,8 +238,8 @@ var Render = class {
|
|
|
238
238
|
*
|
|
239
239
|
* The runner has two command parsers, and a line is a command if either accepts it: after its leading whitespace it
|
|
240
240
|
* starts with `::`, or `##[` occurs ANYWHERE in it (a bare `##` is not one). A document's text must not be able to
|
|
241
|
-
* do that (`::add-mask::`, `::error::`, `##[error]`), so such a `::` line gets a
|
|
242
|
-
* the runner
|
|
241
|
+
* do that (`::add-mask::`, `::error::`, `##[error]`), so such a `::` line gets a braille pattern blank (U+2800) in
|
|
242
|
+
* front, which the runner neither trims nor skips, and every `##[` gets one before the `[`. The text is
|
|
243
243
|
* otherwise unchanged. A
|
|
244
244
|
* group's title is a command's data, so its `%`, CR and LF are escaped. Lines are split at CR, LF and CRLF before
|
|
245
245
|
* that check, as the runner splits them. There is no ANSI and `paint` and `link` are never called, and the audience
|
package/index.d.ts
CHANGED
|
@@ -879,6 +879,8 @@ type Block = {
|
|
|
879
879
|
} | {
|
|
880
880
|
readonly _tag: "Lines";
|
|
881
881
|
readonly lines: ReadonlyArray<ReadonlyArray<Inline>>;
|
|
882
|
+
readonly truncate?: boolean;
|
|
883
|
+
readonly wrap?: boolean;
|
|
882
884
|
} | {
|
|
883
885
|
readonly _tag: "Line";
|
|
884
886
|
readonly content: ReadonlyArray<Inline>;
|
|
@@ -1312,9 +1314,17 @@ export declare class Doc {
|
|
|
1312
1314
|
/**
|
|
1313
1315
|
* Lines, one per entry, in every renderer: markdown joins them with hard breaks so they never collapse into one.
|
|
1314
1316
|
*
|
|
1317
|
+
* @remarks
|
|
1318
|
+
* `truncate` and `wrap: false` hold for every entry as they do for {@link Doc.line}: each entry is cut to the width,
|
|
1319
|
+
* or kept whole on one line, rather than wrapped. Markdown keeps every entry whole either way.
|
|
1320
|
+
*
|
|
1315
1321
|
* @param lines - the entries; each takes a string, an inline or an array of either
|
|
1322
|
+
* @param options - `truncate`, to cut each entry to the width; `wrap: false`, to keep each whole
|
|
1316
1323
|
*/
|
|
1317
|
-
static lines(lines: ReadonlyArray<InlineInput
|
|
1324
|
+
static lines(lines: ReadonlyArray<InlineInput>, options?: {
|
|
1325
|
+
readonly truncate?: boolean;
|
|
1326
|
+
readonly wrap?: boolean;
|
|
1327
|
+
}): BlockOf<"Lines">;
|
|
1318
1328
|
/**
|
|
1319
1329
|
* One line of content; with `truncate`, it is cut to the width with the glyph set's ellipsis instead of wrapping,
|
|
1320
1330
|
* and with `wrap: false` it is kept whole on one line whatever the width.
|
|
@@ -3224,8 +3234,8 @@ interface RenderContext {
|
|
|
3224
3234
|
/**
|
|
3225
3235
|
* Whether the output will be read by the GitHub Actions runner, which has two command parsers: a line is a command
|
|
3226
3236
|
* if, after .NET whitespace, it starts with `::`, or if `##[` occurs ANYWHERE in it (a bare `##` is not one). When
|
|
3227
|
-
* `true`, `plain`, `ansi` and `markdown` put a
|
|
3228
|
-
* `[`
|
|
3237
|
+
* `true`, `plain`, `ansi` and `markdown` put a braille pattern blank (U+2800, one blank cell) in front of such a
|
|
3238
|
+
* `::` line and before the `[` of each `##[`, so a document's text, an error message, say, can never inject a command. Unset or `false`
|
|
3229
3239
|
* leaves their text alone. `Render.githubLog` ignores it and always neutralizes: its output is for the runner by
|
|
3230
3240
|
* definition.
|
|
3231
3241
|
*
|
|
@@ -3418,7 +3428,7 @@ export declare class Render {
|
|
|
3418
3428
|
*
|
|
3419
3429
|
* Under GitHub Actions (`neutralizeWorkflowCommands`) the same neutralizing applies, since markdown can be printed
|
|
3420
3430
|
* to the log. Markdown escapes `[` in text, so `##[` cannot appear outside code and the headings are untouched
|
|
3421
|
-
* (a bare `##` is not a command); code spans and blocks, which are not escaped, get the
|
|
3431
|
+
* (a bare `##` is not a command); code spans and blocks, which are not escaped, get the blank marker, which can
|
|
3422
3432
|
* also land inside code or table text where it would otherwise have formed a command.
|
|
3423
3433
|
*
|
|
3424
3434
|
* GitHub also turns `@user`, `@org/team`, `#123` and commit SHAs in rendered markdown into mentions and references.
|
|
@@ -3470,8 +3480,8 @@ export declare class Render {
|
|
|
3470
3480
|
*
|
|
3471
3481
|
* The runner has two command parsers, and a line is a command if either accepts it: after its leading whitespace it
|
|
3472
3482
|
* starts with `::`, or `##[` occurs ANYWHERE in it (a bare `##` is not one). A document's text must not be able to
|
|
3473
|
-
* do that (`::add-mask::`, `::error::`, `##[error]`), so such a `::` line gets a
|
|
3474
|
-
* the runner
|
|
3483
|
+
* do that (`::add-mask::`, `::error::`, `##[error]`), so such a `::` line gets a braille pattern blank (U+2800) in
|
|
3484
|
+
* front, which the runner neither trims nor skips, and every `##[` gets one before the `[`. The text is
|
|
3475
3485
|
* otherwise unchanged. A
|
|
3476
3486
|
* group's title is a command's data, so its `%`, CR and LF are escaped. Lines are split at CR, LF and CRLF before
|
|
3477
3487
|
* that check, as the runner splits them. There is no ANSI and `paint` and `link` are never called, and the audience
|
package/internal/renderDoc.js
CHANGED
|
@@ -227,6 +227,13 @@ const countsLayout = (walk, block) => {
|
|
|
227
227
|
suffix
|
|
228
228
|
], " "))];
|
|
229
229
|
};
|
|
230
|
+
/** One `Line`, or one entry of a `Lines`: cut with `truncate`, kept whole with `wrap: false`, wrapped otherwise. */
|
|
231
|
+
const lineOf = (walk, content, options, width) => {
|
|
232
|
+
const spans = oneLine(inline(walk, content));
|
|
233
|
+
if (options.truncate === true) return [trimLine(truncateSpans(spans, width, walk.ctx.glyphs.ellipsis))];
|
|
234
|
+
if (options.wrap === false) return [trimLine(spans)];
|
|
235
|
+
return spans.length === 0 ? [[]] : wrapSpans(spans, width, { hardBreak: false }).map(trimLine);
|
|
236
|
+
};
|
|
230
237
|
/**
|
|
231
238
|
* A block as lines. `compact` is set on a compact list's item: a section there joins its title and children with no
|
|
232
239
|
* blank lines between them.
|
|
@@ -275,15 +282,11 @@ const blockLines = (walk, block, width, compact = false) => {
|
|
|
275
282
|
case "Annotation": return [];
|
|
276
283
|
case "CountsTable": return tableLines(walk, countsTableOf(block), width);
|
|
277
284
|
case "Lines": return block.lines.flatMap((entry) => {
|
|
285
|
+
if (block.truncate === true || block.wrap === false) return lineOf(walk, entry, block, width);
|
|
278
286
|
const spans = inline(walk, entry);
|
|
279
287
|
return spans.length === 0 ? [[]] : wrapSpans(spans, width, { hardBreak: false }).map(trimLine);
|
|
280
288
|
});
|
|
281
|
-
case "Line":
|
|
282
|
-
const spans = oneLine(inline(walk, block.content));
|
|
283
|
-
if (block.truncate === true) return [trimLine(truncateSpans(spans, width, walk.ctx.glyphs.ellipsis))];
|
|
284
|
-
if (block.wrap === false) return [trimLine(spans)];
|
|
285
|
-
return spans.length === 0 ? [[]] : wrapSpans(spans, width, { hardBreak: false }).map(trimLine);
|
|
286
|
-
}
|
|
289
|
+
case "Line": return lineOf(walk, block.content, block, width);
|
|
287
290
|
case "DiffText": {
|
|
288
291
|
const cap = capOf(block.cap);
|
|
289
292
|
const lines = textLines(block.text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The presentation boundary of an effect/cli program: audience-aware output, a document IR and renderers, editor links, failure reports and logging, plus opt-in Ink screens, widgets and a live view",
|
|
6
6
|
"keywords": [
|
|
@@ -57,17 +57,17 @@
|
|
|
57
57
|
"./package.json": "./package.json"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@effected/github-commands": "^0.
|
|
60
|
+
"@effected/github-commands": "^0.2.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@effected/config-file": "^0.14.2",
|
|
64
64
|
"@effected/env": "^0.1.0",
|
|
65
65
|
"@effected/glob": "^0.10.0",
|
|
66
66
|
"@effected/walker": "^0.15.0",
|
|
67
|
-
"@types/react": "^19.
|
|
67
|
+
"@types/react": "^19.3.0",
|
|
68
68
|
"effect": "^4.0.0",
|
|
69
|
-
"ink": "^
|
|
70
|
-
"react": "^19.
|
|
69
|
+
"ink": "^8.0.0",
|
|
70
|
+
"react": "^19.3.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependenciesMeta": {
|
|
73
73
|
"@effected/config-file": {
|
package/ui/CliUiLive.js
CHANGED
|
@@ -10,7 +10,6 @@ import { UiStreams } from "./UiStreams.js";
|
|
|
10
10
|
import { makeInkConsole } from "./internal/inkConsole.js";
|
|
11
11
|
import { LazyViewShapeError, loadView } from "./internal/lazyView.js";
|
|
12
12
|
import { mountPermit } from "./internal/mountPermit.js";
|
|
13
|
-
import { drainPerformance, resolveDrain } from "./internal/perfDrain.js";
|
|
14
13
|
import { UiRenderOptions } from "./internal/renderOptions.js";
|
|
15
14
|
import { uiProviders } from "./internal/UiProviders.js";
|
|
16
15
|
import { useTerminalSize } from "./UiTheme.js";
|
|
@@ -63,7 +62,6 @@ const live = (options) => Effect.gen(function* () {
|
|
|
63
62
|
const interactive = yield* CliInteractive;
|
|
64
63
|
const streams = yield* UiStreams;
|
|
65
64
|
const overrides = yield* UiRenderOptions;
|
|
66
|
-
const drain = yield* resolveDrain(options.drainPerformance ?? "auto");
|
|
67
65
|
const bridge = yield* makeInkConsole;
|
|
68
66
|
const inbox = yield* Queue.unbounded();
|
|
69
67
|
const source = options.events;
|
|
@@ -148,7 +146,6 @@ const live = (options) => Effect.gen(function* () {
|
|
|
148
146
|
}, elementOf(state, frame))
|
|
149
147
|
});
|
|
150
148
|
const text = yield* Effect.scoped(Effect.andThen(withInkColour(colour), Effect.sync(() => ink.renderToString(tree, { columns }))));
|
|
151
|
-
drainPerformance(drain);
|
|
152
149
|
if (failure !== void 0) return yield* warnOnce(current, failure.error);
|
|
153
150
|
bridge.print(neutralize ? CommandNeutralizer.text(text) : text);
|
|
154
151
|
});
|
|
@@ -220,7 +217,7 @@ const live = (options) => Effect.gen(function* () {
|
|
|
220
217
|
}))))
|
|
221
218
|
});
|
|
222
219
|
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
223
|
-
|
|
220
|
+
return ink.render(tree, {
|
|
224
221
|
stdin: streams.stdin,
|
|
225
222
|
stdout: streams.stdout,
|
|
226
223
|
stderr: streams.stderr,
|
|
@@ -230,13 +227,10 @@ const live = (options) => Effect.gen(function* () {
|
|
|
230
227
|
...overrides.onRender === void 0 ? {} : { onRender: overrides.onRender },
|
|
231
228
|
...overrides.maxFps === void 0 ? {} : { maxFps: overrides.maxFps }
|
|
232
229
|
});
|
|
233
|
-
drainPerformance(drain);
|
|
234
|
-
return instance;
|
|
235
230
|
}), (instance) => Effect.promise(async () => {
|
|
236
231
|
bridge.detach();
|
|
237
232
|
const exited = instance.waitUntilExit();
|
|
238
233
|
instance.unmount();
|
|
239
|
-
drainPerformance(drain);
|
|
240
234
|
await exited.catch(() => void 0);
|
|
241
235
|
}));
|
|
242
236
|
yield* Effect.forkIn(Effect.repeat(Effect.flatMap(frameOf, (at) => Queue.offer(inbox, {
|
|
@@ -270,7 +264,6 @@ const live = (options) => Effect.gen(function* () {
|
|
|
270
264
|
yield* Effect.callback((resume) => {
|
|
271
265
|
mounted.slot.swap(elementOf(shown, frame), () => queueMicrotask(() => resume(Effect.void)));
|
|
272
266
|
});
|
|
273
|
-
drainPerformance(drain);
|
|
274
267
|
current.frame = frame;
|
|
275
268
|
if (current.failed === void 0) {
|
|
276
269
|
current.painted = true;
|
package/ui/UiTheme.js
CHANGED
|
@@ -69,6 +69,37 @@ const Styled = (props) => {
|
|
|
69
69
|
};
|
|
70
70
|
/** A reported size, or `fallback` when it is unknown: absent, or not positive (a pty `script` opens reports 0x0). */
|
|
71
71
|
const known = (reported, fallback) => reported !== void 0 && reported > 0 ? reported : fallback;
|
|
72
|
+
/** The redraws following each stream's size, and the one `resize` listener that calls them all. */
|
|
73
|
+
const resizeFollowers = /* @__PURE__ */ new WeakMap();
|
|
74
|
+
/**
|
|
75
|
+
* Follow `stdout`'s size with `redraw`, returning the unfollow. However many components follow one stream, it holds
|
|
76
|
+
* one `resize` listener, added with the first and removed with the last: one per component passed Node's default
|
|
77
|
+
* limit of 10 on any list of `DocView` rows and printed a `MaxListenersExceededWarning` into the frame.
|
|
78
|
+
*/
|
|
79
|
+
const followResize = (stdout, redraw) => {
|
|
80
|
+
let entry = resizeFollowers.get(stdout);
|
|
81
|
+
if (entry === void 0) {
|
|
82
|
+
const redraws = /* @__PURE__ */ new Set();
|
|
83
|
+
const onResize = () => {
|
|
84
|
+
for (const each of redraws) each();
|
|
85
|
+
};
|
|
86
|
+
entry = {
|
|
87
|
+
redraws,
|
|
88
|
+
onResize
|
|
89
|
+
};
|
|
90
|
+
resizeFollowers.set(stdout, entry);
|
|
91
|
+
stdout.on("resize", onResize);
|
|
92
|
+
}
|
|
93
|
+
const { redraws, onResize } = entry;
|
|
94
|
+
redraws.add(redraw);
|
|
95
|
+
return () => {
|
|
96
|
+
redraws.delete(redraw);
|
|
97
|
+
if (redraws.size === 0 && resizeFollowers.get(stdout) === entry) {
|
|
98
|
+
resizeFollowers.delete(stdout);
|
|
99
|
+
stdout.off("resize", onResize);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
};
|
|
72
103
|
/**
|
|
73
104
|
* The usable terminal size: the stdout Ink draws on, less one column and one row, re-read on every render and when
|
|
74
105
|
* the terminal resizes; or, under a `UiProvider` given a `size`, that size less one column and one row.
|
|
@@ -89,6 +120,9 @@ const known = (reported, fallback) => reported !== void 0 && reported > 0 ? repo
|
|
|
89
120
|
* same paint, so give a long row Ink's `wrap: "truncate-end"` too: on a shrink Ink then clips it rather than letting
|
|
90
121
|
* the terminal wrap it.
|
|
91
122
|
*
|
|
123
|
+
* Every component following one stdout shares a single `resize` listener on it, so a screen of many rows (a
|
|
124
|
+
* `Viewport` of `DocView`s) holds one listener, not one per row.
|
|
125
|
+
*
|
|
92
126
|
* A React hook: call it from a component rendered inside an Ink tree; it needs no screen, but reads a `UiProvider`'s
|
|
93
127
|
* size when there is one.
|
|
94
128
|
*
|
|
@@ -100,15 +134,10 @@ const useTerminalSize = () => {
|
|
|
100
134
|
const override = react.useContext(screenContext())?.size;
|
|
101
135
|
const [, redraw] = react.useReducer((count) => count + 1, 0);
|
|
102
136
|
const followsStdout = override === void 0;
|
|
103
|
-
react.useEffect(() =>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
stdout.off("resize", redraw);
|
|
108
|
-
};
|
|
109
|
-
}, [stdout, followsStdout]);
|
|
110
|
-
const columns = override?.columns ?? stdout.columns;
|
|
111
|
-
const rows = override?.rows ?? stdout.rows;
|
|
137
|
+
react.useEffect(() => followsStdout ? followResize(stdout, redraw) : void 0, [stdout, followsStdout]);
|
|
138
|
+
const reported = stdout;
|
|
139
|
+
const columns = override?.columns ?? reported.columns;
|
|
140
|
+
const rows = override?.rows ?? reported.rows;
|
|
112
141
|
return {
|
|
113
142
|
columns: Math.max(1, known(columns, 80) - 1),
|
|
114
143
|
rows: Math.max(1, known(rows, 24) - 1)
|
package/ui/testing/CliUiTest.js
CHANGED
|
@@ -218,6 +218,10 @@ const LEADING_MOVES = /^(?:\u001b\[[0-9;?]*[A-Za-ln-z])+/;
|
|
|
218
218
|
* moves; a write of moves alone is Ink clearing the frame for a log line, not a frame. stdout and stderr are two
|
|
219
219
|
* streams, each read alone, and `fake.written` is both in the order written: one terminal, for `written` and the
|
|
220
220
|
* transcript.
|
|
221
|
+
*
|
|
222
|
+
* Ink calls `onRender` after it writes a render's frame, in the same synchronous turn, so the frame is the last
|
|
223
|
+
* frame-like write of that turn. A production render whose write Ink's throttle defers to its trailing timer has
|
|
224
|
+
* none yet: the next frame-like write is taken instead.
|
|
221
225
|
*/
|
|
222
226
|
const makeTerminal = (options, settings = { screens: "debug" }) => {
|
|
223
227
|
const columns = options.columns ?? 80;
|
|
@@ -226,25 +230,48 @@ const makeTerminal = (options, settings = { screens: "debug" }) => {
|
|
|
226
230
|
const captures = [];
|
|
227
231
|
let lastWrite = 0;
|
|
228
232
|
let frameDue = false;
|
|
233
|
+
let turn = [];
|
|
234
|
+
/** A chunk read as a frame in `mode`, or `undefined` for one that is not a frame (brackets or moves alone). */
|
|
235
|
+
const asFrame = (chunk, mode) => {
|
|
236
|
+
if (mode === "debug") return chunk;
|
|
237
|
+
const text = chunk.replace(FRAME_BRACKETS, "");
|
|
238
|
+
if (text === "" || CONTROLS_ONLY.test(text)) return void 0;
|
|
239
|
+
return text.replace(LEADING_MOVES, "").replace(/\n+$/, "");
|
|
240
|
+
};
|
|
229
241
|
const fake = makeFakeStreams({
|
|
230
242
|
columns,
|
|
231
243
|
rows,
|
|
232
244
|
onStdoutWrite: (chunk) => {
|
|
233
245
|
lastWrite = Date.now();
|
|
246
|
+
if (turn.length === 0) {
|
|
247
|
+
const opened = turn;
|
|
248
|
+
queueMicrotask(() => {
|
|
249
|
+
if (turn === opened) turn = [];
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
turn.push(chunk);
|
|
234
253
|
const current = captures.at(-1);
|
|
235
254
|
if (!frameDue || current === void 0 || current.ended) return;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
current.raws.push(chunk);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const text = chunk.replace(FRAME_BRACKETS, "");
|
|
242
|
-
if (text === "") return;
|
|
255
|
+
const frame = asFrame(chunk, current.mode);
|
|
256
|
+
if (frame === void 0) return;
|
|
243
257
|
frameDue = false;
|
|
244
|
-
|
|
245
|
-
current.raws.push(text.replace(LEADING_MOVES, "").replace(/\n+$/, ""));
|
|
258
|
+
current.raws.push(frame);
|
|
246
259
|
}
|
|
247
260
|
});
|
|
261
|
+
/** Called as Ink reports a render: take the frame it wrote this turn, or wait for the one its throttle defers. */
|
|
262
|
+
const rendered = () => {
|
|
263
|
+
const current = captures.at(-1);
|
|
264
|
+
if (current === void 0 || current.ended) return;
|
|
265
|
+
for (let index = turn.length - 1; index >= 0; index--) {
|
|
266
|
+
const frame = asFrame(turn[index], current.mode);
|
|
267
|
+
if (frame === void 0) continue;
|
|
268
|
+
turn = [];
|
|
269
|
+
frameDue = false;
|
|
270
|
+
current.raws.push(frame);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
frameDue = true;
|
|
274
|
+
};
|
|
248
275
|
const streams = fake.streams;
|
|
249
276
|
const stream = {
|
|
250
277
|
isTerminal: true,
|
|
@@ -263,9 +290,7 @@ const makeTerminal = (options, settings = { screens: "debug" }) => {
|
|
|
263
290
|
}).pipe(Layer.provide(terminal)), CliInteractive.layerTest(options.interactive ?? true), Layer.succeed(UiStreams, streams), Layer.succeed(UiRenderOptions, {
|
|
264
291
|
...settings.screens === "debug" ? { debug: true } : {},
|
|
265
292
|
maxFps: 1e3,
|
|
266
|
-
onRender:
|
|
267
|
-
frameDue = true;
|
|
268
|
-
},
|
|
293
|
+
onRender: rendered,
|
|
269
294
|
onMount: (kind) => {
|
|
270
295
|
const mode = kind === "live" ? "production" : settings.screens;
|
|
271
296
|
captures.push({
|
|
@@ -6,8 +6,9 @@ const ESC = String.fromCharCode(27);
|
|
|
6
6
|
*
|
|
7
7
|
* @remarks
|
|
8
8
|
* It applies printable text, line feeds, the erase and cursor moves Ink's log-update writes (erase line, cursor up,
|
|
9
|
-
* cursor to column
|
|
10
|
-
*
|
|
9
|
+
* down and forward, cursor to a column, cursor to the next line), and the clears of Ink's full-clear frame: `ESC[J`
|
|
10
|
+
* erases from the cursor to the end of the screen, `ESC[2J` blanks the visible screen, `ESC[3J` drops the scrollback
|
|
11
|
+
* above it, `ESC[H` homes the cursor to the screen's top left. The visible screen is the last
|
|
11
12
|
* `rows` lines; with `rows` unknown, the whole buffer counts as the screen, so a clear takes everything. Every other
|
|
12
13
|
* escape is ignored. It does not wrap a line wider than the terminal. Shared by `CliUiTest.live`'s transcript and the
|
|
13
14
|
* kit's own production-path tests.
|
|
@@ -38,8 +39,16 @@ const screenAfter = (written, rows) => {
|
|
|
38
39
|
}
|
|
39
40
|
} else if (command === "K") lines[row] = "";
|
|
40
41
|
else if (command === "A") row = Math.max(0, row - Number(params === "" ? 1 : params));
|
|
41
|
-
else if (command === "
|
|
42
|
-
|
|
42
|
+
else if (command === "B" || command === "E") {
|
|
43
|
+
row += Number(params === "" ? 1 : params);
|
|
44
|
+
if (command === "E") column = 0;
|
|
45
|
+
while (lines.length <= row) lines.push("");
|
|
46
|
+
} else if (command === "C") column += Number(params === "" ? 1 : params);
|
|
47
|
+
else if (command === "G") column = Math.max(0, Number(params === "" ? 1 : params) - 1);
|
|
48
|
+
else if (command === "J" && (params === "" || params === "0")) {
|
|
49
|
+
lines[row] = (lines[row] ?? "").slice(0, column);
|
|
50
|
+
for (let index = row + 1; index < lines.length; index++) lines[index] = "";
|
|
51
|
+
} else if (command === "J" && params === "2") for (let index = screenTop(); index < lines.length; index++) lines[index] = "";
|
|
43
52
|
else if (command === "J" && params === "3") {
|
|
44
53
|
const top = screenTop();
|
|
45
54
|
lines.splice(0, top);
|
package/ui-testing.d.ts
CHANGED
|
@@ -179,11 +179,14 @@ interface CliUiTestSession {
|
|
|
179
179
|
* before the call is not missed. It waits at most 2 s, then dies naming the screen's number, the text it waited
|
|
180
180
|
* for and how many screens had mounted. The returned screen's frames start at its own mount. A screen that crashed
|
|
181
181
|
* makes `next` die with the crash instead.
|
|
182
|
+
*
|
|
183
|
+
* A `CliUi.live` run counts as a mount too, each run of the view one of its own: a program that draws a live
|
|
184
|
+
* progress view before its first screen takes one `next` per run before the screen's.
|
|
182
185
|
*/
|
|
183
186
|
readonly next: (options?: CliUiTestNextOptions) => Effect.Effect<CliUiTestScreen>;
|
|
184
187
|
/**
|
|
185
188
|
* How many screens have mounted so far: every run that started mounting, one whose thunk threw before Ink drew
|
|
186
|
-
* included.
|
|
189
|
+
* included, and every run of a `CliUi.live` view.
|
|
187
190
|
*/
|
|
188
191
|
readonly mounts: Effect.Effect<number>;
|
|
189
192
|
/** What the program wrote to stdout through `Console` (`log`, `info`, `debug`), one line per call. */
|
package/ui.d.ts
CHANGED
|
@@ -92,13 +92,6 @@ interface LiveOptions<E, S> {
|
|
|
92
92
|
* turns without events. Anything but a positive, finite number is a defect.
|
|
93
93
|
*/
|
|
94
94
|
readonly tickMillis?: number;
|
|
95
|
-
/**
|
|
96
|
-
* Clear React's user-timing entries after every render: `true`, `false`, or `"auto"` (the default), which clears
|
|
97
|
-
* unless `NODE_ENV` is exactly `"production"`. React's development build records them on every render and never
|
|
98
|
-
* clears them. The clear is process-wide: it removes every `measure` entry, a program's own included; marks are left
|
|
99
|
-
* alone.
|
|
100
|
-
*/
|
|
101
|
-
readonly drainPerformance?: boolean | "auto";
|
|
102
95
|
}
|
|
103
96
|
/**
|
|
104
97
|
* The handle of a live view.
|
|
@@ -1779,6 +1772,9 @@ export declare const Styled: (props: StyledProps) => ReactElement;
|
|
|
1779
1772
|
* same paint, so give a long row Ink's `wrap: "truncate-end"` too: on a shrink Ink then clips it rather than letting
|
|
1780
1773
|
* the terminal wrap it.
|
|
1781
1774
|
*
|
|
1775
|
+
* Every component following one stdout shares a single `resize` listener on it, so a screen of many rows (a
|
|
1776
|
+
* `Viewport` of `DocView`s) holds one listener, not one per row.
|
|
1777
|
+
*
|
|
1782
1778
|
* A React hook: call it from a component rendered inside an Ink tree; it needs no screen, but reads a `UiProvider`'s
|
|
1783
1779
|
* size when there is one.
|
|
1784
1780
|
*
|
package/ui/internal/perfDrain.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Config, Effect } from "effect";
|
|
2
|
-
|
|
3
|
-
//#region src/ui/internal/perfDrain.ts
|
|
4
|
-
/**
|
|
5
|
-
* Whether a drain mode drains: `true` and `false` as given, and `"auto"` unless `NODE_ENV` is exactly `"production"`.
|
|
6
|
-
*
|
|
7
|
-
* @remarks
|
|
8
|
-
* React picks its development build whenever `NODE_ENV` is not exactly `"production"`, and that build records
|
|
9
|
-
* user-timing entries on every render and never clears them. An
|
|
10
|
-
* unset `NODE_ENV`, the common case for a CLI, leaks like `"development"`, so `"auto"` drains then too. `NODE_ENV` is
|
|
11
|
-
* read through `Config`, so a test sets it with a `ConfigProvider` and `./ui` reads no `process`.
|
|
12
|
-
*
|
|
13
|
-
* @internal
|
|
14
|
-
*/
|
|
15
|
-
const resolveDrain = (mode) => mode === "auto" ? Config.String("NODE_ENV").pipe(Config.withDefault(""), Effect.map((env) => env !== "production"), Effect.orElseSucceed(() => true)) : Effect.succeed(mode);
|
|
16
|
-
/**
|
|
17
|
-
* Clear every user-timing `measure` entry in the process, when `drain` is true.
|
|
18
|
-
*
|
|
19
|
-
* @remarks
|
|
20
|
-
* The clear is global: it removes a consumer's own measures too, because the platform's `clearMeasures` filters only
|
|
21
|
-
* by name, and React's entries (`Update`, `Mount`, tagged `detail.devtools`) share their names with anything a
|
|
22
|
-
* consumer might call a measure. Marks are left alone: React's development build leaks measures only, never marks,
|
|
23
|
-
* so clearing marks would only take a host's own. A runtime without `performance` is left alone.
|
|
24
|
-
*
|
|
25
|
-
* @internal
|
|
26
|
-
*/
|
|
27
|
-
const drainPerformance = (drain) => {
|
|
28
|
-
if (!drain) return;
|
|
29
|
-
globalThis.performance?.clearMeasures?.();
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
//#endregion
|
|
33
|
-
export { drainPerformance, resolveDrain };
|