@textui/terminal 0.1.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/svg.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ import type { CellBuffer, Color, ColorDepth } from '@textui/core';
2
+ /**
3
+ * A frame, as a picture.
4
+ *
5
+ * `captureBuffer` writes a frame you can `cat`. This writes one you can put in
6
+ * a README, and the difference matters more than it sounds: an `.ans` file is
7
+ * only a screenshot on a terminal, so the place a terminal application most
8
+ * needs to show what it looks like - a repository page, a docs site, a pull
9
+ * request - is the one place it cannot.
10
+ *
11
+ * The output is one self-contained SVG with no external anything: no font file,
12
+ * no stylesheet, no script. That is what lets it survive GitHub, which serves
13
+ * markdown images from a sanitising proxy that fetches nothing on the page's
14
+ * behalf.
15
+ *
16
+ * It is also **text**, which is the part worth having. A committed SVG diffs:
17
+ * a change that moves a column or recolours a token shows up as a changed line
18
+ * in review, so a screenshot in the docs can be checked by CI rather than
19
+ * re-taken by hand and trusted.
20
+ */
21
+ export interface SvgOptions {
22
+ /**
23
+ * Cell size in pixels. The defaults are close to a 13px monospace face at a
24
+ * normal line height, which is what makes the picture look like a terminal
25
+ * rather than like text that happens to be in a grid.
26
+ */
27
+ cellWidth?: number;
28
+ cellHeight?: number;
29
+ fontSize?: number;
30
+ /**
31
+ * The font stack. No web font by default and none recommended: a font that
32
+ * has to be fetched is a font that is missing exactly where this file is
33
+ * most useful, and a missing monospace font takes the column alignment with
34
+ * it.
35
+ */
36
+ fontFamily?: string;
37
+ /**
38
+ * Where the baseline sits inside the cell, as a fraction of its height.
39
+ *
40
+ * A number rather than `dominant-baseline`, which renderers disagree about
41
+ * enough that the text lands a pixel or two off between two viewers of the
42
+ * same file.
43
+ */
44
+ baseline?: number;
45
+ /** Space around the grid, in pixels. */
46
+ padding?: number;
47
+ /** Corner radius on the backdrop. Zero is a square edge. */
48
+ radius?: number;
49
+ /**
50
+ * What a cell left at the terminal's own colours becomes.
51
+ *
52
+ * A terminal has no answer for this - "default" means whatever the emulator
53
+ * is configured with - so a picture has to choose, and the honest choice is
54
+ * the caller's. These are what the theme would call `background` and `text`.
55
+ *
56
+ * Passing `default` here is the one thing that cannot work, and a theme can
57
+ * hand it over without meaning to: `mono` is *made* of `default`, so
58
+ * `theme.colors.canvas` and `theme.colors.text` are both it. Either one is
59
+ * replaced with the exporter's own, because a picture with no ink is a
60
+ * rectangle.
61
+ */
62
+ background?: Color;
63
+ foreground?: Color;
64
+ /**
65
+ * Reduce colour first, to show what a shallower terminal would have shown.
66
+ * Left off, nothing is reduced: an SVG has no colour limit of its own, so
67
+ * downsampling by default would be inventing a constraint.
68
+ */
69
+ colorDepth?: ColorDepth;
70
+ /** An accessible name, emitted as `<title>`. */
71
+ title?: string;
72
+ }
73
+ export declare function bufferToSvg(buffer: CellBuffer, options?: SvgOptions): string;
74
+ //# sourceMappingURL=svg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../src/svg.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAMlE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;OAYG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA0BD,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,UAAe,GAAG,MAAM,CAmFhF"}
package/dist/svg.js ADDED
@@ -0,0 +1,235 @@
1
+ import { ATTR_BOLD, ATTR_DIM, ATTR_HIDDEN, ATTR_INVERSE, ATTR_ITALIC, ATTR_STRIKE, ATTR_UNDERLINE, COLOR_DEFAULT, downsample, mix, packColor, toHex, } from '@textui/core';
2
+ const DEFAULTS = {
3
+ cellWidth: 8,
4
+ cellHeight: 17,
5
+ fontSize: 13,
6
+ fontFamily: "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace",
7
+ baseline: 0.78,
8
+ padding: 10,
9
+ radius: 6,
10
+ background: '#0d1117',
11
+ foreground: '#c9d1d9',
12
+ };
13
+ export function bufferToSvg(buffer, options = {}) {
14
+ const cw = options.cellWidth ?? DEFAULTS.cellWidth;
15
+ const ch = options.cellHeight ?? DEFAULTS.cellHeight;
16
+ const fontSize = options.fontSize ?? DEFAULTS.fontSize;
17
+ const fontFamily = options.fontFamily ?? DEFAULTS.fontFamily;
18
+ const baseline = options.baseline ?? DEFAULTS.baseline;
19
+ const pad = options.padding ?? DEFAULTS.padding;
20
+ const radius = options.radius ?? DEFAULTS.radius;
21
+ const depth = options.colorDepth;
22
+ const width = buffer.width * cw + pad * 2;
23
+ const height = buffer.height * ch + pad * 2;
24
+ // Packed, so `inverse` and `dim` are worked out in the same space as every
25
+ // other colour rather than as a special case per attribute. `Color` rather
26
+ // than a hex string because that is what a theme holds: `theme.colors.canvas`
27
+ // goes straight in.
28
+ const paperPacked = given(options.background, DEFAULTS.background);
29
+ const inkPacked = given(options.foreground, DEFAULTS.foreground);
30
+ const paper = toHex(paperPacked);
31
+ const reduce = (c) => (depth === undefined ? c : downsample(c, depth));
32
+ const out = [];
33
+ out.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" `
34
+ + `viewBox="0 0 ${width} ${height}" font-family="${escape(fontFamily)}" `
35
+ + `font-size="${fontSize}px">`);
36
+ if (options.title !== undefined)
37
+ out.push(`<title>${escape(options.title)}</title>`);
38
+ out.push(`<rect width="${width}" height="${height}"`
39
+ + `${radius > 0 ? ` rx="${radius}"` : ''} fill="${paper}"/>`);
40
+ for (let y = 0; y < buffer.height; y++) {
41
+ // Backgrounds first and for the whole row, because a glyph drawn over a
42
+ // rect emitted later would be painted out: SVG has no z-index, only
43
+ // document order.
44
+ for (const run of backgroundRuns(buffer, y, paperPacked, inkPacked, reduce)) {
45
+ out.push(`<rect x="${pad + run.x * cw}" y="${pad + y * ch}" `
46
+ + `width="${run.columns * cw}" height="${ch}" fill="${toHex(run.fg)}"/>`);
47
+ }
48
+ }
49
+ const textY = (y) => Math.round(pad + y * ch + ch * baseline);
50
+ for (let y = 0; y < buffer.height; y++) {
51
+ for (const run of glyphRuns(buffer, y, paperPacked, inkPacked, reduce)) {
52
+ const decoration = [
53
+ (run.attrs & ATTR_UNDERLINE) !== 0 ? 'underline' : '',
54
+ (run.attrs & ATTR_STRIKE) !== 0 ? 'line-through' : '',
55
+ ].filter(Boolean).join(' ');
56
+ out.push(`<text x="${pad + run.x * cw}" y="${textY(y)}" fill="${toHex(run.fg)}"`
57
+ + ((run.attrs & ATTR_BOLD) !== 0 ? ' font-weight="bold"' : '')
58
+ + ((run.attrs & ATTR_ITALIC) !== 0 ? ' font-style="italic"' : '')
59
+ + (decoration !== '' ? ` text-decoration="${decoration}"` : '')
60
+ // The run is told how wide it is, in columns, so the grid holds even
61
+ // where the reader's monospace font has a different advance width from
62
+ // the one `cellWidth` was picked for.
63
+ //
64
+ // Which of the two adjustments only matters when the font is already
65
+ // wrong - where the run's natural width is the right one, both are no
66
+ // ops - and then the two kinds of run want opposite things. Letters
67
+ // want `spacing`: looser tracking is readable and squashed letterforms
68
+ // are not. A bar or a border wants `spacingAndGlyphs`: spacing a run of
69
+ // blocks apart is how a solid bar comes out striped and a box's sides
70
+ // stop meeting its corners, and a block stretched to its cell is still
71
+ // a block.
72
+ + ` textLength="${run.columns * cw}"`
73
+ + ` lengthAdjust="${run.graphic ? 'spacingAndGlyphs' : 'spacing'}"`
74
+ // Leading spaces inside a run are part of the picture, and the default
75
+ // is to collapse them.
76
+ + ` xml:space="preserve">${escape(run.text)}</text>`);
77
+ }
78
+ }
79
+ out.push('</svg>');
80
+ return out.join('\n');
81
+ }
82
+ /**
83
+ * A colour the picture can actually use.
84
+ *
85
+ * `toHex` has nothing to return for `default` and returns black, so a `default`
86
+ * that reaches the file makes paper and ink the same colour and the whole
87
+ * frame disappears into the backdrop. That is not a colour the caller chose; it
88
+ * is the caller having no colour to give, which is what the fallback is for.
89
+ */
90
+ function given(color, fallback) {
91
+ const packed = packColor(color ?? fallback);
92
+ return packed === COLOR_DEFAULT ? packColor(fallback) : packed;
93
+ }
94
+ /**
95
+ * The colours a cell actually paints with.
96
+ *
97
+ * `inverse` is resolved here rather than at either end: it swaps the pair, and
98
+ * a cell that inverts while leaving one side at the terminal default is
99
+ * swapping *that* default in - which is only knowable once both have been
100
+ * filled in. Doing it later would invert a colour against itself.
101
+ */
102
+ function colorsOf(fg, bg, attrs, paper, ink) {
103
+ let front = fg === COLOR_DEFAULT ? ink : fg;
104
+ let back = bg === COLOR_DEFAULT ? paper : bg;
105
+ if ((attrs & ATTR_INVERSE) !== 0)
106
+ [front, back] = [back, front];
107
+ // Dim is a reduced intensity, which on a picture is a colour part of the way
108
+ // to the one behind it - there is no "half as bright" for an arbitrary hex.
109
+ if ((attrs & ATTR_DIM) !== 0)
110
+ front = mix(front, back, 0.5);
111
+ return { fg: front, bg: back };
112
+ }
113
+ /** Background rects for one row, coalesced, skipping the paper colour. */
114
+ function backgroundRuns(buffer, y, paper, ink, reduce) {
115
+ const runs = [];
116
+ let open = null;
117
+ for (let x = 0; x < buffer.width; x++) {
118
+ const cell = buffer.get(x, y);
119
+ // A continuation cell has no colours of its own - it is the right half of
120
+ // the glyph before it, and that cell's run already covers this column.
121
+ if (cell?.continuation === true) {
122
+ if (open)
123
+ open.columns += 1;
124
+ continue;
125
+ }
126
+ // Reduced first, resolved after. A shallower terminal shrinks the palette
127
+ // it was *given*; a cell left at the terminal's own colour has nothing to
128
+ // shrink and still comes out as the emulator's. The other order asks
129
+ // `downsample` for the nearest palette entry to paper and ink, and at depth
130
+ // zero it hands back `default` again - which is how a picture ends up as
131
+ // one black rectangle.
132
+ const back = cell
133
+ ? colorsOf(reduce(packColor(cell.fg)), reduce(packColor(cell.bg)), cell.attrs, paper, ink).bg
134
+ : paper;
135
+ // Nothing to draw where the cell is the same colour as the backdrop, which
136
+ // is most of most screens.
137
+ if (back === paper) {
138
+ open = null;
139
+ continue;
140
+ }
141
+ if (open && open.fg === back && open.x + open.columns === x) {
142
+ open.columns += 1;
143
+ continue;
144
+ }
145
+ open = { x, columns: 1, text: '', fg: back, attrs: 0, graphic: false };
146
+ runs.push(open);
147
+ }
148
+ return runs;
149
+ }
150
+ /**
151
+ * Whether a cell is drawn with the drawing characters rather than with letters.
152
+ *
153
+ * The split is where a monospace font stops being certain to have the glyph.
154
+ * Below U+2000 is text - ASCII, Latin, the punctuation any font ships with -
155
+ * and above it is the box drawing, the block elements, the braille the charts
156
+ * are made of, the arrows and the geometric shapes. A font that lacks those
157
+ * substitutes another one for them, and a substitute is under no obligation to
158
+ * use the same advance width, which is the whole reason a run must not span
159
+ * both: `textLength` corrects a run as a unit, so one wrong-width glyph in a
160
+ * run of letters pushes every letter after it off the grid.
161
+ *
162
+ * The `mono` theme is where this shows, because it is where it is worst. Every
163
+ * other theme colours its bars and its borders differently from its text, and
164
+ * a colour change already ends a run - so the split was there by accident.
165
+ * Give a whole screen one colour and each row coalesces into a single run of
166
+ * letters and blocks together, corrected as one.
167
+ */
168
+ function isGraphic(char) {
169
+ return (char.codePointAt(0) ?? 0x20) >= 0x2000;
170
+ }
171
+ /** Text runs for one row, coalesced by colour, attributes, and kind. */
172
+ function glyphRuns(buffer, y, paper, ink, reduce) {
173
+ const runs = [];
174
+ let open = null;
175
+ for (let x = 0; x < buffer.width; x++) {
176
+ const cell = buffer.get(x, y);
177
+ if (cell?.continuation === true) {
178
+ if (open)
179
+ open.columns += 1;
180
+ continue;
181
+ }
182
+ if (!cell) {
183
+ open = null;
184
+ continue;
185
+ }
186
+ const { fg: front } = colorsOf(reduce(packColor(cell.fg)), reduce(packColor(cell.bg)), cell.attrs, paper, ink);
187
+ // `hidden` is a cell whose glyph is not drawn - a masked field. Its
188
+ // background still is, which is why this is here and not in `colorsOf`.
189
+ const char = (cell.attrs & ATTR_HIDDEN) !== 0 ? ' ' : cell.char;
190
+ const graphic = isGraphic(char);
191
+ // Only the attributes that change how a glyph is drawn. `blink` is not one
192
+ // of them: a still frame cannot blink, and an SMIL animation would make
193
+ // the file un-diffable for a flourish nobody asked for.
194
+ const attrs = cell.attrs & (ATTR_BOLD | ATTR_ITALIC | ATTR_UNDERLINE | ATTR_STRIKE);
195
+ if (open && open.fg === front && open.attrs === attrs && open.graphic === graphic
196
+ && open.x + open.columns === x) {
197
+ open.text += char;
198
+ open.columns += 1;
199
+ continue;
200
+ }
201
+ open = { x, columns: 1, text: char, fg: front, attrs, graphic };
202
+ runs.push(open);
203
+ }
204
+ // The row's own trailing blanks. They coalesce onto the last run because
205
+ // they share its colour, and then every row in the file carries spaces out
206
+ // to the right edge - bytes for nothing, and a `textLength` claiming columns
207
+ // the text does not occupy.
208
+ //
209
+ // Only the tail, and only where nothing is drawn through it: a blank in the
210
+ // middle of a row is between two things, and an underlined one is a line.
211
+ const last = runs[runs.length - 1];
212
+ if (last && (last.attrs & (ATTR_UNDERLINE | ATTR_STRIKE)) === 0) {
213
+ const trimmed = last.text.replace(/\s+$/, '');
214
+ last.columns -= last.text.length - trimmed.length;
215
+ last.text = trimmed;
216
+ }
217
+ // A run of blank cells paints nothing - unless it was underlined or struck
218
+ // through, where the line is the whole point.
219
+ return runs.filter((run) => run.text.trim() !== ''
220
+ || (run.attrs & (ATTR_UNDERLINE | ATTR_STRIKE)) !== 0);
221
+ }
222
+ /**
223
+ * The five XML entities, and no more than that.
224
+ *
225
+ * A frame can hold anything somebody typed, and an unescaped `<` in a filename
226
+ * is the difference between a picture and a file no parser will open.
227
+ */
228
+ function escape(text) {
229
+ return text
230
+ .replace(/&/g, '&amp;')
231
+ .replace(/</g, '&lt;')
232
+ .replace(/>/g, '&gt;')
233
+ .replace(/"/g, '&quot;')
234
+ .replace(/'/g, '&apos;');
235
+ }
@@ -0,0 +1,65 @@
1
+ import type { AcquiredState, CapabilityOverrides, Disposable, InputEvent, Size, TerminalAdapter, TerminalCapabilities, TerminalSessionOptions } from '@textui/core';
2
+ /**
3
+ * A terminal with no terminal behind it.
4
+ *
5
+ * The same adapter serves three cases that turn out to be one case: the test
6
+ * harness, an application embedded in a host that owns the real tty, and a
7
+ * browser-hosted terminal such as xterm.js where output goes to a callback
8
+ * rather than a file descriptor.
9
+ *
10
+ * Because it is embedded by default, `acquire` records nothing it did not do -
11
+ * and it does nothing to anyone's terminal.
12
+ */
13
+ export interface VirtualAdapterOptions {
14
+ width?: number;
15
+ height?: number;
16
+ capabilities?: CapabilityOverrides;
17
+ /** Where encoded output goes. Omit to only accumulate it. */
18
+ onWrite?(data: string): void;
19
+ /** Keep every chunk written, for assertions. */
20
+ record?: boolean;
21
+ /** True when this adapter owns terminal setup. Defaults to false. */
22
+ managed?: boolean;
23
+ }
24
+ export declare class VirtualTerminalAdapter implements TerminalAdapter {
25
+ private options;
26
+ readonly id = "virtual";
27
+ private caps;
28
+ private state;
29
+ private currentSize;
30
+ private pending;
31
+ private written;
32
+ private inputListeners;
33
+ private resizeListeners;
34
+ private decoder;
35
+ private title;
36
+ private clipboard;
37
+ constructor(options?: VirtualAdapterOptions);
38
+ get acquired(): AcquiredState | null;
39
+ size(): Size;
40
+ capabilities(): TerminalCapabilities;
41
+ setCapabilityOverrides(overrides: CapabilityOverrides): void;
42
+ acquire(options: TerminalSessionOptions): AcquiredState;
43
+ release(): void;
44
+ write(data: string): void;
45
+ flush(): void;
46
+ onInput(fn: (event: InputEvent) => void): Disposable;
47
+ onResize(fn: (size: Size) => void): Disposable;
48
+ writeClipboard(text: string): void;
49
+ setTitle(title: string): void;
50
+ /** Feed raw bytes, exactly as a terminal would deliver them. */
51
+ feed(data: string): void;
52
+ /** Deliver an already-decoded event. */
53
+ send(event: InputEvent): void;
54
+ resize(width: number, height: number): void;
55
+ /** Everything written since the last `clearOutput`. */
56
+ output(): string;
57
+ chunks(): string[];
58
+ clearOutput(): void;
59
+ currentTitle(): string | null;
60
+ clipboardContents(): string | null;
61
+ private dispatch;
62
+ dispose(): void;
63
+ }
64
+ export declare function createVirtualTerminal(options?: VirtualAdapterOptions): VirtualTerminalAdapter;
65
+ //# sourceMappingURL=virtual.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"virtual.d.ts","sourceRoot":"","sources":["../src/virtual.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAChE,eAAe,EAAE,oBAAoB,EAAE,sBAAsB,EAC9D,MAAM,cAAc,CAAC;AAItB;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,6DAA6D;IAC7D,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qEAAqE;IACrE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAQD,qBAAa,sBAAuB,YAAW,eAAe;IAchD,OAAO,CAAC,OAAO;IAb3B,QAAQ,CAAC,EAAE,aAAa;IAExB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,WAAW,CAAO;IAC1B,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,cAAc,CAA0C;IAChE,OAAO,CAAC,eAAe,CAAmC;IAC1D,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,SAAS,CAAuB;gBAEpB,OAAO,GAAE,qBAA0B;IASvD,IAAI,QAAQ,IAAI,aAAa,GAAG,IAAI,CAEnC;IAED,IAAI,IAAI,IAAI;IAIZ,YAAY,IAAI,oBAAoB;IAIpC,sBAAsB,CAAC,SAAS,EAAE,mBAAmB,GAAG,IAAI;IAI5D,OAAO,CAAC,OAAO,EAAE,sBAAsB,GAAG,aAAa;IAgBvD,OAAO,IAAI,IAAI;IAKf,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAKzB,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,UAAU;IAKpD,QAAQ,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,GAAG,UAAU;IAK9C,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIlC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM7B,gEAAgE;IAChE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIxB,wCAAwC;IACxC,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAI7B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAM3C,uDAAuD;IACvD,MAAM,IAAI,MAAM;IAIhB,MAAM,IAAI,MAAM,EAAE;IAIlB,WAAW,IAAI,IAAI;IAInB,YAAY,IAAI,MAAM,GAAG,IAAI;IAI7B,iBAAiB,IAAI,MAAM,GAAG,IAAI;IAIlC,OAAO,CAAC,QAAQ;IAQhB,OAAO,IAAI,IAAI;CAKhB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,sBAAsB,CAE7F"}
@@ -0,0 +1,137 @@
1
+ import { FULL_CAPABILITIES, toDisposable } from '@textui/core';
2
+ import { createDecoder } from './input.js';
3
+ const NOTHING_ACQUIRED = {
4
+ altScreen: false, mouse: false, wheel: false, focusEvents: false,
5
+ paste: false, cursorHidden: false, enhancedKeys: false, rawMode: false,
6
+ titleSet: false,
7
+ };
8
+ export class VirtualTerminalAdapter {
9
+ options;
10
+ id = 'virtual';
11
+ caps;
12
+ state = null;
13
+ currentSize;
14
+ pending = [];
15
+ written = [];
16
+ inputListeners = new Set();
17
+ resizeListeners = new Set();
18
+ decoder;
19
+ title = null;
20
+ clipboard = null;
21
+ constructor(options = {}) {
22
+ this.options = options;
23
+ this.currentSize = {
24
+ width: options.width ?? 80,
25
+ height: options.height ?? 24,
26
+ };
27
+ this.caps = { ...FULL_CAPABILITIES, ...options.capabilities };
28
+ this.decoder = createDecoder((event) => this.dispatch(event), { escapeTimeoutMs: 0 });
29
+ }
30
+ get acquired() {
31
+ return this.state;
32
+ }
33
+ size() {
34
+ return this.currentSize;
35
+ }
36
+ capabilities() {
37
+ return this.caps;
38
+ }
39
+ setCapabilityOverrides(overrides) {
40
+ this.caps = { ...this.caps, ...overrides };
41
+ }
42
+ acquire(options) {
43
+ const managed = options.managed ?? this.options.managed ?? false;
44
+ this.state = managed
45
+ ? {
46
+ ...NOTHING_ACQUIRED,
47
+ altScreen: (options.altScreen ?? true) && this.caps.altScreen,
48
+ mouse: (options.mouse ?? false) && this.caps.mouse,
49
+ wheel: (options.wheel ?? true) && this.caps.wheel,
50
+ focusEvents: (options.focusEvents ?? false) && this.caps.focusEvents,
51
+ paste: (options.paste ?? true) && this.caps.paste,
52
+ cursorHidden: (options.hideCursor ?? true) && this.caps.cursor,
53
+ }
54
+ : { ...NOTHING_ACQUIRED };
55
+ return this.state;
56
+ }
57
+ release() {
58
+ this.state = null;
59
+ this.decoder.reset();
60
+ }
61
+ write(data) {
62
+ if (data === '')
63
+ return;
64
+ this.pending.push(data);
65
+ }
66
+ flush() {
67
+ if (this.pending.length === 0)
68
+ return;
69
+ const data = this.pending.join('');
70
+ this.pending = [];
71
+ if (this.options.record !== false)
72
+ this.written.push(data);
73
+ this.options.onWrite?.(data);
74
+ }
75
+ onInput(fn) {
76
+ this.inputListeners.add(fn);
77
+ return toDisposable(() => this.inputListeners.delete(fn));
78
+ }
79
+ onResize(fn) {
80
+ this.resizeListeners.add(fn);
81
+ return toDisposable(() => this.resizeListeners.delete(fn));
82
+ }
83
+ writeClipboard(text) {
84
+ this.clipboard = text;
85
+ }
86
+ setTitle(title) {
87
+ this.title = title;
88
+ }
89
+ // ----------------------------------------------------------- test surface
90
+ /** Feed raw bytes, exactly as a terminal would deliver them. */
91
+ feed(data) {
92
+ this.decoder.feed(data);
93
+ }
94
+ /** Deliver an already-decoded event. */
95
+ send(event) {
96
+ this.dispatch(event);
97
+ }
98
+ resize(width, height) {
99
+ if (width === this.currentSize.width && height === this.currentSize.height)
100
+ return;
101
+ this.currentSize = { width, height };
102
+ for (const fn of [...this.resizeListeners])
103
+ fn(this.currentSize);
104
+ }
105
+ /** Everything written since the last `clearOutput`. */
106
+ output() {
107
+ return this.written.join('');
108
+ }
109
+ chunks() {
110
+ return [...this.written];
111
+ }
112
+ clearOutput() {
113
+ this.written = [];
114
+ }
115
+ currentTitle() {
116
+ return this.title;
117
+ }
118
+ clipboardContents() {
119
+ return this.clipboard;
120
+ }
121
+ dispatch(event) {
122
+ if (event.type === 'resize') {
123
+ this.resize(event.width, event.height);
124
+ return;
125
+ }
126
+ for (const fn of [...this.inputListeners])
127
+ fn(event);
128
+ }
129
+ dispose() {
130
+ this.release();
131
+ this.inputListeners.clear();
132
+ this.resizeListeners.clear();
133
+ }
134
+ }
135
+ export function createVirtualTerminal(options) {
136
+ return new VirtualTerminalAdapter(options);
137
+ }
@@ -0,0 +1,19 @@
1
+ import type { Frame } from '@textui/core';
2
+ import type { TerminalCapabilities } from '@textui/core';
3
+ export declare function fgSequence(color: number, depth: TerminalCapabilities['colorDepth']): string;
4
+ export declare function bgSequence(color: number, depth: TerminalCapabilities['colorDepth']): string;
5
+ export declare const ATTR_ON: [number, number][];
6
+ export declare class Writer {
7
+ private capabilities;
8
+ private state;
9
+ constructor(capabilities: TerminalCapabilities);
10
+ setCapabilities(capabilities: TerminalCapabilities): void;
11
+ /** Forget what we believe the terminal's state to be. After a resize. */
12
+ invalidate(): void;
13
+ /** Encode a frame. Returns an empty string when nothing changed. */
14
+ write(frame: Frame): string;
15
+ private encodeRun;
16
+ private encodeStyle;
17
+ }
18
+ export declare function createWriter(capabilities: TerminalCapabilities): Writer;
19
+ //# sourceMappingURL=writer.d.ts.map
@@ -0,0 +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;AA8BzD,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;IA8B3B,OAAO,CAAC,SAAS;IA0BjB,OAAO,CAAC,WAAW;CA6CpB;AAYD,wBAAgB,YAAY,CAAC,YAAY,EAAE,oBAAoB,GAAG,MAAM,CAEvE"}