@textui/terminal 0.1.0 → 0.3.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/README.md +22 -33
- package/dist/ansi.d.ts +2 -0
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +2 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +19 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/input.d.ts.map +1 -1
- package/dist/input.js +4 -0
- package/dist/node.d.ts +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +11 -1
- package/dist/still.d.ts +71 -0
- package/dist/still.d.ts.map +1 -0
- package/dist/still.js +29 -0
- package/package.json +8 -8
- package/src/ansi.ts +3 -0
- package/src/capabilities.ts +19 -1
- package/src/index.ts +2 -0
- package/src/input.ts +4 -0
- package/src/node.ts +11 -5
- package/src/still.ts +111 -0
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @textui/terminal
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@textui/terminal) [](https://github.com/softov/textui/blob/main/LICENSE)
|
|
4
|
+
|
|
5
|
+
What [`@textui/core`](https://www.npmjs.com/package/@textui/core) needs to reach an actual terminal: adapters, capability detection, ANSI writing and input decoding.
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
8
|
npm install @textui/terminal
|
|
@@ -17,40 +18,27 @@ await app.start();
|
|
|
17
18
|
|
|
18
19
|
## Two adapters
|
|
19
20
|
|
|
20
|
-
`createNodeTerminal()` is the real one - stdin, stdout, resize, and the signal
|
|
21
|
-
handling that puts the screen back the way it found it when the process dies.
|
|
21
|
+
`createNodeTerminal()` is the real one - stdin, stdout, resize, and the signal handling that puts the screen back the way it found it when the process dies.
|
|
22
22
|
|
|
23
|
-
`createVirtualTerminal()` has no tty at all. It is what the test harness and
|
|
24
|
-
`--static` run on, and it is the reason a component that only works
|
|
25
|
-
interactively is a component nobody can test cheaply.
|
|
23
|
+
`createVirtualTerminal()` has no tty at all. It is what the test harness and `--static` run on, and it is the reason a component that only works interactively is a component nobody can test cheaply.
|
|
26
24
|
|
|
27
25
|
## Capabilities are detected, and can be lied to
|
|
28
26
|
|
|
29
|
-
Unicode level, colour depth, mouse, hyperlinks, the kitty keyboard protocol -
|
|
30
|
-
detection is right almost always, and the times it is not are the times nobody
|
|
31
|
-
is watching. So every capability can be forced, which is how you see the
|
|
32
|
-
terminal somebody else is sitting at:
|
|
27
|
+
Unicode level, colour depth, mouse, hyperlinks, the kitty keyboard protocol - detection is right almost always, and the times it is not are the times nobody is watching. So every capability can be forced, which is how you see the terminal somebody else is sitting at:
|
|
33
28
|
|
|
34
29
|
```ts
|
|
35
30
|
createNodeTerminal({ capabilities: { unicode: 'ascii', colors: 4 } });
|
|
36
31
|
```
|
|
37
32
|
|
|
38
|
-
Under the kitty protocol, `ctrl+shift+f` is a distinct stroke from `ctrl+f`.
|
|
39
|
-
Without it the terminal sends the same byte for both, and no amount of decoding
|
|
40
|
-
recovers the difference.
|
|
33
|
+
Under the kitty protocol, `ctrl+shift+f` is a distinct stroke from `ctrl+f`. Without it the terminal sends the same byte for both, and no amount of decoding recovers the difference.
|
|
41
34
|
|
|
42
35
|
## A frame you can keep
|
|
43
36
|
|
|
44
|
-
The screen is the output, so the next redraw destroys the frame that was wrong.
|
|
45
|
-
Two functions write one out instead - both from the buffer the runtime last
|
|
46
|
-
painted, neither needing a tty.
|
|
37
|
+
The screen is the output, so the next redraw destroys the frame that was wrong. Two functions write one out instead - both from the buffer the runtime last painted, neither needing a tty.
|
|
47
38
|
|
|
48
|
-
`captureBuffer` gives a frame a terminal can replay: every cell in order, no
|
|
49
|
-
cursor control, so it can go in a file or an issue. `colors: false` strips the
|
|
50
|
-
colour, which is the copy a diff can read.
|
|
39
|
+
`captureBuffer` gives a frame a terminal can replay: every cell in order, no cursor control, so it can go in a file or an issue. `colors: false` strips the colour, which is the copy a diff can read.
|
|
51
40
|
|
|
52
|
-
`bufferToSvg` gives one a repository page can show, because an `.ans` file is
|
|
53
|
-
only a screenshot on a terminal:
|
|
41
|
+
`bufferToSvg` gives one a repository page can show, because an `.ans` file is only a screenshot on a terminal:
|
|
54
42
|
|
|
55
43
|
```ts
|
|
56
44
|
const svg = bufferToSvg(app.buffer(), {
|
|
@@ -59,25 +47,26 @@ const svg = bufferToSvg(app.buffer(), {
|
|
|
59
47
|
});
|
|
60
48
|
```
|
|
61
49
|
|
|
62
|
-
Self-contained - no font, no stylesheet, no script, nothing fetched - which is
|
|
63
|
-
what survives GitHub's image proxy. And it is text, so a committed screenshot
|
|
64
|
-
diffs and CI can check it instead of somebody re-taking it.
|
|
50
|
+
Self-contained - no font, no stylesheet, no script, nothing fetched - which is what survives GitHub's image proxy. And it is text, so a committed screenshot diffs and CI can check it instead of somebody re-taking it.
|
|
65
51
|
|
|
66
52
|
## What it needs from a stream
|
|
67
53
|
|
|
68
|
-
`TerminalInput` and `TerminalOutput`, which say what the adapter uses rather
|
|
69
|
-
than which stream it is - `setRawMode`, `columns`, `write`, and half a dozen
|
|
70
|
-
more. `process.stdin` and `process.stdout` satisfy them, so nothing changes for
|
|
71
|
-
a caller, and so do the streams of runtimes that are not node.
|
|
54
|
+
`TerminalInput` and `TerminalOutput`, which say what the adapter uses rather than which stream it is - `setRawMode`, `columns`, `write`, and half a dozen more. `process.stdin` and `process.stdout` satisfy them, so nothing changes for a caller, and so do the streams of runtimes that are not node.
|
|
72
55
|
|
|
73
|
-
That is also why the published types name no Node interfaces: a package with no
|
|
74
|
-
dependencies should not need `@types/node` installed to compile.
|
|
56
|
+
That is also why the published types name no Node interfaces: a package with no dependencies should not need `@types/node` installed to compile.
|
|
75
57
|
|
|
76
58
|
## Runtime
|
|
77
59
|
|
|
78
|
-
No dependencies beyond `@textui/core
|
|
79
|
-
no other Node API. Node 22+ and Bun.
|
|
60
|
+
No dependencies beyond [`@textui/core`](https://www.npmjs.com/package/@textui/core). Uses `process` for stdio and signals, and no other Node API. Node 22+ and Bun.
|
|
80
61
|
|
|
81
62
|
## Documentation
|
|
82
63
|
|
|
83
64
|
<https://softov.github.io/textui/>
|
|
65
|
+
|
|
66
|
+
<!-- family -->
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
Part of **[TextUI](https://github.com/softov/textui)** - [documentation](https://softov.github.io/textui/) - [getting started](https://softov.github.io/textui/getting-started.html)
|
|
71
|
+
|
|
72
|
+
[`@textui/kit`](https://www.npmjs.com/package/@textui/kit) one install · [`@textui/core`](https://www.npmjs.com/package/@textui/core) the runtime · [`@textui/widgets`](https://www.npmjs.com/package/@textui/widgets) the catalog · **`@textui/terminal`** adapters and input · [`@textui/testing`](https://www.npmjs.com/package/@textui/testing) the harness · [`@textui/cli`](https://www.npmjs.com/package/@textui/cli) the CLI
|
package/dist/ansi.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare const cursorForward: (n?: number) => string;
|
|
|
23
23
|
export declare const cursorBack: (n?: number) => string;
|
|
24
24
|
export declare const cursorColumn: (x: number) => string;
|
|
25
25
|
/** Cursor shapes, for a text field that wants a bar rather than a block. */
|
|
26
|
+
/** DECSCUSR 0: back to whatever the terminal was configured for. */
|
|
27
|
+
export declare const cursorShapeReset = "\u001B[0 q";
|
|
26
28
|
export declare const cursorShape: (shape: "block" | "underline" | "bar", blinking?: boolean) => string;
|
|
27
29
|
export declare const eraseLine = "\u001B[2K";
|
|
28
30
|
export declare const eraseLineRight = "\u001B[0K";
|
package/dist/ansi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ansi.d.ts","sourceRoot":"","sources":["../src/ansi.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,GAAG,WAAS,CAAC;AAC1B,eAAO,MAAM,GAAG,YAAU,CAAC;AAC3B,eAAO,MAAM,GAAG,YAAU,CAAC;AAC3B,eAAO,MAAM,EAAE,aAAW,CAAC;AAC3B,eAAO,MAAM,GAAG,WAAS,CAAC;AAG1B,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,MAAoC,CAAC;AACrF,eAAO,MAAM,UAAU,aAAY,CAAC;AACpC,eAAO,MAAM,UAAU,gBAAe,CAAC;AACvC,eAAO,MAAM,UAAU,gBAAe,CAAC;AACvC,eAAO,MAAM,UAAU,YAAY,CAAC;AACpC,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,QAAQ,GAAI,UAAK,KAAG,MAAuB,CAAC;AACzD,eAAO,MAAM,UAAU,GAAI,UAAK,KAAG,MAAuB,CAAC;AAC3D,eAAO,MAAM,aAAa,GAAI,UAAK,KAAG,MAAuB,CAAC;AAC9D,eAAO,MAAM,UAAU,GAAI,UAAK,KAAG,MAAuB,CAAC;AAC3D,eAAO,MAAM,YAAY,GAAI,GAAG,MAAM,KAAG,MAA2B,CAAC;AAErE,4EAA4E;AAC5E,eAAO,MAAM,WAAW,GACtB,OAAO,OAAO,GAAG,WAAW,GAAG,KAAK,EACpC,kBAAe,KACd,MAGF,CAAC;AAGF,eAAO,MAAM,SAAS,cAAa,CAAC;AACpC,eAAO,MAAM,cAAc,cAAa,CAAC;AACzC,eAAO,MAAM,WAAW,cAAa,CAAC;AACtC,eAAO,MAAM,SAAS,cAAa,CAAC;AACpC,eAAO,MAAM,QAAQ,GAAI,UAAK,KAAG,MAAuB,CAAC;AAGzD,eAAO,MAAM,cAAc,kBAAiB,CAAC;AAC7C,eAAO,MAAM,cAAc,kBAAiB,CAAC;AAC7C,eAAO,MAAM,gBAAgB,kBAAiB,CAAC;AAC/C,eAAO,MAAM,iBAAiB,kBAAiB,CAAC;AAChD,eAAO,MAAM,aAAa,kBAAiB,CAAC;AAC5C,eAAO,MAAM,cAAc,kBAAiB,CAAC;AAE7C,yEAAyE;AACzE,eAAO,MAAM,OAAO,yDAAqD,CAAC;AAC1E,eAAO,MAAM,QAAQ,yDAAqD,CAAC;AAC3E,wEAAwE;AACxE,eAAO,MAAM,cAAc,4CAAyC,CAAC;AACrE,eAAO,MAAM,eAAe,4CAAyC,CAAC;AAEtE,+DAA+D;AAC/D,eAAO,MAAM,SAAS,kBAAiB,CAAC;AACxC,eAAO,MAAM,OAAO,kBAAiB,CAAC;AAEtC,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,eAAc,CAAC;AAC7C,eAAO,MAAM,gBAAgB,cAAa,CAAC;AAE3C,eAAO,MAAM,OAAO,eAAc,CAAC;AACnC,eAAO,MAAM,MAAM,eAAc,CAAC;AAGlC,eAAO,MAAM,qBAAqB,aAAY,CAAC;AAC/C,eAAO,MAAM,mBAAmB,cAAa,CAAC;AAC9C,uCAAuC;AACvC,eAAO,MAAM,cAAc,4BAA4B,CAAC;AACxD,eAAO,MAAM,SAAS,mBAAkB,CAAC;AAGzC,eAAO,MAAM,QAAQ,GAAI,OAAO,MAAM,KAAG,MAA+C,CAAC;AACzF,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,MAAM,MAAM,KAAG,MACS,CAAC;AAC3D,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM,KAAG,MAA6C,CAAC;AACrF,eAAO,MAAM,SAAS,uBAAmB,CAAC;AAE1C,gEAAgE;AAChE,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,MAK7C,CAAC;AASF,eAAO,MAAM,KAAK,cAAa,CAAC;AAEhC,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;CAmBN,CAAC"}
|
|
1
|
+
{"version":3,"file":"ansi.d.ts","sourceRoot":"","sources":["../src/ansi.ts"],"names":[],"mappings":"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"}
|
package/dist/ansi.js
CHANGED
|
@@ -24,6 +24,8 @@ export const cursorForward = (n = 1) => `${CSI}${n}C`;
|
|
|
24
24
|
export const cursorBack = (n = 1) => `${CSI}${n}D`;
|
|
25
25
|
export const cursorColumn = (x) => `${CSI}${x + 1}G`;
|
|
26
26
|
/** Cursor shapes, for a text field that wants a bar rather than a block. */
|
|
27
|
+
/** DECSCUSR 0: back to whatever the terminal was configured for. */
|
|
28
|
+
export const cursorShapeReset = `${CSI}0 q`;
|
|
27
29
|
export const cursorShape = (shape, blinking = true) => {
|
|
28
30
|
const base = shape === 'block' ? 1 : shape === 'underline' ? 3 : 5;
|
|
29
31
|
return `${CSI}${blinking ? base : base + 1} q`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGxG,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,UAAU,CA4BpG;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,YAAY,CAYnF;AAWD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,oBAAoB,
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGxG,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,UAAU,CA4BpG;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,YAAY,CAYnF;AAWD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,oBAAoB,CAgE9E;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,oBAAoB,EAC1B,SAAS,EAAE,mBAAmB,GAAG,SAAS,GACzC,oBAAoB,CAEtB;AAED,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYjF"}
|
package/dist/capabilities.js
CHANGED
|
@@ -101,7 +101,25 @@ export function detectCapabilities(input) {
|
|
|
101
101
|
paste: true,
|
|
102
102
|
// tmux rewrites OSC 8, and Apple Terminal ignores it.
|
|
103
103
|
hyperlinks: modern && !mux && program !== 'Apple_Terminal',
|
|
104
|
-
|
|
104
|
+
/*
|
|
105
|
+
* OSC 52, and on for anything that is not known to mangle it.
|
|
106
|
+
*
|
|
107
|
+
* This used to require a recognised terminal, which switched it off in
|
|
108
|
+
* precisely the situation it exists for. Over ssh none of the variables
|
|
109
|
+
* that name a terminal survive the hop - `TERM_PROGRAM`, `KITTY_WINDOW_ID`
|
|
110
|
+
* and `WT_SESSION` are all set by the terminal on the machine you are
|
|
111
|
+
* sitting at, not the one the program is running on - so a remote session
|
|
112
|
+
* saw a bare `xterm-256color` and refused to copy. Reaching the clipboard
|
|
113
|
+
* of a machine the program is not running on is the entire reason OSC 52
|
|
114
|
+
* was specified.
|
|
115
|
+
*
|
|
116
|
+
* Assuming it works is safe in a way that assuming most sequences work is
|
|
117
|
+
* not: a terminal that does not implement OSC 52 ignores the whole string
|
|
118
|
+
* rather than printing part of it, because an OSC runs to its terminator.
|
|
119
|
+
* The one that does not is `screen`, which needs the payload wrapped in
|
|
120
|
+
* its own passthrough and shows it as text otherwise.
|
|
121
|
+
*/
|
|
122
|
+
clipboard: !(env.TERM ?? '').startsWith('screen'),
|
|
105
123
|
altScreen: true,
|
|
106
124
|
cursor: true,
|
|
107
125
|
// Synchronized output is safe when supported and harmless when not, but
|
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/input.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAwB,MAAM,cAAc,CAAC;AAiErE,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,YAAY;IAOrB,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,OAAO;IAPjB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,WAAW,CAA8C;gBAGvD,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,EACjC,OAAO,GAAE,cAAmB;IAGtC,iDAAiD;IACjD,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAqBzB,OAAO,CAAC,gBAAgB;IAOxB,sEAAsE;IACtE,OAAO,CAAC,IAAI;IAyDZ,OAAO,CAAC,SAAS;IA8CjB,OAAO,CAAC,SAAS;IAkBjB,wEAAwE;IACxE,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,OAAO;IA8Ff,iEAAiE;IACjE,KAAK,IAAI,IAAI;CAMd;
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAwB,MAAM,cAAc,CAAC;AAiErE,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,YAAY;IAOrB,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,OAAO;IAPjB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,WAAW,CAA8C;gBAGvD,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,EACjC,OAAO,GAAE,cAAmB;IAGtC,iDAAiD;IACjD,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAqBzB,OAAO,CAAC,gBAAgB;IAOxB,sEAAsE;IACtE,OAAO,CAAC,IAAI;IAyDZ,OAAO,CAAC,SAAS;IA8CjB,OAAO,CAAC,SAAS;IAkBjB,wEAAwE;IACxE,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,OAAO;IA8Ff,iEAAiE;IACjE,KAAK,IAAI,IAAI;CAMd;AAgDD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,EACjC,OAAO,CAAC,EAAE,cAAc,GACvB,YAAY,CAEd"}
|
package/dist/input.js
CHANGED
|
@@ -333,6 +333,10 @@ function decodeSgrMouse(body, final) {
|
|
|
333
333
|
x: col - 1,
|
|
334
334
|
y: row - 1,
|
|
335
335
|
ctrl, alt, shift,
|
|
336
|
+
// Stamped on arrival, because nothing downstream can tell a double click
|
|
337
|
+
// from two clicks without knowing when each one landed. The wire says
|
|
338
|
+
// press and release and nothing else.
|
|
339
|
+
at: Date.now(),
|
|
336
340
|
handled: false,
|
|
337
341
|
};
|
|
338
342
|
if (wheel) {
|
package/dist/node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AcquiredState, CapabilityOverrides, Disposable, InputEvent, Size, TerminalAdapter, TerminalCapabilities, TerminalSessionOptions } from '@textui/core';
|
|
1
|
+
import type { AcquiredState, CapabilityOverrides, CursorStyle, Disposable, InputEvent, Size, TerminalAdapter, TerminalCapabilities, TerminalSessionOptions } from '@textui/core';
|
|
2
2
|
/**
|
|
3
3
|
* The Node TTY adapter.
|
|
4
4
|
*
|
|
@@ -91,6 +91,7 @@ export declare class NodeTerminalAdapter implements TerminalAdapter {
|
|
|
91
91
|
onResize(fn: (size: Size) => void): Disposable;
|
|
92
92
|
writeClipboard(text: string): void;
|
|
93
93
|
setTitle(title: string): void;
|
|
94
|
+
setCursorShape(shape: CursorStyle): void;
|
|
94
95
|
private installExitHandlers;
|
|
95
96
|
private removeExitHandlers;
|
|
96
97
|
dispose(): void;
|
package/dist/node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
|
package/dist/node.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createDecoder } from './input.js';
|
|
|
5
5
|
const NOTHING_ACQUIRED = {
|
|
6
6
|
altScreen: false, mouse: false, wheel: false, focusEvents: false,
|
|
7
7
|
paste: false, cursorHidden: false, enhancedKeys: false, rawMode: false,
|
|
8
|
-
titleSet: false,
|
|
8
|
+
titleSet: false, cursorShaped: false,
|
|
9
9
|
};
|
|
10
10
|
export class NodeTerminalAdapter {
|
|
11
11
|
options;
|
|
@@ -147,6 +147,8 @@ export class NodeTerminalAdapter {
|
|
|
147
147
|
out.push(ansi.altScreenLeave);
|
|
148
148
|
if (state.cursorHidden)
|
|
149
149
|
out.push(ansi.cursorShow);
|
|
150
|
+
if (state.cursorShaped)
|
|
151
|
+
out.push(ansi.cursorShapeReset);
|
|
150
152
|
out.push(ansi.reset);
|
|
151
153
|
try {
|
|
152
154
|
this.stdout.write(out.join(''));
|
|
@@ -199,6 +201,14 @@ export class NodeTerminalAdapter {
|
|
|
199
201
|
return;
|
|
200
202
|
this.stdout.write(ansi.setTitle(title));
|
|
201
203
|
}
|
|
204
|
+
setCursorShape(shape) {
|
|
205
|
+
// No session means nothing to put back, and nothing that would put it
|
|
206
|
+
// back - so a shape set outside one is a shape left on the user's shell.
|
|
207
|
+
if (!this.caps.cursor || !this.state)
|
|
208
|
+
return;
|
|
209
|
+
this.stdout.write(ansi.cursorShape(shape));
|
|
210
|
+
this.state.cursorShaped = true;
|
|
211
|
+
}
|
|
202
212
|
installExitHandlers() {
|
|
203
213
|
this.exitHandler = () => this.release();
|
|
204
214
|
process.on('exit', this.exitHandler);
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textui/terminal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Terminal adapters, capability detection, ANSI writing and input decoding for TextUI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terminal",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"import": "./dist/virtual.js"
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
|
-
"dependencies": {
|
|
52
|
-
"@textui/core": "^0.1.0"
|
|
53
|
-
},
|
|
54
|
-
"publishConfig": {
|
|
55
|
-
"access": "public"
|
|
56
|
-
},
|
|
57
51
|
"scripts": {
|
|
58
52
|
"build": "tsc -p tsconfig.json",
|
|
59
53
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
60
54
|
"test": "vitest run"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@textui/core": "^0.3.0"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
61
|
}
|
|
62
|
-
}
|
|
62
|
+
}
|
package/src/ansi.ts
CHANGED
|
@@ -26,6 +26,9 @@ export const cursorBack = (n = 1): string => `${CSI}${n}D`;
|
|
|
26
26
|
export const cursorColumn = (x: number): string => `${CSI}${x + 1}G`;
|
|
27
27
|
|
|
28
28
|
/** Cursor shapes, for a text field that wants a bar rather than a block. */
|
|
29
|
+
/** DECSCUSR 0: back to whatever the terminal was configured for. */
|
|
30
|
+
export const cursorShapeReset = `${CSI}0 q`;
|
|
31
|
+
|
|
29
32
|
export const cursorShape = (
|
|
30
33
|
shape: 'block' | 'underline' | 'bar',
|
|
31
34
|
blinking = true,
|
package/src/capabilities.ts
CHANGED
|
@@ -106,7 +106,25 @@ export function detectCapabilities(input: DetectionInput): TerminalCapabilities
|
|
|
106
106
|
paste: true,
|
|
107
107
|
// tmux rewrites OSC 8, and Apple Terminal ignores it.
|
|
108
108
|
hyperlinks: modern && !mux && program !== 'Apple_Terminal',
|
|
109
|
-
|
|
109
|
+
/*
|
|
110
|
+
* OSC 52, and on for anything that is not known to mangle it.
|
|
111
|
+
*
|
|
112
|
+
* This used to require a recognised terminal, which switched it off in
|
|
113
|
+
* precisely the situation it exists for. Over ssh none of the variables
|
|
114
|
+
* that name a terminal survive the hop - `TERM_PROGRAM`, `KITTY_WINDOW_ID`
|
|
115
|
+
* and `WT_SESSION` are all set by the terminal on the machine you are
|
|
116
|
+
* sitting at, not the one the program is running on - so a remote session
|
|
117
|
+
* saw a bare `xterm-256color` and refused to copy. Reaching the clipboard
|
|
118
|
+
* of a machine the program is not running on is the entire reason OSC 52
|
|
119
|
+
* was specified.
|
|
120
|
+
*
|
|
121
|
+
* Assuming it works is safe in a way that assuming most sequences work is
|
|
122
|
+
* not: a terminal that does not implement OSC 52 ignores the whole string
|
|
123
|
+
* rather than printing part of it, because an OSC runs to its terminator.
|
|
124
|
+
* The one that does not is `screen`, which needs the payload wrapped in
|
|
125
|
+
* its own passthrough and shows it as text otherwise.
|
|
126
|
+
*/
|
|
127
|
+
clipboard: !(env.TERM ?? '').startsWith('screen'),
|
|
110
128
|
altScreen: true,
|
|
111
129
|
cursor: true,
|
|
112
130
|
// Synchronized output is safe when supported and harmless when not, but
|
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/input.ts
CHANGED
|
@@ -362,6 +362,10 @@ function decodeSgrMouse(body: string, final: string): MouseEvent | null {
|
|
|
362
362
|
x: col - 1,
|
|
363
363
|
y: row - 1,
|
|
364
364
|
ctrl, alt, shift,
|
|
365
|
+
// Stamped on arrival, because nothing downstream can tell a double click
|
|
366
|
+
// from two clicks without knowing when each one landed. The wire says
|
|
367
|
+
// press and release and nothing else.
|
|
368
|
+
at: Date.now(),
|
|
365
369
|
handled: false,
|
|
366
370
|
};
|
|
367
371
|
|
package/src/node.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AcquiredState, CapabilityOverrides, Disposable, InputEvent, Size,
|
|
3
|
-
TerminalAdapter, TerminalCapabilities, TerminalSessionOptions,
|
|
4
|
-
} from '@textui/core';
|
|
1
|
+
import type { AcquiredState, CapabilityOverrides, CursorStyle, Disposable, InputEvent, Size, TerminalAdapter, TerminalCapabilities, TerminalSessionOptions } from '@textui/core';
|
|
5
2
|
import { toDisposable } from '@textui/core';
|
|
6
3
|
import * as ansi from './ansi.js';
|
|
7
4
|
import { applyOverrides, detectCapabilities, describeEnvironment } from './capabilities.js';
|
|
@@ -71,7 +68,7 @@ export interface NodeAdapterOptions {
|
|
|
71
68
|
const NOTHING_ACQUIRED: AcquiredState = {
|
|
72
69
|
altScreen: false, mouse: false, wheel: false, focusEvents: false,
|
|
73
70
|
paste: false, cursorHidden: false, enhancedKeys: false, rawMode: false,
|
|
74
|
-
titleSet: false,
|
|
71
|
+
titleSet: false, cursorShaped: false,
|
|
75
72
|
};
|
|
76
73
|
|
|
77
74
|
export class NodeTerminalAdapter implements TerminalAdapter {
|
|
@@ -235,6 +232,7 @@ export class NodeTerminalAdapter implements TerminalAdapter {
|
|
|
235
232
|
if (state.mouse) out.push(state.wheel ? ansi.mouseOff : ansi.mouseButtonsOff);
|
|
236
233
|
if (state.altScreen) out.push(ansi.altScreenLeave);
|
|
237
234
|
if (state.cursorHidden) out.push(ansi.cursorShow);
|
|
235
|
+
if (state.cursorShaped) out.push(ansi.cursorShapeReset);
|
|
238
236
|
out.push(ansi.reset);
|
|
239
237
|
|
|
240
238
|
try {
|
|
@@ -291,6 +289,14 @@ export class NodeTerminalAdapter implements TerminalAdapter {
|
|
|
291
289
|
this.stdout.write(ansi.setTitle(title));
|
|
292
290
|
}
|
|
293
291
|
|
|
292
|
+
setCursorShape(shape: CursorStyle): void {
|
|
293
|
+
// No session means nothing to put back, and nothing that would put it
|
|
294
|
+
// back - so a shape set outside one is a shape left on the user's shell.
|
|
295
|
+
if (!this.caps.cursor || !this.state) return;
|
|
296
|
+
this.stdout.write(ansi.cursorShape(shape));
|
|
297
|
+
this.state.cursorShaped = true;
|
|
298
|
+
}
|
|
299
|
+
|
|
294
300
|
private installExitHandlers(): void {
|
|
295
301
|
this.exitHandler = () => this.release();
|
|
296
302
|
process.on('exit', this.exitHandler);
|
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
|
+
}
|