@travetto/terminal 3.4.0 → 4.0.0-rc.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 +17 -20
- package/__index__.ts +4 -9
- package/package.json +5 -1
- package/src/style.ts +122 -0
- package/src/terminal.ts +116 -169
- package/src/trv.d.ts +32 -0
- package/src/util.ts +43 -0
- package/src/writer.ts +65 -71
- package/src/codes.ts +0 -41
- package/src/color-define.ts +0 -195
- package/src/color-output.ts +0 -128
- package/src/iterable.ts +0 -100
- package/src/named-colors.ts +0 -160
- package/src/operation.ts +0 -158
- package/src/query.ts +0 -94
- package/src/types.ts +0 -33
package/README.md
CHANGED
|
@@ -23,31 +23,28 @@ Oddly enough, colorizing output in a terminal is a fairly complex process. The
|
|
|
23
23
|
* 1 - Basic color support, 16 colors
|
|
24
24
|
* 2 - Enhanced color support, 225 colors, providing a fair representation of most colors
|
|
25
25
|
* 3 - True color, 24bit color with R, G, B each getting 8-bits. Can represent any color needed
|
|
26
|
-
This module provides the ability to define color palettes using RGB
|
|
26
|
+
This module provides the ability to define color palettes using RGB colors, and additionally provides support for palettes based on a dark or light background for a given terminal. Support for this is widespread, but when it fails, it will gracefully assume a dark background.
|
|
27
27
|
|
|
28
|
-
These palettes then are usable at runtime, with the module
|
|
28
|
+
These palettes then are usable at runtime, with the module determining light or dark palettes, as well as falling back to the closest color value based on what the existing terminal supports. This means a color like 'olivegreen', will get the proper output in 24bit color support, a close approximation in enhanced color support, fall back to green in basic color support, and will be color less at level 0.
|
|
29
29
|
|
|
30
30
|
**Code: CLI Color Palette**
|
|
31
31
|
```typescript
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
subsubtitle: 'darkGray'
|
|
32
|
+
import { StyleUtil } from '@travetto/terminal';
|
|
33
|
+
|
|
34
|
+
export const cliTpl = StyleUtil.getTemplate({
|
|
35
|
+
input: '#6b8e23', // Olive drab
|
|
36
|
+
output: '#ffc0cb', // Pink
|
|
37
|
+
path: '#008080', // Teal
|
|
38
|
+
success: '#00ff00', // Green
|
|
39
|
+
failure: '#ff0000', // Red
|
|
40
|
+
param: ['#ffff00', '#daa520'], // Yellow / Goldenrod
|
|
41
|
+
type: '#00ffff', // Teal
|
|
42
|
+
description: ['#e5e5e5', '#808080'], // White / Gray
|
|
43
|
+
title: ['#ffffff', '#000000'], // Bright white / black
|
|
44
|
+
identifier: '#1e90ff', // Dodger blue
|
|
45
|
+
subtitle: ['#d3d3d3', '#a9a9a9'], // Light gray / Dark Gray
|
|
46
|
+
subsubtitle: '#a9a9a9' // Dark gray
|
|
48
47
|
});
|
|
49
|
-
|
|
50
|
-
export const cliTpl = Util.makeTemplate(tplFn);
|
|
51
48
|
```
|
|
52
49
|
|
|
53
50
|
When the color palette is combined with [Base](https://github.com/travetto/travetto/tree/main/module/base#readme "Environment config and common utilities for travetto applications.")'s Util.makeTemplate, you produce a string template function that will automatically colorize:
|
package/__index__.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export * from './src/
|
|
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';
|
|
1
|
+
/// <reference path="./src/trv.d.ts" />
|
|
2
|
+
export * from './src/style';
|
|
8
3
|
export * from './src/terminal';
|
|
9
|
-
export * from './src/
|
|
10
|
-
export * from './src/
|
|
4
|
+
export * from './src/writer';
|
|
5
|
+
export * from './src/util';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/terminal",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-rc.0",
|
|
4
4
|
"description": "General terminal support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terminal",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"url": "https://github.com/travetto/travetto.git",
|
|
24
24
|
"directory": "module/terminal"
|
|
25
25
|
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@travetto/base": "^4.0.0-rc.0",
|
|
28
|
+
"chalk": "^4.1.2"
|
|
29
|
+
},
|
|
26
30
|
"travetto": {
|
|
27
31
|
"displayName": "Terminal"
|
|
28
32
|
},
|
package/src/style.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import { Env, TypedObject, Primitive } from '@travetto/base';
|
|
4
|
+
|
|
5
|
+
type TemplatePrim = Primitive | RegExp;
|
|
6
|
+
type Color = `#${string}`;
|
|
7
|
+
export type TermStyleInput = Color | { text: Color, background?: Color, inverse?: boolean, bold?: boolean, italic?: boolean, underline?: boolean };
|
|
8
|
+
type TermStylePairInput = TermStyleInput | [dark: TermStyleInput, light: TermStyleInput];
|
|
9
|
+
export type TermStyleFn = (input: TemplatePrim) => string;
|
|
10
|
+
type TermStyledTemplate<T extends string> = (values: TemplateStringsArray, ...keys: (Partial<Record<T, TemplatePrim>> | string)[]) => string;
|
|
11
|
+
export type ColorLevel = 0 | 1 | 2 | 3;
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line no-control-regex
|
|
14
|
+
const ANSI_CODE_REGEX = /(\x1b|\x1B)[\[\]][?]?[0-9;]+[A-Za-z]/g;
|
|
15
|
+
|
|
16
|
+
export class StyleUtil {
|
|
17
|
+
|
|
18
|
+
static #darkAnsi256 = new Set([
|
|
19
|
+
0, 1, 2, 3, 4, 5, 6, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26, 28, 29, 30, 31, 32, 34, 35, 36, 37, 38, 40, 41, 42, 43, 44, 52, 53, 54,
|
|
20
|
+
55, 56, 58, 59, 60, 64, 65, 66, 70, 76, 88, 89, 90, 91, 92, 94, 95, 96, 100, 101, 106, 112, 124, 125, 126, 127, 128, 130, 136, 142,
|
|
21
|
+
148, 160, 161, 162, 163, 164, 166, 172, 178, 184, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
static #scheme: { key: string, dark: boolean } = { key: '', dark: true };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create text render function from style input using current color levels
|
|
28
|
+
*/
|
|
29
|
+
static getStyle(input: TermStyleInput): TermStyleFn {
|
|
30
|
+
if (typeof input === 'string') {
|
|
31
|
+
return chalk.hex(input);
|
|
32
|
+
} else {
|
|
33
|
+
let style: chalk.Chalk = chalk;
|
|
34
|
+
for (const key of TypedObject.keys(input)) {
|
|
35
|
+
switch (key) {
|
|
36
|
+
case 'text': style = style.hex(input[key]!); break;
|
|
37
|
+
case 'background': style = style.bgHex(input[key]!); break;
|
|
38
|
+
default: style = (input[key] ? style[key] : style); break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return style;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read foreground/background color if env var is set
|
|
47
|
+
*/
|
|
48
|
+
static isBackgroundDark(): boolean {
|
|
49
|
+
const key = Env.COLORFGBG.val ?? '';
|
|
50
|
+
|
|
51
|
+
if (this.#scheme.key === key) {
|
|
52
|
+
return this.#scheme.dark;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const [, bg = '0'] = key.split(';');
|
|
56
|
+
const dark = this.#darkAnsi256.has(+bg);
|
|
57
|
+
Object.assign(this.#scheme, { key, dark });
|
|
58
|
+
return dark;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create renderer from input source
|
|
63
|
+
*/
|
|
64
|
+
static getThemedStyle(input: TermStylePairInput): TermStyleFn {
|
|
65
|
+
const [dark, light] = (Array.isArray(input) ? input : [input]);
|
|
66
|
+
const isDark = this.isBackgroundDark();
|
|
67
|
+
return isDark ? this.getStyle(dark) : this.getStyle(light ?? dark);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Is styling currently enabled
|
|
72
|
+
*/
|
|
73
|
+
static get enabled(): boolean {
|
|
74
|
+
return chalk.level > 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build style palette, with support for background theme awareness
|
|
79
|
+
*/
|
|
80
|
+
static getPalette<K extends string>(inp: Record<K, TermStylePairInput>): Record<K, TermStyleFn> {
|
|
81
|
+
return TypedObject.fromEntries(
|
|
82
|
+
TypedObject.entries(inp).map(([k, v]) => [k, this.getThemedStyle(v)]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Make a template function based on the input set
|
|
87
|
+
*/
|
|
88
|
+
static getTemplate<K extends string>(input: Record<K, TermStylePairInput>): TermStyledTemplate<K> {
|
|
89
|
+
const palette = this.getPalette(input);
|
|
90
|
+
|
|
91
|
+
return (values: TemplateStringsArray, ...keys: (Partial<Record<K, TemplatePrim>> | string)[]) => {
|
|
92
|
+
if (keys.length === 0) {
|
|
93
|
+
return values[0];
|
|
94
|
+
} else {
|
|
95
|
+
const out = keys.map((el, i) => {
|
|
96
|
+
let final = el;
|
|
97
|
+
if (typeof el !== 'string') {
|
|
98
|
+
const subKeys = TypedObject.keys(el);
|
|
99
|
+
if (subKeys.length !== 1) {
|
|
100
|
+
throw new Error('Invalid template variable, one and only one key should be specified');
|
|
101
|
+
}
|
|
102
|
+
const [k] = subKeys;
|
|
103
|
+
const v = el[k]!;
|
|
104
|
+
final = v === undefined ? '' : palette[k](v)!;
|
|
105
|
+
}
|
|
106
|
+
return `${values[i] ?? ''}${final ?? ''}`;
|
|
107
|
+
});
|
|
108
|
+
if (values.length > keys.length) {
|
|
109
|
+
out.push(values[values.length - 1]);
|
|
110
|
+
}
|
|
111
|
+
return out.join('');
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Remove style escape sequences
|
|
118
|
+
*/
|
|
119
|
+
static cleanText(text: string): string {
|
|
120
|
+
return text.replace(ANSI_CODE_REGEX, '');
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/terminal.ts
CHANGED
|
@@ -1,208 +1,155 @@
|
|
|
1
|
-
import
|
|
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
|
-
minDelay?: number;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
type TerminalProgressConfig = TerminalStreamPositionConfig & {
|
|
20
|
-
style?: TermStyleInput;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* An enhanced tty write stream
|
|
25
|
-
*/
|
|
26
|
-
export class Terminal implements TermState {
|
|
27
|
-
|
|
28
|
-
static async for(config: Partial<TermState>): Promise<Terminal> {
|
|
29
|
-
const term = new Terminal(config);
|
|
30
|
-
await term.init();
|
|
31
|
-
return term;
|
|
32
|
-
}
|
|
1
|
+
import timers from 'node:timers/promises';
|
|
2
|
+
import tty from 'node:tty';
|
|
33
3
|
|
|
34
|
-
|
|
35
|
-
#output: tty.WriteStream;
|
|
36
|
-
#input: tty.ReadStream;
|
|
37
|
-
#interactive: boolean;
|
|
38
|
-
#width?: number;
|
|
39
|
-
#backgroundScheme?: TermColorScheme;
|
|
40
|
-
#colorLevel?: TermColorLevel;
|
|
41
|
-
#query: TerminalQuerier;
|
|
42
|
-
|
|
43
|
-
constructor(config: Partial<TermState>) {
|
|
44
|
-
this.#output = config.output ?? process.stdout;
|
|
45
|
-
this.#input = config.input ?? process.stdin;
|
|
46
|
-
this.#interactive = config.interactive ?? (this.#output.isTTY && !/^(true|yes|on|1)$/i.test(process.env.TRV_QUIET ?? ''));
|
|
47
|
-
this.#width = config.width;
|
|
48
|
-
this.#colorLevel = config.colorLevel;
|
|
49
|
-
this.#backgroundScheme = config.backgroundScheme;
|
|
50
|
-
this.#query = TerminalQuerier.for(this.#input, this.#output);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
get output(): tty.WriteStream {
|
|
54
|
-
return this.#output;
|
|
55
|
-
}
|
|
4
|
+
import { Env, Util } from '@travetto/base';
|
|
56
5
|
|
|
57
|
-
|
|
58
|
-
return this.#input;
|
|
59
|
-
}
|
|
6
|
+
import { TerminalWriter } from './writer';
|
|
60
7
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
8
|
+
type TerminalStreamingConfig = { minDelay?: number, outputStreamToMain?: boolean };
|
|
9
|
+
type Coord = { x: number, y: number };
|
|
64
10
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
11
|
+
export const WAIT_TOKEN = '%WAIT%';
|
|
12
|
+
const STD_WAIT_STATES = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'.split('');
|
|
68
13
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
14
|
+
const lineStatus = (l: string): string => l.replace(WAIT_TOKEN, ' ');
|
|
15
|
+
const lineMain = (l: string): string => l.replace(WAIT_TOKEN, '').trim();
|
|
72
16
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
17
|
+
/** An basic tty wrapper */
|
|
18
|
+
export class Terminal {
|
|
76
19
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
async writeLines(...text: string[]): Promise<void> {
|
|
86
|
-
return this.writer().writeLines(text, this.interactive).commit();
|
|
87
|
-
}
|
|
20
|
+
#interactive: boolean;
|
|
21
|
+
#writer: TerminalWriter;
|
|
22
|
+
#reset: () => Promise<void>;
|
|
23
|
+
#cleanExit = 0;
|
|
24
|
+
#width: number;
|
|
25
|
+
#height: number;
|
|
26
|
+
#output: tty.WriteStream;
|
|
88
27
|
|
|
89
|
-
async
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
})();
|
|
98
|
-
process.on('exit', () => this.reset());
|
|
28
|
+
async #showWaitingIndicator(pos: Coord, signal: AbortSignal): Promise<void> {
|
|
29
|
+
let done = false;
|
|
30
|
+
signal.addEventListener('abort', () => done = true);
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (!done) {
|
|
33
|
+
await this.#writer.setPosition(pos).write(STD_WAIT_STATES[i++ % STD_WAIT_STATES.length]).commit(true);
|
|
34
|
+
await timers.setTimeout(100);
|
|
99
35
|
}
|
|
100
|
-
return this.#init;
|
|
101
36
|
}
|
|
102
37
|
|
|
103
|
-
|
|
104
|
-
this.#
|
|
105
|
-
|
|
106
|
-
this.#output.write(this.writer().resetCommands());
|
|
38
|
+
#cleanOnExit(): () => void {
|
|
39
|
+
if (this.#cleanExit === 0) {
|
|
40
|
+
process.on('exit', this.#reset);
|
|
107
41
|
}
|
|
42
|
+
this.#cleanExit += 1;
|
|
43
|
+
return () => (this.#cleanExit -= 1) === 0 && process.off('exit', this.#reset);
|
|
108
44
|
}
|
|
109
45
|
|
|
110
|
-
|
|
111
|
-
|
|
46
|
+
constructor(output?: tty.WriteStream, config?: { width?: number, height?: number }) {
|
|
47
|
+
this.#output = output ?? process.stdout;
|
|
48
|
+
this.#interactive = this.#output.isTTY && !Env.TRV_QUIET.isTrue;
|
|
49
|
+
this.#width = config?.width ?? (this.#output.isTTY ? this.#output.columns : 120);
|
|
50
|
+
this.#height = config?.height ?? (this.#output.isTTY ? this.#output.rows : 120);
|
|
51
|
+
const w = this.#writer = new TerminalWriter(this);
|
|
52
|
+
this.#reset = (): Promise<void> => w.softReset().commit();
|
|
112
53
|
}
|
|
113
54
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!this.interactive) {
|
|
120
|
-
await this.writeLines(`${message}...`);
|
|
121
|
-
return res;
|
|
122
|
-
}
|
|
123
|
-
return res.finally(TerminalOperation.streamWaiting(this, message, config));
|
|
124
|
-
}
|
|
55
|
+
get output(): tty.WriteStream { return this.#output; }
|
|
56
|
+
get width(): number { return this.#width; }
|
|
57
|
+
get height(): number { return this.#height; }
|
|
58
|
+
get writer(): TerminalWriter { return this.#writer; }
|
|
59
|
+
get interactive(): boolean { return this.#interactive; }
|
|
125
60
|
|
|
126
61
|
/**
|
|
127
|
-
* Stream
|
|
128
|
-
*
|
|
129
|
-
* @param lines
|
|
130
|
-
* @param config
|
|
131
|
-
* @returns
|
|
62
|
+
* Stream lines if interactive, with waiting indicator, otherwise print out
|
|
132
63
|
*/
|
|
133
|
-
async
|
|
134
|
-
if (!this
|
|
135
|
-
for await (const line of
|
|
64
|
+
async streamLines(source: AsyncIterable<string | undefined>): Promise<void> {
|
|
65
|
+
if (!this.#interactive) {
|
|
66
|
+
for await (const line of source) {
|
|
136
67
|
if (line !== undefined) {
|
|
137
|
-
|
|
138
|
-
await this.writeLines(out);
|
|
68
|
+
await this.#writer.writeLine(`> ${line}`).commit();
|
|
139
69
|
}
|
|
140
70
|
}
|
|
141
71
|
} else {
|
|
142
|
-
|
|
72
|
+
await this.streamToBottom(Util.mapAsyncItr(source, x => `%WAIT% ${x}`), { outputStreamToMain: true });
|
|
143
73
|
}
|
|
144
74
|
}
|
|
145
75
|
|
|
146
76
|
/**
|
|
147
|
-
*
|
|
77
|
+
* Allows for writing at bottom of screen with scrolling support for main content
|
|
148
78
|
*/
|
|
149
|
-
async
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
79
|
+
async streamToBottom(source: AsyncIterable<string | undefined>, config: TerminalStreamingConfig = {}): Promise<void> {
|
|
80
|
+
const writePos = { x: 0, y: -1 };
|
|
81
|
+
const minDelay = config.minDelay ?? 0;
|
|
82
|
+
const remove = this.#cleanOnExit();
|
|
83
|
+
|
|
84
|
+
let prev: string | undefined;
|
|
85
|
+
let stop: AbortController | undefined;
|
|
86
|
+
let start = Date.now();
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await this.#writer.hideCursor()
|
|
90
|
+
.storePosition().scrollRange({ end: -2 }).restorePosition()
|
|
91
|
+
.changePosition({ y: -1 }).write('\n')
|
|
92
|
+
.commit();
|
|
93
|
+
|
|
94
|
+
for await (const line of source) {
|
|
95
|
+
// Previous line
|
|
96
|
+
if (prev && config.outputStreamToMain) {
|
|
97
|
+
await this.writer.writeLine(lineMain(prev)).commit();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (line && (Date.now() - start) >= minDelay) {
|
|
101
|
+
start = Date.now();
|
|
102
|
+
stop?.abort();
|
|
103
|
+
this.writer.setPosition(writePos).write(lineStatus(line)).clearLine(1).commit(true);
|
|
104
|
+
|
|
105
|
+
const idx = line.indexOf(WAIT_TOKEN);
|
|
106
|
+
if (idx >= 0) {
|
|
107
|
+
stop = new AbortController();
|
|
108
|
+
this.#showWaitingIndicator({ y: writePos.y, x: idx }, stop.signal);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
prev = line;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
stop?.abort();
|
|
116
|
+
if (prev !== undefined && config.outputStreamToMain) {
|
|
117
|
+
await this.writer.writeLine(lineMain(prev)).commit();
|
|
161
118
|
}
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
119
|
|
|
165
|
-
|
|
120
|
+
await this.#writer.setPosition(writePos).clearLine().commit(true);
|
|
121
|
+
} finally {
|
|
122
|
+
await this.#writer.softReset().commit();
|
|
123
|
+
remove();
|
|
124
|
+
}
|
|
166
125
|
}
|
|
167
126
|
|
|
168
127
|
/**
|
|
169
|
-
*
|
|
128
|
+
* Consumes a stream, of events, tied to specific list indices, and updates in place
|
|
170
129
|
*/
|
|
171
|
-
async
|
|
172
|
-
if (!this
|
|
173
|
-
|
|
174
|
-
|
|
130
|
+
async streamList(source: AsyncIterable<{ idx: number, text: string, done?: boolean }>): Promise<void> {
|
|
131
|
+
if (!this.#interactive) {
|
|
132
|
+
const collected = [];
|
|
133
|
+
for await (const ev of source) {
|
|
134
|
+
if (ev.done) {
|
|
135
|
+
collected[ev.idx] = ev.text;
|
|
136
|
+
}
|
|
175
137
|
}
|
|
176
|
-
await
|
|
138
|
+
await this.#writer.writeLines(collected).commit();
|
|
177
139
|
return;
|
|
178
140
|
}
|
|
179
|
-
return TerminalOperation.streamToPosition(this, IterableUtil.map(source, resolve), config);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Track progress of an asynchronous iterator, allowing the showing of a progress bar if the stream produces idx and total
|
|
184
|
-
*/
|
|
185
|
-
async trackProgress<T, V extends TerminalProgressEvent>(
|
|
186
|
-
source: AsyncIterable<T>, resolve: MapFn<T, V>, config?: TerminalProgressConfig
|
|
187
|
-
): Promise<void> {
|
|
188
|
-
const render = TerminalOperation.buildProgressBar(this, config?.style ?? { background: 'limeGreen', text: 'black' });
|
|
189
|
-
return this.streamToPosition(source, async (v, i) => render(await resolve(v, i)), config);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Creates a colorer function */
|
|
193
|
-
colorer(style: TermStyleInput | [light: TermStyleInput, dark: TermStyleInput]): TermColorFn {
|
|
194
|
-
return ColorOutputUtil.colorer(this, style);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Creates a color palette based on input styles */
|
|
198
|
-
palette<P extends TermColorPaletteInput>(input: P): TermColorPalette<P> {
|
|
199
|
-
return ColorOutputUtil.palette(this, input);
|
|
200
|
-
}
|
|
201
141
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
142
|
+
const remove = this.#cleanOnExit();
|
|
143
|
+
let max = 0;
|
|
144
|
+
try {
|
|
145
|
+
await this.#writer.hideCursor().commit();
|
|
146
|
+
for await (const { idx, text } of source) {
|
|
147
|
+
max = Math.max(idx, max);
|
|
148
|
+
await this.#writer.write('\n'.repeat(idx)).setPosition({ x: 0 }).write(text).clearLine(1).changePosition({ y: -idx }).commit();
|
|
149
|
+
}
|
|
150
|
+
} finally {
|
|
151
|
+
await this.#writer.changePosition({ y: max + 1 }).writeLine('\n').softReset().commit();
|
|
152
|
+
remove();
|
|
153
|
+
}
|
|
205
154
|
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export const GlobalTerminal = new Terminal({ output: process.stdout });
|
|
155
|
+
}
|
package/src/trv.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ColorLevel } from './style';
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
interface TravettoEnv {
|
|
5
|
+
/**
|
|
6
|
+
* Flag for node to disable colors
|
|
7
|
+
*/
|
|
8
|
+
NODE_DISABLE_COLORS: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Terminal colors provided as ansi 256 color schemes
|
|
11
|
+
*/
|
|
12
|
+
COLORFGBG: string;
|
|
13
|
+
/**
|
|
14
|
+
* Enables color, even if `tty` is not available
|
|
15
|
+
* @default undefined
|
|
16
|
+
*/
|
|
17
|
+
FORCE_COLOR: boolean | ColorLevel;
|
|
18
|
+
/**
|
|
19
|
+
* Disables color even if `tty` is available
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
NO_COLOR: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Determines terminal color level
|
|
25
|
+
*/
|
|
26
|
+
COLORTERM: string;
|
|
27
|
+
/**
|
|
28
|
+
* Terminal operation mode, false means simple output
|
|
29
|
+
*/
|
|
30
|
+
TRV_QUIET: boolean;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { StyleUtil, TermStyleFn, TermStyleInput } from './style';
|
|
2
|
+
import { Terminal, WAIT_TOKEN } from './terminal';
|
|
3
|
+
|
|
4
|
+
type ProgressEvent<T> = { total?: number, idx: number, value: T };
|
|
5
|
+
type ProgressStyle = { complete: TermStyleFn, incomplete?: TermStyleFn };
|
|
6
|
+
|
|
7
|
+
export class TerminalUtil {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a progress bar updater, suitable for streaming to the bottom of the screen
|
|
11
|
+
*/
|
|
12
|
+
static progressBarUpdater(
|
|
13
|
+
term: Terminal,
|
|
14
|
+
cfg?: {
|
|
15
|
+
withWaiting?: boolean;
|
|
16
|
+
style?: { complete: TermStyleInput, incomplete?: TermStyleInput } | (() => ProgressStyle);
|
|
17
|
+
}
|
|
18
|
+
): (ev: ProgressEvent<string>) => string {
|
|
19
|
+
const styleBase = typeof cfg?.style !== 'function' ? {
|
|
20
|
+
complete: StyleUtil.getStyle(cfg?.style?.complete ?? { background: '#32cd32', text: '#ffffff' }),
|
|
21
|
+
incomplete: cfg?.style?.incomplete ? StyleUtil.getStyle(cfg.style.incomplete) : undefined,
|
|
22
|
+
} : undefined;
|
|
23
|
+
|
|
24
|
+
const style = typeof cfg?.style === 'function' ? cfg.style : (): ProgressStyle => styleBase!;
|
|
25
|
+
|
|
26
|
+
let width: number;
|
|
27
|
+
return ev => {
|
|
28
|
+
const text = ev.value ?? (ev.total ? '%idx/%total' : '%idx');
|
|
29
|
+
const pct = ev.total === undefined ? 0 : (ev.idx / ev.total);
|
|
30
|
+
if (ev.total) {
|
|
31
|
+
width ??= Math.trunc(Math.ceil(Math.log10(ev.total ?? 10000)));
|
|
32
|
+
}
|
|
33
|
+
const state: Record<string, string> = { total: `${ev.total}`, idx: `${ev.idx}`.padStart(width ?? 0), pct: `${Math.trunc(pct * 100)}` };
|
|
34
|
+
const line = text.replace(/[%](idx|total|pct)/g, (_, k) => state[k]);
|
|
35
|
+
const full = term.writer.padToWidth(line, cfg?.withWaiting ? 2 : 0);
|
|
36
|
+
const mid = Math.trunc(pct * term.width);
|
|
37
|
+
const [l, r] = [full.substring(0, mid), full.substring(mid)];
|
|
38
|
+
|
|
39
|
+
const { complete, incomplete } = style();
|
|
40
|
+
return `${cfg?.withWaiting ? `${WAIT_TOKEN} ` : ''}${complete(l)}${incomplete?.(r) ?? r}`;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|