@textui/terminal 0.2.0 → 0.4.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/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/still.d.ts +71 -0
- package/dist/still.d.ts.map +1 -0
- package/dist/still.js +29 -0
- package/dist/writer.d.ts.map +1 -1
- package/dist/writer.js +15 -5
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/still.ts +111 -0
- package/src/writer.ts +26 -5
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export * as ansi from './ansi.js';
|
|
|
2
2
|
export { Writer, createWriter } from './writer.js';
|
|
3
3
|
export { captureBuffer } from './capture.js';
|
|
4
4
|
export type { CaptureOptions } from './capture.js';
|
|
5
|
+
export { renderStill } from './still.js';
|
|
6
|
+
export type { Still, StillOptions } from './still.js';
|
|
5
7
|
export { bufferToSvg } from './svg.js';
|
|
6
8
|
export type { SvgOptions } from './svg.js';
|
|
7
9
|
export { detectCapabilities, detectColorDepth, detectUnicode, applyOverrides, describeEnvironment, } from './capabilities.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EACL,kBAAkB,EAAE,gBAAgB,EAAE,aAAa,EACnD,cAAc,EAAE,mBAAmB,GACpC,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACpE,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACnG,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC7E,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EACL,kBAAkB,EAAE,gBAAgB,EAAE,aAAa,EACnD,cAAc,EAAE,mBAAmB,GACpC,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACpE,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACnG,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC7E,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * as ansi from './ansi.js';
|
|
2
2
|
export { Writer, createWriter } from './writer.js';
|
|
3
3
|
export { captureBuffer } from './capture.js';
|
|
4
|
+
export { renderStill } from './still.js';
|
|
4
5
|
export { bufferToSvg } from './svg.js';
|
|
5
6
|
export { detectCapabilities, detectColorDepth, detectUnicode, applyOverrides, describeEnvironment, } from './capabilities.js';
|
|
6
7
|
export { InputDecoder, createDecoder } from './input.js';
|
package/dist/still.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CapabilityOverrides, CellBuffer, CreateAppOptions, TextUIApp } from '@textui/core';
|
|
2
|
+
import type { CaptureOptions } from './capture.js';
|
|
3
|
+
/**
|
|
4
|
+
* One frame of an application, as text.
|
|
5
|
+
*
|
|
6
|
+
* The same application, not a second rendering path: it is mounted against a
|
|
7
|
+
* terminal that is a size and nothing else, rendered until it stops changing,
|
|
8
|
+
* and every cell it painted is what comes back. Which is what a program does
|
|
9
|
+
* when its output is a pipe rather than a screen - there is no frame after
|
|
10
|
+
* this one to correct it, so it has to be the finished picture.
|
|
11
|
+
*
|
|
12
|
+
* Every example in this repository had written this out by hand, and all of
|
|
13
|
+
* them ended the same way: a sleep loop of four milliseconds times a number
|
|
14
|
+
* somebody had tried until the picture looked right. Eight, mostly - four in
|
|
15
|
+
* one, twelve in another - for the same eight lines of setup either side. The
|
|
16
|
+
* number was the tell. Nobody knew it, a small one silently writes a
|
|
17
|
+
* half-drawn frame, and `TextUIApp.settled` is the answer they were all
|
|
18
|
+
* approximating.
|
|
19
|
+
*/
|
|
20
|
+
export interface StillOptions extends Omit<CreateAppOptions, 'terminal' | 'session'> {
|
|
21
|
+
/** Columns. The terminal is only a size, so this is the whole of it. */
|
|
22
|
+
width?: number;
|
|
23
|
+
height?: number;
|
|
24
|
+
/** What the terminal should claim to be able to do. */
|
|
25
|
+
capabilities?: CapabilityOverrides;
|
|
26
|
+
/**
|
|
27
|
+
* Drive it before the frame is taken.
|
|
28
|
+
*
|
|
29
|
+
* A still of an application in its opening state is the least interesting
|
|
30
|
+
* one. This is where a screen is pushed, a message sent, or a scripted host
|
|
31
|
+
* pumped to the point worth photographing - and it may be async, because
|
|
32
|
+
* most of those are.
|
|
33
|
+
*/
|
|
34
|
+
before?(app: TextUIApp): void | Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* The frame is drawn and the application is still alive.
|
|
37
|
+
*
|
|
38
|
+
* For what a string cannot carry and `stop` would take away: the buffer, to
|
|
39
|
+
* crop it to the rows that were used, and the theme, whose two colours are
|
|
40
|
+
* the honest background for a picture of *this* screen rather than the
|
|
41
|
+
* exporter's guess. It runs before the capture, so a buffer changed here is
|
|
42
|
+
* the buffer that comes back as `text`.
|
|
43
|
+
*/
|
|
44
|
+
after?(app: TextUIApp): void | Promise<void>;
|
|
45
|
+
/** Plain text or SGR, and at what depth. Defaults to what the terminal claims. */
|
|
46
|
+
capture?: CaptureOptions;
|
|
47
|
+
/** Settle passes before giving up. See `TextUIApp.settled`. */
|
|
48
|
+
settleLimit?: number;
|
|
49
|
+
}
|
|
50
|
+
export interface Still {
|
|
51
|
+
/** Every cell it painted, rows separated by newlines. */
|
|
52
|
+
text: string;
|
|
53
|
+
/**
|
|
54
|
+
* The cells themselves, for anything text cannot carry - `bufferToSvg`, a
|
|
55
|
+
* pixel diff, an assertion about one cell's colour. Valid after the
|
|
56
|
+
* application has stopped: nothing writes to it again.
|
|
57
|
+
*/
|
|
58
|
+
buffer: CellBuffer;
|
|
59
|
+
/**
|
|
60
|
+
* Whether it went quiet, or the limit ran out first.
|
|
61
|
+
*
|
|
62
|
+
* An application that animates settles between its frames, so this is
|
|
63
|
+
* `true` for one of those and the picture is a photograph of something
|
|
64
|
+
* moving. `false` means the passes never stopped producing work - a render
|
|
65
|
+
* loop that does not converge - and the frame was taken anyway, because a
|
|
66
|
+
* still is better evidence of that than nothing is.
|
|
67
|
+
*/
|
|
68
|
+
settled: boolean;
|
|
69
|
+
}
|
|
70
|
+
export declare function renderStill(options?: StillOptions): Promise<Still>;
|
|
71
|
+
//# sourceMappingURL=still.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"still.d.ts","sourceRoot":"","sources":["../src/still.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EAAE,UAAU,EAAE,gBAAgB,EAAE,SAAS,EAC7D,MAAM,cAAc,CAAC;AAKtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,YAAa,SAAQ,IAAI,CAAC,gBAAgB,EAAE,UAAU,GAAG,SAAS,CAAC;IAClF,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC;;;;;;;OAOG;IACH,MAAM,CAAC,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C;;;;;;;;OAQG;IACH,KAAK,CAAC,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,kFAAkF;IAClF,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IACpB,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;;;;OAQG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,KAAK,CAAC,CAgC5E"}
|
package/dist/still.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { WRITER_KEY, createApp } from '@textui/core';
|
|
2
|
+
import { createVirtualTerminal } from './virtual.js';
|
|
3
|
+
import { createWriter } from './writer.js';
|
|
4
|
+
import { captureBuffer } from './capture.js';
|
|
5
|
+
export async function renderStill(options = {}) {
|
|
6
|
+
const { width = 80, height = 24, capabilities, before, after, capture, settleLimit, ...app } = options;
|
|
7
|
+
const terminal = createVirtualTerminal({
|
|
8
|
+
width,
|
|
9
|
+
height,
|
|
10
|
+
...(capabilities ? { capabilities } : {}),
|
|
11
|
+
});
|
|
12
|
+
const created = createApp({ ...app, terminal });
|
|
13
|
+
// The writer is provided even though the buffer is what is read: a virtual
|
|
14
|
+
// terminal records what was written to it, and a still that quietly stopped
|
|
15
|
+
// producing that would break anything reading the bytes rather than the
|
|
16
|
+
// cells.
|
|
17
|
+
created.services.provide(WRITER_KEY, createWriter(terminal.capabilities()));
|
|
18
|
+
await created.start();
|
|
19
|
+
await before?.(created);
|
|
20
|
+
const settled = await created.settled(settleLimit === undefined ? {} : { limit: settleLimit });
|
|
21
|
+
// Even when it did not settle: the limit means "stop waiting", not "give up
|
|
22
|
+
// on the frame", and a still of a moving thing is still a still.
|
|
23
|
+
created.flush();
|
|
24
|
+
await after?.(created);
|
|
25
|
+
const text = captureBuffer(created.buffer(), terminal.capabilities(), capture ?? {});
|
|
26
|
+
const buffer = created.buffer();
|
|
27
|
+
await created.stop();
|
|
28
|
+
return { text, buffer, settled };
|
|
29
|
+
}
|
package/dist/writer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAO,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAO,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AA0CzD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,oBAAoB,CAAC,YAAY,CAAC,GAAG,MAAM,CAU3F;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,oBAAoB,CAAC,YAAY,CAAC,GAAG,MAAM,CAU3F;AAED,eAAO,MAAM,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EASrC,CAAC;AAMF,qBAAa,MAAM;IAGL,OAAO,CAAC,YAAY;IAFhC,OAAO,CAAC,KAAK,CAAgB;gBAET,YAAY,EAAE,oBAAoB;IAEtD,eAAe,CAAC,YAAY,EAAE,oBAAoB,GAAG,IAAI;IAKzD,yEAAyE;IACzE,UAAU,IAAI,IAAI;IAIlB,oEAAoE;IACpE,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM;IAuC3B,OAAO,CAAC,SAAS;IA0BjB,OAAO,CAAC,WAAW;CA6CpB;AAYD,wBAAgB,YAAY,CAAC,YAAY,EAAE,oBAAoB,GAAG,MAAM,CAEvE"}
|
package/dist/writer.js
CHANGED
|
@@ -2,7 +2,10 @@ import { COLOR_DEFAULT, downsample, isRgb, unpackRgb } from '@textui/core';
|
|
|
2
2
|
import { ATTR_BLINK, ATTR_BOLD, ATTR_DIM, ATTR_HIDDEN, ATTR_INVERSE, ATTR_ITALIC, ATTR_STRIKE, ATTR_UNDERLINE, } from '@textui/core';
|
|
3
3
|
import * as ansi from './ansi.js';
|
|
4
4
|
function freshState() {
|
|
5
|
-
return {
|
|
5
|
+
return {
|
|
6
|
+
fg: COLOR_DEFAULT, bg: COLOR_DEFAULT, attrs: 0, link: undefined,
|
|
7
|
+
x: -1, y: -1, valid: false, cursorShown: false,
|
|
8
|
+
};
|
|
6
9
|
}
|
|
7
10
|
export function fgSequence(color, depth) {
|
|
8
11
|
const c = downsample(color, depth);
|
|
@@ -61,7 +64,9 @@ export class Writer {
|
|
|
61
64
|
}
|
|
62
65
|
/** Encode a frame. Returns an empty string when nothing changed. */
|
|
63
66
|
write(frame) {
|
|
64
|
-
|
|
67
|
+
// Nothing to paint, nowhere to put the caret, and none on screen to take
|
|
68
|
+
// away. A frame that has to *hide* one still has work to do.
|
|
69
|
+
if (frame.runs.length === 0 && !frame.cursor && !this.state.cursorShown)
|
|
65
70
|
return '';
|
|
66
71
|
const out = [];
|
|
67
72
|
const sync = this.capabilities.synchronizedOutput;
|
|
@@ -69,9 +74,12 @@ export class Writer {
|
|
|
69
74
|
out.push(ansi.syncStart);
|
|
70
75
|
// A frame is painted with the cursor hidden; showing it once at the end
|
|
71
76
|
// is the difference between a steady caret and one that streaks.
|
|
72
|
-
const hideForPaint = this.capabilities.cursor
|
|
73
|
-
|
|
77
|
+
const hideForPaint = this.capabilities.cursor
|
|
78
|
+
&& (frame.runs.length > 0 || (this.state.cursorShown && !frame.cursor));
|
|
79
|
+
if (hideForPaint) {
|
|
74
80
|
out.push(ansi.cursorHide);
|
|
81
|
+
this.state.cursorShown = false;
|
|
82
|
+
}
|
|
75
83
|
for (const run of frame.runs)
|
|
76
84
|
out.push(this.encodeRun(run));
|
|
77
85
|
if (this.state.link !== undefined) {
|
|
@@ -82,8 +90,10 @@ export class Writer {
|
|
|
82
90
|
out.push(ansi.cursorTo(frame.cursor.x, frame.cursor.y));
|
|
83
91
|
this.state.x = frame.cursor.x;
|
|
84
92
|
this.state.y = frame.cursor.y;
|
|
85
|
-
if (frame.cursor.visible)
|
|
93
|
+
if (frame.cursor.visible) {
|
|
86
94
|
out.push(ansi.cursorShow);
|
|
95
|
+
this.state.cursorShown = true;
|
|
96
|
+
}
|
|
87
97
|
}
|
|
88
98
|
if (sync)
|
|
89
99
|
out.push(ansi.syncEnd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textui/terminal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Terminal adapters, capability detection, ANSI writing and input decoding for TextUI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terminal",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"test": "vitest run"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@textui/core": "^0.
|
|
57
|
+
"@textui/core": "^0.4.0"
|
|
58
58
|
},
|
|
59
59
|
"publishConfig": {
|
|
60
60
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export * as ansi from './ansi.js';
|
|
|
2
2
|
export { Writer, createWriter } from './writer.js';
|
|
3
3
|
export { captureBuffer } from './capture.js';
|
|
4
4
|
export type { CaptureOptions } from './capture.js';
|
|
5
|
+
export { renderStill } from './still.js';
|
|
6
|
+
export type { Still, StillOptions } from './still.js';
|
|
5
7
|
export { bufferToSvg } from './svg.js';
|
|
6
8
|
export type { SvgOptions } from './svg.js';
|
|
7
9
|
export {
|
package/src/still.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CapabilityOverrides, CellBuffer, CreateAppOptions, TextUIApp,
|
|
3
|
+
} from '@textui/core';
|
|
4
|
+
import { WRITER_KEY, createApp } from '@textui/core';
|
|
5
|
+
import { createVirtualTerminal } from './virtual.js';
|
|
6
|
+
import { createWriter } from './writer.js';
|
|
7
|
+
import { captureBuffer } from './capture.js';
|
|
8
|
+
import type { CaptureOptions } from './capture.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One frame of an application, as text.
|
|
12
|
+
*
|
|
13
|
+
* The same application, not a second rendering path: it is mounted against a
|
|
14
|
+
* terminal that is a size and nothing else, rendered until it stops changing,
|
|
15
|
+
* and every cell it painted is what comes back. Which is what a program does
|
|
16
|
+
* when its output is a pipe rather than a screen - there is no frame after
|
|
17
|
+
* this one to correct it, so it has to be the finished picture.
|
|
18
|
+
*
|
|
19
|
+
* Every example in this repository had written this out by hand, and all of
|
|
20
|
+
* them ended the same way: a sleep loop of four milliseconds times a number
|
|
21
|
+
* somebody had tried until the picture looked right. Eight, mostly - four in
|
|
22
|
+
* one, twelve in another - for the same eight lines of setup either side. The
|
|
23
|
+
* number was the tell. Nobody knew it, a small one silently writes a
|
|
24
|
+
* half-drawn frame, and `TextUIApp.settled` is the answer they were all
|
|
25
|
+
* approximating.
|
|
26
|
+
*/
|
|
27
|
+
export interface StillOptions extends Omit<CreateAppOptions, 'terminal' | 'session'> {
|
|
28
|
+
/** Columns. The terminal is only a size, so this is the whole of it. */
|
|
29
|
+
width?: number;
|
|
30
|
+
height?: number;
|
|
31
|
+
/** What the terminal should claim to be able to do. */
|
|
32
|
+
capabilities?: CapabilityOverrides;
|
|
33
|
+
/**
|
|
34
|
+
* Drive it before the frame is taken.
|
|
35
|
+
*
|
|
36
|
+
* A still of an application in its opening state is the least interesting
|
|
37
|
+
* one. This is where a screen is pushed, a message sent, or a scripted host
|
|
38
|
+
* pumped to the point worth photographing - and it may be async, because
|
|
39
|
+
* most of those are.
|
|
40
|
+
*/
|
|
41
|
+
before?(app: TextUIApp): void | Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* The frame is drawn and the application is still alive.
|
|
44
|
+
*
|
|
45
|
+
* For what a string cannot carry and `stop` would take away: the buffer, to
|
|
46
|
+
* crop it to the rows that were used, and the theme, whose two colours are
|
|
47
|
+
* the honest background for a picture of *this* screen rather than the
|
|
48
|
+
* exporter's guess. It runs before the capture, so a buffer changed here is
|
|
49
|
+
* the buffer that comes back as `text`.
|
|
50
|
+
*/
|
|
51
|
+
after?(app: TextUIApp): void | Promise<void>;
|
|
52
|
+
/** Plain text or SGR, and at what depth. Defaults to what the terminal claims. */
|
|
53
|
+
capture?: CaptureOptions;
|
|
54
|
+
/** Settle passes before giving up. See `TextUIApp.settled`. */
|
|
55
|
+
settleLimit?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Still {
|
|
59
|
+
/** Every cell it painted, rows separated by newlines. */
|
|
60
|
+
text: string;
|
|
61
|
+
/**
|
|
62
|
+
* The cells themselves, for anything text cannot carry - `bufferToSvg`, a
|
|
63
|
+
* pixel diff, an assertion about one cell's colour. Valid after the
|
|
64
|
+
* application has stopped: nothing writes to it again.
|
|
65
|
+
*/
|
|
66
|
+
buffer: CellBuffer;
|
|
67
|
+
/**
|
|
68
|
+
* Whether it went quiet, or the limit ran out first.
|
|
69
|
+
*
|
|
70
|
+
* An application that animates settles between its frames, so this is
|
|
71
|
+
* `true` for one of those and the picture is a photograph of something
|
|
72
|
+
* moving. `false` means the passes never stopped producing work - a render
|
|
73
|
+
* loop that does not converge - and the frame was taken anyway, because a
|
|
74
|
+
* still is better evidence of that than nothing is.
|
|
75
|
+
*/
|
|
76
|
+
settled: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function renderStill(options: StillOptions = {}): Promise<Still> {
|
|
80
|
+
const {
|
|
81
|
+
width = 80, height = 24, capabilities, before, after, capture, settleLimit, ...app
|
|
82
|
+
} = options;
|
|
83
|
+
|
|
84
|
+
const terminal = createVirtualTerminal({
|
|
85
|
+
width,
|
|
86
|
+
height,
|
|
87
|
+
...(capabilities ? { capabilities } : {}),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const created = createApp({ ...app, terminal });
|
|
91
|
+
// The writer is provided even though the buffer is what is read: a virtual
|
|
92
|
+
// terminal records what was written to it, and a still that quietly stopped
|
|
93
|
+
// producing that would break anything reading the bytes rather than the
|
|
94
|
+
// cells.
|
|
95
|
+
created.services.provide(WRITER_KEY, createWriter(terminal.capabilities()));
|
|
96
|
+
|
|
97
|
+
await created.start();
|
|
98
|
+
await before?.(created);
|
|
99
|
+
|
|
100
|
+
const settled = await created.settled(settleLimit === undefined ? {} : { limit: settleLimit });
|
|
101
|
+
// Even when it did not settle: the limit means "stop waiting", not "give up
|
|
102
|
+
// on the frame", and a still of a moving thing is still a still.
|
|
103
|
+
created.flush();
|
|
104
|
+
await after?.(created);
|
|
105
|
+
|
|
106
|
+
const text = captureBuffer(created.buffer(), terminal.capabilities(), capture ?? {});
|
|
107
|
+
const buffer = created.buffer();
|
|
108
|
+
await created.stop();
|
|
109
|
+
|
|
110
|
+
return { text, buffer, settled };
|
|
111
|
+
}
|
package/src/writer.ts
CHANGED
|
@@ -23,10 +23,22 @@ interface WriterState {
|
|
|
23
23
|
x: number;
|
|
24
24
|
y: number;
|
|
25
25
|
valid: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the terminal is currently showing its cursor.
|
|
28
|
+
*
|
|
29
|
+
* Tracked because a frame can need the cursor *taken away* without painting
|
|
30
|
+
* anything: focus moving from a text field to a control that has no caret
|
|
31
|
+
* changes no cell the field did not already own, and the cursor was left
|
|
32
|
+
* sitting in the field somebody had just tabbed out of.
|
|
33
|
+
*/
|
|
34
|
+
cursorShown: boolean;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
function freshState(): WriterState {
|
|
29
|
-
return {
|
|
38
|
+
return {
|
|
39
|
+
fg: COLOR_DEFAULT, bg: COLOR_DEFAULT, attrs: 0, link: undefined,
|
|
40
|
+
x: -1, y: -1, valid: false, cursorShown: false,
|
|
41
|
+
};
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
export function fgSequence(color: number, depth: TerminalCapabilities['colorDepth']): string {
|
|
@@ -85,7 +97,9 @@ export class Writer {
|
|
|
85
97
|
|
|
86
98
|
/** Encode a frame. Returns an empty string when nothing changed. */
|
|
87
99
|
write(frame: Frame): string {
|
|
88
|
-
|
|
100
|
+
// Nothing to paint, nowhere to put the caret, and none on screen to take
|
|
101
|
+
// away. A frame that has to *hide* one still has work to do.
|
|
102
|
+
if (frame.runs.length === 0 && !frame.cursor && !this.state.cursorShown) return '';
|
|
89
103
|
|
|
90
104
|
const out: string[] = [];
|
|
91
105
|
const sync = this.capabilities.synchronizedOutput;
|
|
@@ -93,8 +107,12 @@ export class Writer {
|
|
|
93
107
|
|
|
94
108
|
// A frame is painted with the cursor hidden; showing it once at the end
|
|
95
109
|
// is the difference between a steady caret and one that streaks.
|
|
96
|
-
const hideForPaint = this.capabilities.cursor
|
|
97
|
-
|
|
110
|
+
const hideForPaint = this.capabilities.cursor
|
|
111
|
+
&& (frame.runs.length > 0 || (this.state.cursorShown && !frame.cursor));
|
|
112
|
+
if (hideForPaint) {
|
|
113
|
+
out.push(ansi.cursorHide);
|
|
114
|
+
this.state.cursorShown = false;
|
|
115
|
+
}
|
|
98
116
|
|
|
99
117
|
for (const run of frame.runs) out.push(this.encodeRun(run));
|
|
100
118
|
|
|
@@ -107,7 +125,10 @@ export class Writer {
|
|
|
107
125
|
out.push(ansi.cursorTo(frame.cursor.x, frame.cursor.y));
|
|
108
126
|
this.state.x = frame.cursor.x;
|
|
109
127
|
this.state.y = frame.cursor.y;
|
|
110
|
-
if (frame.cursor.visible)
|
|
128
|
+
if (frame.cursor.visible) {
|
|
129
|
+
out.push(ansi.cursorShow);
|
|
130
|
+
this.state.cursorShown = true;
|
|
131
|
+
}
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
if (sync) out.push(ansi.syncEnd);
|