@travetto/terminal 3.0.0-rc.10
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 +9 -0
- package/__index__.ts +10 -0
- package/package.json +33 -0
- package/src/codes.ts +41 -0
- package/src/color-define.ts +195 -0
- package/src/color-output.ts +127 -0
- package/src/iterable.ts +100 -0
- package/src/named-colors.ts +160 -0
- package/src/operation.ts +134 -0
- package/src/query.ts +96 -0
- package/src/terminal.ts +207 -0
- package/src/types.ts +33 -0
- package/src/writer.ts +146 -0
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- This file was generated by @travetto/doc and should not be modified directly -->
|
|
2
|
+
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/terminal/DOC.ts and execute "npx trv doc" to rebuild -->
|
|
3
|
+
# Terminal
|
|
4
|
+
## General terminal support
|
|
5
|
+
|
|
6
|
+
**Install: @travetto/terminal**
|
|
7
|
+
```bash
|
|
8
|
+
npm install @travetto/terminal
|
|
9
|
+
```
|
package/__index__.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './src/codes';
|
|
2
|
+
export * from './src/color-define';
|
|
3
|
+
export * from './src/color-output';
|
|
4
|
+
export * from './src/iterable';
|
|
5
|
+
export * from './src/named-colors';
|
|
6
|
+
export * from './src/operation';
|
|
7
|
+
export * from './src/query';
|
|
8
|
+
export * from './src/terminal';
|
|
9
|
+
export * from './src/types';
|
|
10
|
+
export * from './src/writer';
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@travetto/terminal",
|
|
3
|
+
"version": "3.0.0-rc.10",
|
|
4
|
+
"description": "General terminal support",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"terminal",
|
|
7
|
+
"ansi256",
|
|
8
|
+
"travetto",
|
|
9
|
+
"typescript"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://travetto.io",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": {
|
|
14
|
+
"email": "travetto.framework@gmail.com",
|
|
15
|
+
"name": "Travetto Framework"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"__index__.ts",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"main": "__index__.ts",
|
|
22
|
+
"repository": {
|
|
23
|
+
"url": "https://github.com/travetto/travetto.git",
|
|
24
|
+
"directory": "module/terminal"
|
|
25
|
+
},
|
|
26
|
+
"travetto": {
|
|
27
|
+
"displayName": "Terminal"
|
|
28
|
+
},
|
|
29
|
+
"private": false,
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/codes.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const ESC = '\x1b[';
|
|
2
|
+
const OSC = '\x1b]';
|
|
3
|
+
const ST = '\x1b\\';
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line no-control-regex
|
|
6
|
+
export const ANSI_CODE_REGEX = /(\x1b|\x1B)[\[\]][?]?[0-9;]+[A-Za-z]/g;
|
|
7
|
+
|
|
8
|
+
export const OSC_QUERY_FIELDS = {
|
|
9
|
+
backgroundColor: 11,
|
|
10
|
+
foregroundColor: 10
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type OSCQueryField = keyof (typeof OSC_QUERY_FIELDS);
|
|
14
|
+
|
|
15
|
+
export const DEVICE_STATUS_FIELDS = {
|
|
16
|
+
cursorPosition: 6,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type DeviceStatusField = keyof (typeof DEVICE_STATUS_FIELDS);
|
|
20
|
+
|
|
21
|
+
export const ANSICodes = {
|
|
22
|
+
DEBUG: (text: string): string => text.replaceAll(ESC, '<ESC>').replaceAll(OSC, '<OSC>').replaceAll('\n', '<NL>').replaceAll(ST, '<ST>'),
|
|
23
|
+
CURSOR_DY: (row: number): string => `${ESC}${Math.abs(row)}${row < 0 ? 'A' : 'B'}`,
|
|
24
|
+
CURSOR_DX: (col: number): string => `${ESC}${Math.abs(col)}${col < 0 ? 'D' : 'C'}`,
|
|
25
|
+
COLUMN_SET: (col: number): string => `${ESC}${col}G`,
|
|
26
|
+
POSITION_SET: (row: number, col: number): string => `${ESC}${row};${col}H`,
|
|
27
|
+
ERASE_SCREEN: (dir: 0 | 1 | 2 | 3 = 0): string => `${ESC}${dir}J`,
|
|
28
|
+
ERASE_LINE: (dir: 0 | 1 | 2 = 0): string => `${ESC}${dir}K`,
|
|
29
|
+
STYLE: (codes: (string | number)[]): string => `${ESC}${codes.join(';')}m`,
|
|
30
|
+
SHOW_CURSOR: (): string => `${ESC}?25h`,
|
|
31
|
+
HIDE_CURSOR: (): string => `${ESC}?25l`,
|
|
32
|
+
SCROLL_RANGE_SET: (start: number, end: number): string => `${ESC}${start};${end}r`,
|
|
33
|
+
SCROLL_RANGE_CLEAR: (): string => `${ESC}r`,
|
|
34
|
+
SCROLL_WINDOW: (rows: number): string => `${ESC}${Math.abs(rows)}${rows < 0 ? 'S' : 'T'}`,
|
|
35
|
+
POSITION_RESTORE: (): string => `${ESC}u`,
|
|
36
|
+
POSITION_SAVE: (): string => `${ESC}s`,
|
|
37
|
+
DEVICE_STATUS_REPORT: (code: DeviceStatusField): string => `${ESC}${DEVICE_STATUS_FIELDS[code]}n`,
|
|
38
|
+
OSC_QUERY: (code: OSCQueryField): string => `${OSC}${OSC_QUERY_FIELDS[code]};?${ST}`,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const stripAnsiCodes = (text: string): string => text.replace(ANSI_CODE_REGEX, '');
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { NAMED_COLORS } from './named-colors';
|
|
2
|
+
import { RGB, TermColorScheme } from './types';
|
|
3
|
+
|
|
4
|
+
type I = number;
|
|
5
|
+
type HSL = [h: I, s: I, l: I];
|
|
6
|
+
export type DefinedColor = { rgb: RGB, hsl: HSL, idx16: I, idx16bg: I, idx256: I, scheme: TermColorScheme };
|
|
7
|
+
export type RGBInput = I | keyof (typeof NAMED_COLORS) | `#${string}`;
|
|
8
|
+
|
|
9
|
+
const _rgb = (r: I, g: I = r, b: I = g): RGB => [r, g, b];
|
|
10
|
+
|
|
11
|
+
const STD_PRE = ['std', 'bright'] as const;
|
|
12
|
+
const STD_COLORS = ['Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White'] as const;
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line no-bitwise
|
|
15
|
+
const toRgbArray = (val: number): RGB => [(val >> 16) & 255, (val >> 8) & 255, val & 255];
|
|
16
|
+
|
|
17
|
+
const ANSI256_TO_16_IDX = [30, 31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97];
|
|
18
|
+
const ANSI16_TO_BG = new Map(ANSI256_TO_16_IDX.map(x => [x, x + 10]));
|
|
19
|
+
|
|
20
|
+
// Inspired/sourced from: https://github.com/mina86/ansi_colours/blob/master/src/ansi256.rs
|
|
21
|
+
const ANSI256_GRAY_MAPPING = [
|
|
22
|
+
[16, 5],
|
|
23
|
+
[232, 9], [233, 10], [234, 10], [235, 10], [236, 10], [237, 10], [238, 10], [239, 10], [240, 8],
|
|
24
|
+
[59, 5],
|
|
25
|
+
[241, 7], [242, 10], [243, 9], [244, 9],
|
|
26
|
+
[102, 5],
|
|
27
|
+
[245, 6], [246, 10], [247, 10], [248, 9],
|
|
28
|
+
[145, 5],
|
|
29
|
+
[249, 6], [250, 10], [251, 10], [252, 9],
|
|
30
|
+
[188, 5],
|
|
31
|
+
[253, 6], [254, 10], [255, 14],
|
|
32
|
+
[231, 9]
|
|
33
|
+
].flatMap(([v, r]) => new Array<number>(v).fill(r));
|
|
34
|
+
|
|
35
|
+
const STEPS_256 = [0, 95, 135, 175, 215, 255];
|
|
36
|
+
|
|
37
|
+
const ANSI256_TO_RGB: RGB[] = [
|
|
38
|
+
...STD_PRE.flatMap(p => STD_COLORS.map(c => NAMED_COLORS[`${p}${c}`]).map(toRgbArray)),
|
|
39
|
+
...STEPS_256.flatMap(r => STEPS_256.flatMap(g => STEPS_256.map(b => _rgb(r, g, b)))),
|
|
40
|
+
...new Array(24).fill(0).map((_, i) => _rgb(i * 10 + 8)) // Grays
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Pulled from: https://github.com/mina86/ansi_colours/blob/master/src/ansi256.rs
|
|
44
|
+
const RED_256_LEVELS = [38, 115, 155, 196, 235];
|
|
45
|
+
const GREEN_256_LEVELS = [38, 115, 155, 196, 235];
|
|
46
|
+
const BLUE_256_LEVELS = [35, 115, 155, 195, 235];
|
|
47
|
+
|
|
48
|
+
export class ColorDefineUtil {
|
|
49
|
+
static CACHE = new Map<number | string, DefinedColor>();
|
|
50
|
+
|
|
51
|
+
static #snapToLevel(levels: number[], val: number): number {
|
|
52
|
+
for (let i = 0; i < levels.length; i += 1) {
|
|
53
|
+
if (val < levels[i]) { return i; }
|
|
54
|
+
}
|
|
55
|
+
return levels.length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Returns luminance of given sRGB color.
|
|
59
|
+
// Pulled from: https://github.com/mina86/ansi_colours/blob/master/src/ansi256.rs
|
|
60
|
+
// eslint-disable-next-line no-bitwise
|
|
61
|
+
static #luminance = ([r, g, b]: RGB): number => (((3567664 * r + 11998547 * g + 1211005 * b) + (1 << 23)) >> 24) & 255;
|
|
62
|
+
|
|
63
|
+
// Pulled from: https://github.com/mina86/ansi_colours/blob/master/src/ansi256.rs
|
|
64
|
+
static #distance = ([xr, xg, xb]: RGB, [yr, yg, yb]: RGB, sr = xr + yr): number =>
|
|
65
|
+
(1024 + sr) * ((xr - yr) ** 2) + 2048 * ((xg - yg) ** 2) + (1534 - sr) * ((xb - yb) ** 2);
|
|
66
|
+
|
|
67
|
+
static #snapToAnsi256Bands([r, g, b]: RGB): [idx: number, snapped: RGB] {
|
|
68
|
+
const ri = this.#snapToLevel(RED_256_LEVELS, r);
|
|
69
|
+
const gi = this.#snapToLevel(GREEN_256_LEVELS, g);
|
|
70
|
+
const bi = this.#snapToLevel(BLUE_256_LEVELS, b);
|
|
71
|
+
return [(ri * 36 + 16 + gi * 6 + bi), [STEPS_256[ri], STEPS_256[gi], STEPS_256[bi]]];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Converts [R,G,B] to an ANSI 256 index
|
|
76
|
+
*/
|
|
77
|
+
// Inspired/sourced from: https://github.com/mina86/ansi_colours/blob/master/src/ansi256.rs
|
|
78
|
+
static ansi256FromRgb(rgb: RGB): number {
|
|
79
|
+
const grayIdx = ANSI256_GRAY_MAPPING[this.#luminance(rgb)];
|
|
80
|
+
const grayDist = this.#distance(rgb, ANSI256_TO_RGB[grayIdx]);
|
|
81
|
+
const [snappedIdx, snapped] = this.#snapToAnsi256Bands(rgb);
|
|
82
|
+
return this.#distance(rgb, snapped) < grayDist ? snappedIdx : grayIdx;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Converts [R,G,B] to an ANSI 16 index
|
|
87
|
+
*/
|
|
88
|
+
static ansi16FromRgb(rgb: RGB): number {
|
|
89
|
+
let min = Number.MAX_SAFE_INTEGER;
|
|
90
|
+
let minIdx = -1;
|
|
91
|
+
for (let i = 0; i < 16; i += 1) {
|
|
92
|
+
const dist = this.#distance(rgb, ANSI256_TO_RGB[i]);
|
|
93
|
+
if (dist < min) {
|
|
94
|
+
min = dist;
|
|
95
|
+
minIdx = i;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return ANSI256_TO_16_IDX[minIdx];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* ANSI 256 into RGB
|
|
103
|
+
*/
|
|
104
|
+
static rgbFromAnsi256(val: number): RGB {
|
|
105
|
+
return ANSI256_TO_RGB[val];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Converts [R,G,B] to [H,S,L]
|
|
110
|
+
*/
|
|
111
|
+
static hsl([r, g, b]: RGB): HSL {
|
|
112
|
+
const [rf, gf, bf] = [r / 255, g / 255, b / 255];
|
|
113
|
+
const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
|
|
114
|
+
let h = 0;
|
|
115
|
+
let s = 0;
|
|
116
|
+
const l = (max + min) / 2;
|
|
117
|
+
|
|
118
|
+
if (max === min) {
|
|
119
|
+
h = s = 0; // achromatic
|
|
120
|
+
} else {
|
|
121
|
+
const d = max - min;
|
|
122
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
123
|
+
switch (max) {
|
|
124
|
+
case rf: h = (gf - bf) / d + (gf < bf ? 6 : 0); break;
|
|
125
|
+
case gf: h = (bf - rf) / d + 2; break;
|
|
126
|
+
case bf: h = (rf - gf) / d + 4; break;
|
|
127
|
+
}
|
|
128
|
+
h /= 6;
|
|
129
|
+
}
|
|
130
|
+
return [h, s, l];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Converts input value into [R,G,B] output
|
|
136
|
+
*/
|
|
137
|
+
static toRgb(val: RGBInput): RGB {
|
|
138
|
+
if (typeof val === 'string') {
|
|
139
|
+
if (val.startsWith('#')) {
|
|
140
|
+
const res = val.match(/^#(?<rh>[a-f0-9]{2})(?<gh>[a-f0-9]{2})(?<bh>[a-f0-9]{2})$/i);
|
|
141
|
+
if (res) {
|
|
142
|
+
const { rh, gh, bh } = res.groups!;
|
|
143
|
+
return [parseInt(rh, 16), parseInt(gh, 16), parseInt(bh, 16)];
|
|
144
|
+
}
|
|
145
|
+
} else if (val in NAMED_COLORS) {
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
147
|
+
return this.toRgb(NAMED_COLORS[val as keyof typeof NAMED_COLORS]);
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`Unknown color format: ${val}`);
|
|
150
|
+
} else if (typeof val === 'number') {
|
|
151
|
+
return toRgbArray(val);
|
|
152
|
+
} else {
|
|
153
|
+
return val;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Determine if a color is light or dark
|
|
159
|
+
*/
|
|
160
|
+
static getScheme(color: RGB): TermColorScheme {
|
|
161
|
+
const [r, g, b] = color;
|
|
162
|
+
return (r + g + b) / 3 < 128 ? 'dark' : 'light';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Define a color and all its parts
|
|
167
|
+
*/
|
|
168
|
+
static defineColor(val: RGBInput): DefinedColor {
|
|
169
|
+
if (!this.CACHE.has(val)) {
|
|
170
|
+
const rgb = this.toRgb(val);
|
|
171
|
+
const idx16 = this.ansi16FromRgb(rgb);
|
|
172
|
+
const idx256 = this.ansi256FromRgb(rgb);
|
|
173
|
+
const hsl = this.hsl(rgb);
|
|
174
|
+
const idx16bg = ANSI16_TO_BG.get(idx16)!;
|
|
175
|
+
const scheme = this.getScheme(rgb);
|
|
176
|
+
this.CACHE.set(val, { rgb, idx16, idx16bg, idx256, hsl, scheme });
|
|
177
|
+
}
|
|
178
|
+
return this.CACHE.get(val)!;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Build ANSI compatible color codes by level
|
|
183
|
+
*/
|
|
184
|
+
static getColorCodes(inp: RGBInput, bg: boolean): [number[], number[]][] {
|
|
185
|
+
const spec = this.defineColor(inp);
|
|
186
|
+
const { idx16, idx16bg, idx256, rgb } = spec;
|
|
187
|
+
const [open, close] = bg ? [48, 49] : [38, 39];
|
|
188
|
+
return [
|
|
189
|
+
[[], []],
|
|
190
|
+
[[bg ? idx16bg : idx16], [close]],
|
|
191
|
+
[[open, 5, idx256], [close]],
|
|
192
|
+
[[open, 2, ...rgb], [close]]
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import tty from 'tty';
|
|
2
|
+
|
|
3
|
+
import { ColorDefineUtil, RGBInput } from './color-define';
|
|
4
|
+
import { ANSICodes } from './codes';
|
|
5
|
+
import { TermColorScheme, TermColorLevel, TermState, RGB } from './types';
|
|
6
|
+
|
|
7
|
+
export type TermStyle =
|
|
8
|
+
{ text?: RGBInput, background?: RGBInput, italic?: boolean, underline?: boolean, inverse?: boolean, blink?: boolean };
|
|
9
|
+
|
|
10
|
+
export type TermStyleInput = TermStyle | RGBInput;
|
|
11
|
+
export type Prim = string | number | boolean | Date | RegExp;
|
|
12
|
+
export type TermColorPaletteInput = Record<string, TermStyleInput | [TermStyleInput, TermStyleInput]>;
|
|
13
|
+
export type TermColorFn = (text: Prim) => string;
|
|
14
|
+
export type TermColorPalette<T> = Record<keyof T, TermColorFn>;
|
|
15
|
+
|
|
16
|
+
const COLOR_LEVEL_MAP = { 1: 0, 4: 1, 8: 2, 24: 3 } as const;
|
|
17
|
+
type ColorBits = keyof (typeof COLOR_LEVEL_MAP);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Utils for colorizing output
|
|
21
|
+
*/
|
|
22
|
+
export class ColorOutputUtil {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Detect color level from tty information
|
|
26
|
+
*/
|
|
27
|
+
static async readTermColorLevel(stream: tty.WriteStream): Promise<TermColorLevel> {
|
|
28
|
+
const force = process.env.FORCE_COLOR;
|
|
29
|
+
const disable = process.env.NO_COLOR ?? process.env.NODE_DISABLE_COLORS;
|
|
30
|
+
if (force !== undefined) {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
32
|
+
return Math.max(Math.min(/^\d+$/.test(force) ? parseInt(force, 10) : 1, 3), 0) as TermColorLevel;
|
|
33
|
+
} else if (disable !== undefined && /^(1|true|yes|on)/i.test(disable)) {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
37
|
+
return stream.isTTY ? COLOR_LEVEL_MAP[stream.getColorDepth() as ColorBits] : 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read foreground/background color if env var is set
|
|
42
|
+
*/
|
|
43
|
+
static async readBackgroundScheme(
|
|
44
|
+
query: () => Promise<RGB | undefined> | RGB | undefined,
|
|
45
|
+
env: string | undefined = process.env.COLORFGBG
|
|
46
|
+
): Promise<TermColorScheme | undefined> {
|
|
47
|
+
let color = await query();
|
|
48
|
+
if (!color && env) {
|
|
49
|
+
const [, bg] = env.split(';');
|
|
50
|
+
color = ColorDefineUtil.rgbFromAnsi256(+bg);
|
|
51
|
+
}
|
|
52
|
+
if (color) {
|
|
53
|
+
const hex = `#${color.map(x => x.toString(16).padStart(2, '0')).join('')}` as const;
|
|
54
|
+
return ColorDefineUtil.defineColor(hex).scheme;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get styled levels, 0-3
|
|
60
|
+
*/
|
|
61
|
+
static getStyledLevels(inp: TermStyle | RGBInput): [string, string][] {
|
|
62
|
+
const cfg = (typeof inp !== 'object') ? { text: inp } : inp;
|
|
63
|
+
const levelPairs: [string, string][] = [['', '']];
|
|
64
|
+
const text = cfg.text ? ColorDefineUtil.getColorCodes(cfg.text, false) : undefined;
|
|
65
|
+
const bg = cfg.background ? ColorDefineUtil.getColorCodes(cfg.background, true) : undefined;
|
|
66
|
+
|
|
67
|
+
for (const level of [1, 2, 3]) {
|
|
68
|
+
const prefix: number[] = [];
|
|
69
|
+
const suffix: number[] = [];
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
71
|
+
for (const key of Object.keys(cfg) as (keyof typeof cfg)[]) {
|
|
72
|
+
if (!cfg[key]) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
switch (key) {
|
|
76
|
+
case 'inverse': prefix.push(7); suffix.push(27); break;
|
|
77
|
+
case 'underline': prefix.push(4); suffix.push(24); break;
|
|
78
|
+
case 'italic': prefix.push(3); suffix.push(23); break;
|
|
79
|
+
case 'blink': prefix.push(5); suffix.push(25); break;
|
|
80
|
+
case 'text': prefix.push(...text![level][0]); suffix.push(...text![level][1]); break;
|
|
81
|
+
case 'background': prefix.push(...bg![level][0]); suffix.push(...bg![level][1]); break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
levelPairs[level] = [
|
|
85
|
+
ANSICodes.STYLE(prefix),
|
|
86
|
+
ANSICodes.STYLE(suffix.reverse())
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
return levelPairs;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Make a simple primitive colorer
|
|
94
|
+
*/
|
|
95
|
+
static colorer(term: TermState, style: TermStyleInput | [light: TermStyleInput, dark: TermStyleInput]): TermColorFn {
|
|
96
|
+
const schemes = {
|
|
97
|
+
light: this.getStyledLevels(Array.isArray(style) ? style[1] ?? style[0] : style),
|
|
98
|
+
dark: this.getStyledLevels(Array.isArray(style) ? style[0] : style),
|
|
99
|
+
};
|
|
100
|
+
return (v: Prim): string => {
|
|
101
|
+
const [prefix, suffix] = schemes[term.backgroundScheme][term.colorLevel];
|
|
102
|
+
return (v === undefined || v === null) ? '' : `${prefix}${v}${suffix}`;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates a color palette based on input styles
|
|
108
|
+
*/
|
|
109
|
+
static palette<P extends TermColorPaletteInput>(term: TermState, input: P): TermColorPalette<P> {
|
|
110
|
+
// Common color support
|
|
111
|
+
const out: Partial<TermColorPalette<P>> = {};
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
113
|
+
for (const [k, col] of Object.entries(input) as [keyof P, TermStyleInput][]) {
|
|
114
|
+
out[k] = this.colorer(term, col);
|
|
115
|
+
}
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
117
|
+
return out as TermColorPalette<P>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Convenience method to creates a color template function based on input styles
|
|
122
|
+
*/
|
|
123
|
+
static templateFunction<P extends TermColorPaletteInput>(term: TermState, input: P): (key: keyof P, val: Prim) => string {
|
|
124
|
+
const pal = this.palette(term, input);
|
|
125
|
+
return (key: keyof P, val: Prim) => pal[key](val);
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/iterable.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import timers from 'timers/promises';
|
|
2
|
+
import { DelayedConfig, Indexed } from './types';
|
|
3
|
+
|
|
4
|
+
export type StoppableIterable<T> = { stream: AsyncIterable<T>, stop: () => void };
|
|
5
|
+
|
|
6
|
+
export type MapFn<T, U> = (val: T, i: number) => U | Promise<U>;
|
|
7
|
+
|
|
8
|
+
const isIdx = (x: unknown): x is Indexed => (x !== undefined && x !== null) && typeof x === 'object' && 'idx' in x;
|
|
9
|
+
|
|
10
|
+
export class IterableUtil {
|
|
11
|
+
|
|
12
|
+
static DELAY = ({ initialDelay = 0, cycleDelay = 0 }: DelayedConfig = {}) =>
|
|
13
|
+
<T>(x: T, i: number): Promise<T> => timers.setTimeout(i === 0 ? initialDelay : cycleDelay).then(() => x);
|
|
14
|
+
|
|
15
|
+
static ORDERED = <T extends Indexed>(): (val: T) => boolean => {
|
|
16
|
+
let last = -1;
|
|
17
|
+
return (v: T): boolean => (last = Math.max(v.idx, last)) === v.idx;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
static map<T, U, V, W>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>, fn3: MapFn<V, W>): AsyncIterable<W>;
|
|
21
|
+
static map<T, U, V>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>): AsyncIterable<V>;
|
|
22
|
+
static map<T, U>(source: AsyncIterable<T>, fn: MapFn<T, U>): AsyncIterable<U>;
|
|
23
|
+
static async * map<T>(source: AsyncIterable<T>, ...fns: MapFn<unknown, unknown>[]): AsyncIterable<unknown> {
|
|
24
|
+
let idx = -1;
|
|
25
|
+
for await (const el of source) {
|
|
26
|
+
if (el !== undefined) {
|
|
27
|
+
idx += 1;
|
|
28
|
+
let m = el;
|
|
29
|
+
for (const fn of fns) {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
31
|
+
m = (await fn(m, idx)) as typeof m;
|
|
32
|
+
}
|
|
33
|
+
yield m;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static async * filter<T>(source: AsyncIterable<T>, pred: (val: T, i: number) => boolean | Promise<boolean>): AsyncIterable<T> {
|
|
39
|
+
let idx = -1;
|
|
40
|
+
for await (const el of source) {
|
|
41
|
+
if (await pred(el, idx += 1)) {
|
|
42
|
+
yield el;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static cycle<T>(items: T[]): StoppableIterable<T> {
|
|
48
|
+
let done = false;
|
|
49
|
+
async function* buildStream(): AsyncIterable<T> {
|
|
50
|
+
let i = -1;
|
|
51
|
+
while (!done) {
|
|
52
|
+
yield items[(i += 1) % items.length];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { stream: buildStream(), stop: (): void => { done = true; } };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static async drain<T>(source: AsyncIterable<T>): Promise<T[]> {
|
|
59
|
+
const items: T[] = [];
|
|
60
|
+
for await (const ev of source) {
|
|
61
|
+
items[isIdx(ev) ? ev.idx : items.length] = ev;
|
|
62
|
+
}
|
|
63
|
+
return items;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static simpleQueue(): {
|
|
67
|
+
close: () => Promise<void>;
|
|
68
|
+
add: <T>(item: () => Promise<T>) => Promise<T>;
|
|
69
|
+
running: Promise<void>;
|
|
70
|
+
} {
|
|
71
|
+
let done = false;
|
|
72
|
+
let fire: () => void;
|
|
73
|
+
let next = new Promise<void>(r => fire = r);
|
|
74
|
+
|
|
75
|
+
const queue: Function[] = [];
|
|
76
|
+
async function run(): Promise<void> {
|
|
77
|
+
while (!done) {
|
|
78
|
+
if (!queue.length) {
|
|
79
|
+
await next;
|
|
80
|
+
next = new Promise(r => fire = r);
|
|
81
|
+
}
|
|
82
|
+
await queue.shift()?.();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const running = run();
|
|
86
|
+
return {
|
|
87
|
+
running,
|
|
88
|
+
close: (): Promise<void> => {
|
|
89
|
+
done = true;
|
|
90
|
+
fire();
|
|
91
|
+
return running;
|
|
92
|
+
},
|
|
93
|
+
add: <T>(fn: () => Promise<T>): Promise<T> => {
|
|
94
|
+
const prom = new Promise<T>(r => queue.push(() => fn().then(r)));
|
|
95
|
+
fire();
|
|
96
|
+
return prom;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export const NAMED_COLORS = {
|
|
2
|
+
// Name: RGB
|
|
3
|
+
stdBlack: 0x000000,
|
|
4
|
+
stdRed: 0xcd0000,
|
|
5
|
+
stdGreen: 0x00cd00,
|
|
6
|
+
stdYellow: 0xcdcd00,
|
|
7
|
+
stdBlue: 0x0000ee,
|
|
8
|
+
stdMagenta: 0xcd00cd,
|
|
9
|
+
stdCyan: 0x00cdcd,
|
|
10
|
+
stdWhite: 0xe5e5e5,
|
|
11
|
+
brightBlack: 0x7f7f7f,
|
|
12
|
+
brightRed: 0xff0000,
|
|
13
|
+
brightGreen: 0x00fc00,
|
|
14
|
+
brightYellow: 0xffff00,
|
|
15
|
+
brightBlue: 0x0000fc,
|
|
16
|
+
brightMagenta: 0xff00ff,
|
|
17
|
+
brightCyan: 0x00ffff,
|
|
18
|
+
brightWhite: 0xffffff,
|
|
19
|
+
|
|
20
|
+
aliceBlue: 0xf0f8ff,
|
|
21
|
+
antiqueWhite: 0xfaebd7,
|
|
22
|
+
aqua: 0x00ffff,
|
|
23
|
+
aquamarine: 0x7fffd4,
|
|
24
|
+
azure: 0xf0ffff,
|
|
25
|
+
beige: 0xf5f5dc,
|
|
26
|
+
bisque: 0xffe4c4,
|
|
27
|
+
black: 0x000000,
|
|
28
|
+
blanchedAlmond: 0xffebcd,
|
|
29
|
+
blue: 0x0000ff,
|
|
30
|
+
blueViolet: 0x8a2be2,
|
|
31
|
+
brown: 0xa52a2a,
|
|
32
|
+
burlyWood: 0xdeb887,
|
|
33
|
+
cadetBlue: 0x5f9ea0,
|
|
34
|
+
chartreuse: 0x7fff00,
|
|
35
|
+
chocolate: 0xd2691e,
|
|
36
|
+
coral: 0xff7f50,
|
|
37
|
+
cornflowerBlue: 0x6495ed,
|
|
38
|
+
cornSilk: 0xfff8dc,
|
|
39
|
+
crimson: 0xdc143c,
|
|
40
|
+
cyan: 0x00ffff,
|
|
41
|
+
darkBlue: 0x00008b,
|
|
42
|
+
darkCyan: 0x008b8b,
|
|
43
|
+
darkGoldenrod: 0xb8860b,
|
|
44
|
+
darkGray: 0xa9a9a9,
|
|
45
|
+
darkGreen: 0x006400,
|
|
46
|
+
darkKhaki: 0xbdb76b,
|
|
47
|
+
darkMagenta: 0x8b008b,
|
|
48
|
+
darkOliveGreen: 0x556b2f,
|
|
49
|
+
darkOrange: 0xff8c00,
|
|
50
|
+
darkOrchid: 0x9932cc,
|
|
51
|
+
darkRed: 0x8b0000,
|
|
52
|
+
darkSalmon: 0xe9967a,
|
|
53
|
+
darkSeaGreen: 0x8fbc8f,
|
|
54
|
+
darkSlateBlue: 0x483d8b,
|
|
55
|
+
darkSlateGray: 0x2f4f4f,
|
|
56
|
+
darkTurquoise: 0x00ced1,
|
|
57
|
+
darkViolet: 0x9400d3,
|
|
58
|
+
deepPink: 0xff1493,
|
|
59
|
+
deepSkyBlue: 0x00bfff,
|
|
60
|
+
dimGray: 0x696969,
|
|
61
|
+
dodgerBlue: 0x1e90ff,
|
|
62
|
+
firebrick: 0xb22222,
|
|
63
|
+
floralWhite: 0xfffaf0,
|
|
64
|
+
forestGreen: 0x228b22,
|
|
65
|
+
fuchsia: 0xff00ff,
|
|
66
|
+
gainsboro: 0xdcdcdc,
|
|
67
|
+
ghostWhite: 0xf8f8ff,
|
|
68
|
+
gold: 0xffd700,
|
|
69
|
+
goldenrod: 0xdaa520,
|
|
70
|
+
gray: 0x808080,
|
|
71
|
+
green: 0x008000,
|
|
72
|
+
greenYellow: 0xadff2f,
|
|
73
|
+
honeydew: 0xf0fff0,
|
|
74
|
+
hotPink: 0xff69b4,
|
|
75
|
+
indianRed: 0xcd5c5c,
|
|
76
|
+
indigo: 0x4b0082,
|
|
77
|
+
ivory: 0xfffff0,
|
|
78
|
+
khaki: 0xf0e68c,
|
|
79
|
+
lavender: 0xe6e6fa,
|
|
80
|
+
lavenderBlush: 0xfff0f5,
|
|
81
|
+
lawnGreen: 0x7cfc00,
|
|
82
|
+
lemonChiffon: 0xfffacd,
|
|
83
|
+
lightBlue: 0xadd8e6,
|
|
84
|
+
lightCoral: 0xf08080,
|
|
85
|
+
lightCyan: 0xe0ffff,
|
|
86
|
+
lightGoldenRodYellow: 0xfafad2,
|
|
87
|
+
lightGray: 0xd3d3d3,
|
|
88
|
+
lightGreen: 0x90ee90,
|
|
89
|
+
lightPink: 0xffb6c1,
|
|
90
|
+
lightSalmon: 0xffa07a,
|
|
91
|
+
lightSeaGreen: 0x20b2aa,
|
|
92
|
+
lightSkyBlue: 0x87cefa,
|
|
93
|
+
lightSlateGray: 0x778899,
|
|
94
|
+
lightSteelBlue: 0xb0c4de,
|
|
95
|
+
lightYellow: 0xffffe0,
|
|
96
|
+
lime: 0x00ff00,
|
|
97
|
+
limeGreen: 0x32cd32,
|
|
98
|
+
linen: 0xfaf0e6,
|
|
99
|
+
magenta: 0xff00ff,
|
|
100
|
+
maroon: 0x800000,
|
|
101
|
+
mediumAquamarine: 0x66cdaa,
|
|
102
|
+
mediumBlue: 0x0000cd,
|
|
103
|
+
mediumOrchid: 0xba55d3,
|
|
104
|
+
mediumPurple: 0x9370db,
|
|
105
|
+
mediumSeaGreen: 0x3cb371,
|
|
106
|
+
mediumSlateBlue: 0x7b68ee,
|
|
107
|
+
mediumSpringGreen: 0x00fa9a,
|
|
108
|
+
mediumTurquoise: 0x48d1cc,
|
|
109
|
+
mediumVioletRed: 0xc71585,
|
|
110
|
+
midnightBlue: 0x191970,
|
|
111
|
+
mintCream: 0xf5fffa,
|
|
112
|
+
mistyRose: 0xffe4e1,
|
|
113
|
+
moccasin: 0xffe4b5,
|
|
114
|
+
navajoWhite: 0xffdead,
|
|
115
|
+
navy: 0x000080,
|
|
116
|
+
oldLace: 0xfdf5e6,
|
|
117
|
+
olive: 0x808000,
|
|
118
|
+
oliveDrab: 0x6b8e23,
|
|
119
|
+
orange: 0xffa500,
|
|
120
|
+
orangeRed: 0xff4500,
|
|
121
|
+
orchid: 0xda70d6,
|
|
122
|
+
paleGoldenrod: 0xeee8aa,
|
|
123
|
+
paleGreen: 0x98fb98,
|
|
124
|
+
paleTurquoise: 0xafeeee,
|
|
125
|
+
paleVioletRed: 0xdb7093,
|
|
126
|
+
papayaWhip: 0xffefd5,
|
|
127
|
+
peachPuff: 0xffdab9,
|
|
128
|
+
peru: 0xcd853f,
|
|
129
|
+
pink: 0xffc0cb,
|
|
130
|
+
plum: 0xdda0dd,
|
|
131
|
+
powderBlue: 0xb0e0e6,
|
|
132
|
+
purple: 0x800080,
|
|
133
|
+
red: 0xff0000,
|
|
134
|
+
rosyBrown: 0xbc8f8f,
|
|
135
|
+
royalBlue: 0x4169e1,
|
|
136
|
+
saddleBrown: 0x8b4513,
|
|
137
|
+
salmon: 0xfa8072,
|
|
138
|
+
sandyBrown: 0xf4a460,
|
|
139
|
+
seaGreen: 0x2e8b57,
|
|
140
|
+
seashell: 0xfff5ee,
|
|
141
|
+
sienna: 0xa0522d,
|
|
142
|
+
silver: 0xc0c0c0,
|
|
143
|
+
skyBlue: 0x87ceeb,
|
|
144
|
+
slateBlue: 0x6a5acd,
|
|
145
|
+
slateGray: 0x708090,
|
|
146
|
+
snow: 0xfffafa,
|
|
147
|
+
springGreen: 0x00ff7f,
|
|
148
|
+
steelBlue: 0x4682b4,
|
|
149
|
+
tan: 0xd2b48c,
|
|
150
|
+
teal: 0x008080,
|
|
151
|
+
thistle: 0xd8bfd8,
|
|
152
|
+
tomato: 0xff6347,
|
|
153
|
+
turquoise: 0x40e0d0,
|
|
154
|
+
violet: 0xee82ee,
|
|
155
|
+
wheat: 0xf5deb3,
|
|
156
|
+
white: 0xffffff,
|
|
157
|
+
whitesmoke: 0xf5f5f5,
|
|
158
|
+
yellow: 0xffff00,
|
|
159
|
+
yellowGreen: 0x9acd32,
|
|
160
|
+
} as const;
|
package/src/operation.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { IterableUtil } from './iterable';
|
|
2
|
+
import { TerminalWriter } from './writer';
|
|
3
|
+
import { Indexed, TerminalProgressRender, TerminalStreamingConfig, TerminalWaitingConfig, TermState } from './types';
|
|
4
|
+
import { ColorOutputUtil, TermStyleInput } from './color-output';
|
|
5
|
+
|
|
6
|
+
const STD_WAIT_STATES = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'.split('');
|
|
7
|
+
|
|
8
|
+
export class TerminalOperation {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Allows for writing at top, bottom, or current position while new text is added
|
|
12
|
+
*/
|
|
13
|
+
static async streamToPosition(term: TermState, source: AsyncIterable<string>, config: TerminalStreamingConfig = {}): Promise<void> {
|
|
14
|
+
const curPos = config.at ?? { ...await term.getCursorPosition() };
|
|
15
|
+
const pos = config.position ?? 'inline';
|
|
16
|
+
|
|
17
|
+
const writePos = pos === 'inline' ?
|
|
18
|
+
{ ...curPos, x: 0 } :
|
|
19
|
+
{ x: 0, y: pos === 'top' ? 0 : -1 };
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const batch = TerminalWriter.for(term).hideCursor();
|
|
23
|
+
if (pos !== 'inline') {
|
|
24
|
+
batch.storePosition().scrollRange(pos === 'top' ? { start: 2 } : { end: -2 }).restorePosition();
|
|
25
|
+
if (pos === 'top' && curPos.y === 0) {
|
|
26
|
+
batch.changePosition({ y: 1 }).write('');
|
|
27
|
+
} else if (pos === 'bottom' && curPos.y === term.height - 1) {
|
|
28
|
+
batch.changePosition({ y: -1 }).write('\n');
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
batch.write('\n'); // Move past line
|
|
32
|
+
}
|
|
33
|
+
await batch.commit();
|
|
34
|
+
|
|
35
|
+
for await (const text of source) {
|
|
36
|
+
await TerminalWriter.for(term).setPosition(writePos).write(text).clearLine(1).commit(true);
|
|
37
|
+
}
|
|
38
|
+
if (config.clearOnFinish ?? true) {
|
|
39
|
+
await TerminalWriter.for(term).setPosition(writePos).clearLine().commit(true);
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
const finalCursor = await term.getCursorPosition();
|
|
43
|
+
await TerminalWriter.for(term).scrollRangeClear().setPosition(finalCursor).showCursor().commit();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Consumes a stream, of events, tied to specific list indices, and updates in place
|
|
49
|
+
*/
|
|
50
|
+
static async streamList(term: TermState, source: AsyncIterable<Indexed & { text: string }>): Promise<void> {
|
|
51
|
+
let max = 0;
|
|
52
|
+
try {
|
|
53
|
+
await TerminalWriter.for(term).hideCursor().commit();
|
|
54
|
+
for await (const { idx, text } of source) {
|
|
55
|
+
max = Math.max(idx, max);
|
|
56
|
+
await TerminalWriter.for(term).write('\n'.repeat(idx)).rewriteLine(text).clearLine(1).changePosition({ y: -idx }).commit();
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
await TerminalWriter.for(term).changePosition({ y: max + 1 }).writeLine('\n').showCursor().commit();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Waiting indicator, streamed to a specific position, can be canceled
|
|
65
|
+
*/
|
|
66
|
+
static streamWaiting(term: TermState, message: string, config: TerminalWaitingConfig = {}): () => Promise<void> {
|
|
67
|
+
const { stop, stream } = IterableUtil.cycle(STD_WAIT_STATES);
|
|
68
|
+
const indicator = IterableUtil.map(
|
|
69
|
+
stream,
|
|
70
|
+
IterableUtil.DELAY(config),
|
|
71
|
+
(ch, i) => config.end ? `${message} ${ch}` : `${ch} ${message}`
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const final = this.streamToPosition(term, indicator, config);
|
|
75
|
+
return async () => { stop(); return final; };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build progress par formatter for terminal progress events
|
|
80
|
+
*/
|
|
81
|
+
static buildProgressBar(term: TermState, style: TermStyleInput): TerminalProgressRender {
|
|
82
|
+
const color = ColorOutputUtil.colorer(term, style);
|
|
83
|
+
return ({ total, idx, text }): string => {
|
|
84
|
+
text ||= total ? '%idx/%total' : '%idx';
|
|
85
|
+
|
|
86
|
+
const totalStr = `${total ?? ''}`;
|
|
87
|
+
const idxStr = `${idx}`.padStart(totalStr.length);
|
|
88
|
+
const pct = total === undefined ? 0 : (idx / total);
|
|
89
|
+
const line = text
|
|
90
|
+
.replace(/%idx/, idxStr)
|
|
91
|
+
.replace(/%total/, totalStr)
|
|
92
|
+
.replace(/%pct/, `${Math.trunc(pct * 100)}`);
|
|
93
|
+
const full = ` ${line}`.padEnd(term.width);
|
|
94
|
+
const mid = Math.trunc(pct * term.width);
|
|
95
|
+
const [l, r] = [full.substring(0, mid), full.substring(mid)];
|
|
96
|
+
return `${color(l)}${r}`;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Stream lines with a waiting indicator
|
|
102
|
+
*/
|
|
103
|
+
static async streamLinesWithWaiting(term: TermState, lines: AsyncIterable<string | undefined>, cfg: TerminalWaitingConfig = {}): Promise<void> {
|
|
104
|
+
let writer: (() => Promise<unknown>) | undefined;
|
|
105
|
+
let line: string | undefined;
|
|
106
|
+
|
|
107
|
+
let pos = await term.getCursorPosition();
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
const commitLine = async (): Promise<void> => {
|
|
111
|
+
await writer?.();
|
|
112
|
+
if (line) {
|
|
113
|
+
const msg = cfg.committedPrefix ? `${cfg.committedPrefix} ${line}` : line;
|
|
114
|
+
if (cfg.position === 'inline') {
|
|
115
|
+
await TerminalWriter.for(term).setPosition(pos).write(msg).commit(true);
|
|
116
|
+
} else {
|
|
117
|
+
await TerminalWriter.for(term).writeLine(msg).commit();
|
|
118
|
+
}
|
|
119
|
+
line = undefined;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for await (let msg of lines) {
|
|
124
|
+
await commitLine();
|
|
125
|
+
if (msg !== undefined) {
|
|
126
|
+
msg = msg.replace(/\n$/, '');
|
|
127
|
+
pos = await term.getCursorPosition();
|
|
128
|
+
writer = this.streamWaiting(term, msg, { ...cfg, at: pos, clearOnFinish: false });
|
|
129
|
+
line = msg;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
await commitLine();
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import tty from 'tty';
|
|
2
|
+
|
|
3
|
+
import { ANSICodes } from './codes';
|
|
4
|
+
import { IterableUtil } from './iterable';
|
|
5
|
+
import { RGB, TermCoord, TermQuery } from './types';
|
|
6
|
+
|
|
7
|
+
const to256 = (x: string): number => Math.trunc(parseInt(x, 16) / (16 ** (x.length - 2)));
|
|
8
|
+
const COLOR_RESPONSE = /(?<r>][0-9a-f]+)[/](?<g>[0-9a-f]+)[/](?<b>[0-9a-f]+)[/]?(?<a>[0-9a-f]+)?/i;
|
|
9
|
+
|
|
10
|
+
const ANSIQueries = {
|
|
11
|
+
/** Parse xterm color response */
|
|
12
|
+
color: (field: 'background' | 'foreground'): TermQuery<RGB | undefined> => ({
|
|
13
|
+
query: (): string => ANSICodes.OSC_QUERY(`${field}Color`),
|
|
14
|
+
response: (response: Buffer): RGB | undefined => {
|
|
15
|
+
const groups = response.toString('utf8').match(COLOR_RESPONSE)?.groups ?? {};
|
|
16
|
+
return 'r' in groups ? [to256(groups.r), to256(groups.g), to256(groups.b)] : undefined;
|
|
17
|
+
}
|
|
18
|
+
}),
|
|
19
|
+
/** Parse cursor query response into {x,y} */
|
|
20
|
+
cursorPosition: {
|
|
21
|
+
query: (): string => ANSICodes.DEVICE_STATUS_REPORT('cursorPosition'),
|
|
22
|
+
response: (response: Buffer): TermCoord => {
|
|
23
|
+
const groups = response.toString('utf8').match(/(?<r>\d*);(?<c>\d*)/)?.groups ?? {};
|
|
24
|
+
return 'c' in groups ? { x: +(groups.c) - 1, y: +(groups.r) - 1 } : { x: 0, y: 0 };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Terminal query support with centralized queuing for multiple writes to the same stream
|
|
31
|
+
*/
|
|
32
|
+
export class TerminalQuerier {
|
|
33
|
+
|
|
34
|
+
static #cache = new Map<tty.ReadStream, TerminalQuerier>();
|
|
35
|
+
|
|
36
|
+
static for(input: tty.ReadStream, output: tty.WriteStream): TerminalQuerier {
|
|
37
|
+
if (!this.#cache.has(input)) {
|
|
38
|
+
this.#cache.set(input, new TerminalQuerier(input, output));
|
|
39
|
+
}
|
|
40
|
+
return this.#cache.get(input)!;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#queue = IterableUtil.simpleQueue();
|
|
44
|
+
#output: tty.WriteStream;
|
|
45
|
+
#input: tty.ReadStream;
|
|
46
|
+
|
|
47
|
+
constructor(input: tty.ReadStream, output: tty.WriteStream) {
|
|
48
|
+
this.#input = input;
|
|
49
|
+
this.#output = output;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async #readInput(query: string): Promise<Buffer> {
|
|
53
|
+
const isRaw = this.#input.isRaw;
|
|
54
|
+
const isPaused = this.#input.isPaused();
|
|
55
|
+
const data = this.#input.listeners('data');
|
|
56
|
+
try {
|
|
57
|
+
this.#input.removeAllListeners('data');
|
|
58
|
+
|
|
59
|
+
if (isPaused) {
|
|
60
|
+
this.#input.resume();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.#input.setRawMode(true);
|
|
64
|
+
// Send data, but do not wait on it
|
|
65
|
+
this.#output.write(query);
|
|
66
|
+
await new Promise(res => this.#input.once('readable', res));
|
|
67
|
+
const val: Buffer | string = this.#input.read();
|
|
68
|
+
return typeof val === 'string' ? Buffer.from(val, 'utf8') : val;
|
|
69
|
+
} finally {
|
|
70
|
+
if (isPaused) {
|
|
71
|
+
this.#input.pause();
|
|
72
|
+
}
|
|
73
|
+
this.#input.setRawMode(isRaw);
|
|
74
|
+
for (const fn of data) {
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
this.#input.on('data', fn);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
query<T>(q: TermQuery<T>): Promise<T> {
|
|
82
|
+
return this.#queue.add(() => this.#readInput(q.query()).then(q.response));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cursorPosition(): Promise<TermCoord> {
|
|
86
|
+
return this.query(ANSIQueries.cursorPosition);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
backgroundColor(): Promise<RGB | undefined> {
|
|
90
|
+
return this.query(ANSIQueries.color('background'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
close(): Promise<void> {
|
|
94
|
+
return this.#queue.close();
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/terminal.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import tty from 'tty';
|
|
2
|
+
|
|
3
|
+
import { IterableUtil, MapFn } from './iterable';
|
|
4
|
+
import {
|
|
5
|
+
TermColorLevel, TermColorScheme, TerminalProgressEvent, TerminalTableConfig,
|
|
6
|
+
TerminalTableEvent, TerminalWaitingConfig, TermLinePosition, TermState, TermCoord
|
|
7
|
+
} from './types';
|
|
8
|
+
import { TerminalOperation } from './operation';
|
|
9
|
+
import { TerminalQuerier } from './query';
|
|
10
|
+
import { TerminalWriter } from './writer';
|
|
11
|
+
import { ColorOutputUtil, Prim, TermColorFn, TermColorPalette, TermColorPaletteInput, TermStyleInput } from './color-output';
|
|
12
|
+
|
|
13
|
+
type TerminalStreamPositionConfig = {
|
|
14
|
+
position?: TermLinePosition;
|
|
15
|
+
staticMessage?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type TerminalProgressConfig = TerminalStreamPositionConfig & {
|
|
19
|
+
style?: TermStyleInput;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* An enhanced tty write stream
|
|
24
|
+
*/
|
|
25
|
+
export class Terminal implements TermState {
|
|
26
|
+
|
|
27
|
+
static async for(config: Partial<TermState>): Promise<Terminal> {
|
|
28
|
+
const term = new Terminal(config);
|
|
29
|
+
await term.init();
|
|
30
|
+
return term;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#init: Promise<void>;
|
|
34
|
+
#output: tty.WriteStream;
|
|
35
|
+
#input: tty.ReadStream;
|
|
36
|
+
#interactive: boolean;
|
|
37
|
+
#width?: number;
|
|
38
|
+
#backgroundScheme?: TermColorScheme;
|
|
39
|
+
#colorLevel?: TermColorLevel;
|
|
40
|
+
#query: TerminalQuerier;
|
|
41
|
+
|
|
42
|
+
constructor(config: Partial<TermState>) {
|
|
43
|
+
this.#output = config.output ?? process.stdout;
|
|
44
|
+
this.#input = config.input ?? process.stdin;
|
|
45
|
+
this.#interactive = config.interactive ?? (this.#output.isTTY && !/^(true|yes|on|1)$/i.test(process.env.TRV_QUIET ?? ''));
|
|
46
|
+
this.#width = config.width;
|
|
47
|
+
this.#colorLevel = config.colorLevel;
|
|
48
|
+
this.#backgroundScheme = config.backgroundScheme;
|
|
49
|
+
this.#query = TerminalQuerier.for(this.#input, this.#output);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get output(): tty.WriteStream {
|
|
53
|
+
return this.#output;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get input(): tty.ReadStream {
|
|
57
|
+
return this.#input;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get interactive(): boolean {
|
|
61
|
+
return this.#interactive;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get width(): number {
|
|
65
|
+
return this.#width ?? (this.#output.isTTY ? this.#output.columns : 120);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get height(): number {
|
|
69
|
+
return (this.#output.isTTY ? this.#output.rows : 120);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get colorLevel(): TermColorLevel {
|
|
73
|
+
return this.#colorLevel ?? 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get backgroundScheme(): TermColorScheme {
|
|
77
|
+
return this.#backgroundScheme ?? 'dark';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
writer(): TerminalWriter {
|
|
81
|
+
return TerminalWriter.for(this);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async writeLines(...text: string[]): Promise<void> {
|
|
85
|
+
return this.writer().writeLines(text, this.interactive).commit();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async init(): Promise<void> {
|
|
89
|
+
if (!this.#init) {
|
|
90
|
+
this.#init = (async (): Promise<void> => {
|
|
91
|
+
this.#colorLevel ??= await ColorOutputUtil.readTermColorLevel(this.#output);
|
|
92
|
+
this.#backgroundScheme ??= await ColorOutputUtil.readBackgroundScheme(
|
|
93
|
+
() => this.interactive ? this.#query.backgroundColor() : undefined
|
|
94
|
+
);
|
|
95
|
+
})();
|
|
96
|
+
process.on('exit', () => this.reset());
|
|
97
|
+
}
|
|
98
|
+
return this.#init;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async reset(): Promise<void> {
|
|
102
|
+
await this.#query.close();
|
|
103
|
+
if (this.interactive) {
|
|
104
|
+
await this.writer().reset().commit();
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getCursorPosition(): Promise<TermCoord> {
|
|
110
|
+
return this.#query.cursorPosition();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Waiting message with a callback to end
|
|
115
|
+
*/
|
|
116
|
+
async withWaiting<T>(message: string, work: Promise<T> | (() => Promise<T>), config: TerminalWaitingConfig = {}): Promise<T> {
|
|
117
|
+
const res = (typeof work === 'function' ? work() : work);
|
|
118
|
+
if (!this.interactive) {
|
|
119
|
+
await this.writeLines(`${message}...`);
|
|
120
|
+
return res;
|
|
121
|
+
}
|
|
122
|
+
return res.finally(TerminalOperation.streamWaiting(this, message, config));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Stream line output, showing a waiting indicator for each line until the next one occurs
|
|
127
|
+
*
|
|
128
|
+
* @param lines
|
|
129
|
+
* @param config
|
|
130
|
+
* @returns
|
|
131
|
+
*/
|
|
132
|
+
async streamLinesWithWaiting(lines: AsyncIterable<string | undefined>, config: TerminalWaitingConfig): Promise<void> {
|
|
133
|
+
if (!this.interactive) {
|
|
134
|
+
for await (const line of lines) {
|
|
135
|
+
if (line !== undefined) {
|
|
136
|
+
const out = config.committedPrefix ? `${config.committedPrefix} ${line}` : line;
|
|
137
|
+
await this.writeLines(out);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
return TerminalOperation.streamLinesWithWaiting(this, lines, { position: 'bottom', ...config });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Consumes a stream, of events, tied to specific list indices, and updates in place
|
|
147
|
+
*/
|
|
148
|
+
async streamList<T>(source: AsyncIterable<T>, resolve: MapFn<T, TerminalTableEvent>, config: TerminalTableConfig = {}): Promise<void> {
|
|
149
|
+
const resolved = IterableUtil.map(source, resolve);
|
|
150
|
+
|
|
151
|
+
await this.writeLines(...(config.header ?? []));
|
|
152
|
+
|
|
153
|
+
if (!this.interactive) {
|
|
154
|
+
const isDone = IterableUtil.filter(resolved, ev => !!ev.done);
|
|
155
|
+
if (config.forceNonInteractiveOrder) {
|
|
156
|
+
await this.writeLines(...(await IterableUtil.drain(isDone)).map(x => x.text), '');
|
|
157
|
+
} else {
|
|
158
|
+
await IterableUtil.drain(IterableUtil.map(isDone, ev => this.writeLines(ev.text)));
|
|
159
|
+
await this.writeLines('');
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await TerminalOperation.streamList(this, resolved);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Streams an iterable to a specific location, with support for non-interactive ttys
|
|
169
|
+
*/
|
|
170
|
+
async streamToPosition<T>(source: AsyncIterable<T>, resolve: MapFn<T, string>, config?: TerminalStreamPositionConfig): Promise<void> {
|
|
171
|
+
if (!this.interactive) {
|
|
172
|
+
if (config?.staticMessage) {
|
|
173
|
+
await this.writeLines(config.staticMessage);
|
|
174
|
+
}
|
|
175
|
+
await IterableUtil.drain(source);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
return TerminalOperation.streamToPosition(this, IterableUtil.map(source, resolve), config);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Track progress of an asynchronous iterator, allowing the showing of a progress bar if the stream produces idx and total
|
|
183
|
+
*/
|
|
184
|
+
async trackProgress<T, V extends TerminalProgressEvent>(
|
|
185
|
+
source: AsyncIterable<T>, resolve: MapFn<T, V>, config?: TerminalProgressConfig
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const render = TerminalOperation.buildProgressBar(this, config?.style ?? { background: 'limeGreen', text: 'black' });
|
|
188
|
+
return this.streamToPosition(source, async (v, i) => render(await resolve(v, i)), config);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Creates a colorer function */
|
|
192
|
+
colorer(style: TermStyleInput | [light: TermStyleInput, dark: TermStyleInput]): TermColorFn {
|
|
193
|
+
return ColorOutputUtil.colorer(this, style);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Creates a color palette based on input styles */
|
|
197
|
+
palette<P extends TermColorPaletteInput>(input: P): TermColorPalette<P> {
|
|
198
|
+
return ColorOutputUtil.palette(this, input);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Convenience method to creates a color template function based on input styles */
|
|
202
|
+
templateFunction<P extends TermColorPaletteInput>(input: P): (key: keyof P, val: Prim) => string {
|
|
203
|
+
return ColorOutputUtil.templateFunction(this, input);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const GlobalTerminal = new Terminal({ output: process.stdout });
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import tty from 'tty';
|
|
2
|
+
|
|
3
|
+
export type Indexed = { idx: number };
|
|
4
|
+
export type DelayedConfig = { initialDelay?: number, cycleDelay?: number };
|
|
5
|
+
|
|
6
|
+
type I = number;
|
|
7
|
+
export type RGB = [r: I, g: I, b: I] | (readonly [r: I, g: I, b: I]);
|
|
8
|
+
export type TermCoord = { x: number, y: number };
|
|
9
|
+
export type TermLinePosition = 'top' | 'bottom' | 'inline';
|
|
10
|
+
export type TermColorField = 'foregroundColor' | 'backgroundColor';
|
|
11
|
+
|
|
12
|
+
export type TerminalTableEvent = { idx: number, text: string, done?: boolean };
|
|
13
|
+
export type TerminalTableConfig = { header?: string[], forceNonInteractiveOrder?: boolean };
|
|
14
|
+
export type TerminalProgressEvent = { idx: number, total?: number, text?: string };
|
|
15
|
+
export type TerminalProgressRender = (ev: TerminalProgressEvent) => string;
|
|
16
|
+
export type TerminalStreamingConfig = { position?: TermLinePosition, clearOnFinish?: boolean, at?: TermCoord };
|
|
17
|
+
export type TerminalWaitingConfig = { end?: boolean, committedPrefix?: string } & TerminalStreamingConfig & DelayedConfig;
|
|
18
|
+
|
|
19
|
+
export type TermColorLevel = 0 | 1 | 2 | 3;
|
|
20
|
+
export type TermColorScheme = 'dark' | 'light';
|
|
21
|
+
|
|
22
|
+
export type TermState = {
|
|
23
|
+
interactive: boolean;
|
|
24
|
+
height: number;
|
|
25
|
+
width: number;
|
|
26
|
+
input: tty.ReadStream;
|
|
27
|
+
output: tty.WriteStream;
|
|
28
|
+
colorLevel: TermColorLevel;
|
|
29
|
+
backgroundScheme: TermColorScheme;
|
|
30
|
+
getCursorPosition(): Promise<TermCoord>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type TermQuery<T> = { query: () => string, response: (inp: Buffer) => T };
|
package/src/writer.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ANSICodes } from './codes';
|
|
2
|
+
import { TermCoord, TermState } from './types';
|
|
3
|
+
|
|
4
|
+
const boundIndex = (v: number, size: number): number => {
|
|
5
|
+
if (v < 0) {
|
|
6
|
+
v = size + v;
|
|
7
|
+
}
|
|
8
|
+
return Math.max(Math.min(v, size - 1), 0);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Buffered/batched writer. Meant to be similar to readline.Readline, but with more general writing support and extensibility
|
|
13
|
+
*/
|
|
14
|
+
export class TerminalWriter {
|
|
15
|
+
|
|
16
|
+
static for(term: TermState): TerminalWriter {
|
|
17
|
+
return new TerminalWriter(term);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#queue: (string | number)[] = [];
|
|
21
|
+
#term: TermState;
|
|
22
|
+
#restoreOnCommit = false;
|
|
23
|
+
|
|
24
|
+
constructor(term: TermState) {
|
|
25
|
+
this.#term = term;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Restore on commit */
|
|
29
|
+
restoreOnCommit(): this {
|
|
30
|
+
this.#restoreOnCommit = true;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
commit(restorePosition: boolean = this.#restoreOnCommit): Promise<void> {
|
|
35
|
+
const q = this.#queue.filter(x => x !== undefined);
|
|
36
|
+
this.#queue = [];
|
|
37
|
+
if (q.length && restorePosition) {
|
|
38
|
+
q.unshift(ANSICodes.POSITION_SAVE());
|
|
39
|
+
q.push(ANSICodes.POSITION_RESTORE());
|
|
40
|
+
}
|
|
41
|
+
return q.length ? new Promise(r => this.#term.output.write(q.join(''), () => r())) : Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
write(...text: (string | number)[]): this {
|
|
45
|
+
this.#queue.push(...text);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Stores current cursor position, if called multiple times before restore, last one ones */
|
|
50
|
+
storePosition(): this {
|
|
51
|
+
return this.write(ANSICodes.POSITION_SAVE());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Restores cursor position, will not behave correctly if nested */
|
|
55
|
+
restorePosition(): this {
|
|
56
|
+
return this.write(ANSICodes.POSITION_RESTORE());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Rewrite a single line in the stream */
|
|
60
|
+
rewriteLine(text: string): this {
|
|
61
|
+
return this.setPosition({ x: 0 }).write(text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Clear line, -1 (left), 0 (both), 1 (right), from current cursor */
|
|
65
|
+
clearLine(dir: -1 | 0 | 1 = 0): this {
|
|
66
|
+
return this.write(ANSICodes.ERASE_LINE(dir === 0 ? 2 : (dir === 1 ? 0 : 1)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Set position */
|
|
70
|
+
setPosition({ x = 0, y }: Partial<TermCoord>): this {
|
|
71
|
+
if (y !== undefined) {
|
|
72
|
+
y = boundIndex(y, this.#term.output.rows);
|
|
73
|
+
}
|
|
74
|
+
x = boundIndex(x, this.#term.output.columns);
|
|
75
|
+
if (y !== undefined) {
|
|
76
|
+
return this.write(ANSICodes.POSITION_SET(y + 1, x + 1));
|
|
77
|
+
} else {
|
|
78
|
+
return this.write(ANSICodes.COLUMN_SET(x + 1));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Relative movement */
|
|
83
|
+
changePosition({ x, y }: Partial<TermCoord>): this {
|
|
84
|
+
if (x) {
|
|
85
|
+
this.write(ANSICodes.CURSOR_DX(x));
|
|
86
|
+
}
|
|
87
|
+
if (y) {
|
|
88
|
+
this.write(ANSICodes.CURSOR_DY(y));
|
|
89
|
+
}
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Write single line */
|
|
94
|
+
writeLine(line: string = ''): this {
|
|
95
|
+
return this.write(`${line}\n`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Write multiple lines */
|
|
99
|
+
writeLines(lines: (string | undefined)[], clear = false): this {
|
|
100
|
+
lines = lines.filter(x => x !== undefined);
|
|
101
|
+
let text = lines.join('\n');
|
|
102
|
+
if (text.length > 0) {
|
|
103
|
+
if (clear) {
|
|
104
|
+
text = text.replaceAll('\n', `${ANSICodes.ERASE_LINE(0)}\n`);
|
|
105
|
+
}
|
|
106
|
+
text = `${text}\n`;
|
|
107
|
+
}
|
|
108
|
+
return this.write(text);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Show cursor */
|
|
112
|
+
showCursor(): this {
|
|
113
|
+
return this.write(ANSICodes.SHOW_CURSOR());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Hide cursor */
|
|
117
|
+
hideCursor(): this {
|
|
118
|
+
return this.write(ANSICodes.HIDE_CURSOR());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Set scrolling range */
|
|
122
|
+
scrollRange({ start, end }: { start?: number, end?: number }): this {
|
|
123
|
+
start = boundIndex(start ?? 0, this.#term.output.rows);
|
|
124
|
+
end = boundIndex(end ?? -1, this.#term.output.rows);
|
|
125
|
+
return this.write(ANSICodes.SCROLL_RANGE_SET(start + 1, end + 1));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Clear scrolling range */
|
|
129
|
+
scrollRangeClear(): this {
|
|
130
|
+
return this.write(ANSICodes.SCROLL_RANGE_CLEAR());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Scrolling window y <=0 - up, else down */
|
|
134
|
+
scrollY(y: number): this {
|
|
135
|
+
return this.write(ANSICodes.SCROLL_WINDOW(y));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Reset */
|
|
139
|
+
reset(): this {
|
|
140
|
+
return this
|
|
141
|
+
.write(ANSICodes.POSITION_SAVE())
|
|
142
|
+
.write(ANSICodes.SHOW_CURSOR())
|
|
143
|
+
.write(ANSICodes.SCROLL_RANGE_CLEAR())
|
|
144
|
+
.write(ANSICodes.POSITION_RESTORE());
|
|
145
|
+
}
|
|
146
|
+
}
|