@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 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 or [named colors](https://github.com/travetto/travetto/tree/main/module/terminal/src/named-colors.ts#L1) modeled after the standard HTML color names. The module also provides the ability to specify 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.
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 determine 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.
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 { Util } from '@travetto/base';
33
- import { GlobalTerminal } from '@travetto/terminal';
34
-
35
- const tplFn = GlobalTerminal.templateFunction({
36
- input: 'oliveDrab',
37
- output: 'pink',
38
- path: 'teal',
39
- success: 'green',
40
- failure: 'red',
41
- param: ['yellow', 'goldenrod'],
42
- type: 'cyan',
43
- description: ['white', 'gray'],
44
- title: ['brightWhite', 'black'],
45
- identifier: 'dodgerBlue',
46
- subtitle: ['lightGray', 'darkGray'],
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
- 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';
1
+ /// <reference path="./src/trv.d.ts" />
2
+ export * from './src/style';
8
3
  export * from './src/terminal';
9
- export * from './src/types';
10
- export * from './src/writer';
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.4.0",
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 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
- 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
- #init: Promise<void>;
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
- get input(): tty.ReadStream {
58
- return this.#input;
59
- }
6
+ import { TerminalWriter } from './writer';
60
7
 
61
- get interactive(): boolean {
62
- return this.#interactive;
63
- }
8
+ type TerminalStreamingConfig = { minDelay?: number, outputStreamToMain?: boolean };
9
+ type Coord = { x: number, y: number };
64
10
 
65
- get width(): number {
66
- return this.#width ?? (this.#output.isTTY ? this.#output.columns : 120);
67
- }
11
+ export const WAIT_TOKEN = '%WAIT%';
12
+ const STD_WAIT_STATES = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'.split('');
68
13
 
69
- get height(): number {
70
- return (this.#output.isTTY ? this.#output.rows : 120);
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
- get colorLevel(): TermColorLevel {
74
- return this.#colorLevel ?? 0;
75
- }
17
+ /** An basic tty wrapper */
18
+ export class Terminal {
76
19
 
77
- get backgroundScheme(): TermColorScheme {
78
- return this.#backgroundScheme ?? 'dark';
79
- }
80
-
81
- writer(): TerminalWriter {
82
- return TerminalWriter.for(this);
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 init(): Promise<void> {
90
- if (!this.#init) {
91
- this.#init = (async (): Promise<void> => {
92
- this.#colorLevel ??= await ColorOutputUtil.readTermColorLevel(this.#output);
93
- this.#backgroundScheme ??= await ColorOutputUtil.readBackgroundScheme(
94
- this.#output,
95
- () => this.interactive ? this.#query.backgroundColor() : undefined
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
- reset(): void {
104
- this.#query.close();
105
- if (this.interactive) {
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
- getCursorPosition(): Promise<TermCoord> {
111
- return this.#query.cursorPosition();
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
- * Waiting message with a callback to end
116
- */
117
- async withWaiting<T>(message: string, work: Promise<T> | (() => Promise<T>), config: TerminalWaitingConfig = {}): Promise<T> {
118
- const res = (typeof work === 'function' ? work() : work);
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 line output, showing a waiting indicator for each line until the next one occurs
128
- *
129
- * @param lines
130
- * @param config
131
- * @returns
62
+ * Stream lines if interactive, with waiting indicator, otherwise print out
132
63
  */
133
- async streamLinesWithWaiting(lines: AsyncIterable<string | undefined>, config: TerminalWaitingConfig): Promise<void> {
134
- if (!this.interactive) {
135
- for await (const line of lines) {
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
- const out = config.committedPrefix ? `${config.committedPrefix} ${line}` : line;
138
- await this.writeLines(out);
68
+ await this.#writer.writeLine(`> ${line}`).commit();
139
69
  }
140
70
  }
141
71
  } else {
142
- return TerminalOperation.streamLinesWithWaiting(this, lines, { position: 'bottom', ...config });
72
+ await this.streamToBottom(Util.mapAsyncItr(source, x => `%WAIT% ${x}`), { outputStreamToMain: true });
143
73
  }
144
74
  }
145
75
 
146
76
  /**
147
- * Consumes a stream, of events, tied to specific list indices, and updates in place
77
+ * Allows for writing at bottom of screen with scrolling support for main content
148
78
  */
149
- async streamList<T>(source: AsyncIterable<T>, resolve: MapFn<T, TerminalTableEvent>, config: TerminalTableConfig = {}): Promise<void> {
150
- const resolved = IterableUtil.map(source, resolve);
151
-
152
- await this.writeLines(...(config.header ?? []));
153
-
154
- if (!this.interactive) {
155
- const isDone = IterableUtil.filter(resolved, ev => !!ev.done);
156
- if (config.forceNonInteractiveOrder) {
157
- await this.writeLines(...(await IterableUtil.drain(isDone)).map(x => x.text), '');
158
- } else {
159
- await IterableUtil.drain(IterableUtil.map(isDone, ev => this.writeLines(ev.text)));
160
- await this.writeLines('');
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
- await TerminalOperation.streamList(this, resolved);
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
- * Streams an iterable to a specific location, with support for non-interactive ttys
128
+ * Consumes a stream, of events, tied to specific list indices, and updates in place
170
129
  */
171
- async streamToPosition<T>(source: AsyncIterable<T>, resolve: MapFn<T, string>, config?: TerminalStreamPositionConfig): Promise<void> {
172
- if (!this.interactive) {
173
- if (config?.staticMessage) {
174
- await this.writeLines(config.staticMessage);
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 IterableUtil.drain(source);
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
- /** Convenience method to creates a color template function based on input styles */
203
- templateFunction<P extends TermColorPaletteInput>(input: P): (key: keyof P, val: Prim) => string {
204
- return ColorOutputUtil.templateFunction(this, input);
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
+ }