1 | 'use strict';
|
2 | const readline = require('readline');
|
3 | const chalk = require('chalk');
|
4 | const cliCursor = require('cli-cursor');
|
5 | const cliSpinners = require('cli-spinners');
|
6 | const logSymbols = require('log-symbols');
|
7 | const stripAnsi = require('strip-ansi');
|
8 | const wcwidth = require('wcwidth');
|
9 | const isInteractive = require('is-interactive');
|
10 | const MuteStream = require('mute-stream');
|
11 |
|
12 | const TEXT = Symbol('text');
|
13 | const PREFIX_TEXT = Symbol('prefixText');
|
14 |
|
15 | const ASCII_ETX_CODE = 0x03;
|
16 |
|
17 | class StdinDiscarder {
|
18 | constructor() {
|
19 | this.requests = 0;
|
20 |
|
21 | this.mutedStream = new MuteStream();
|
22 | this.mutedStream.pipe(process.stdout);
|
23 | this.mutedStream.mute();
|
24 |
|
25 | const self = this;
|
26 | this.ourEmit = function (event, data, ...args) {
|
27 | const {stdin} = process;
|
28 | if (self.requests > 0 || stdin.emit === self.ourEmit) {
|
29 | if (event === 'keypress') {
|
30 | return;
|
31 | }
|
32 |
|
33 | if (event === 'data' && data.includes(ASCII_ETX_CODE)) {
|
34 | process.emit('SIGINT');
|
35 | }
|
36 |
|
37 | Reflect.apply(self.oldEmit, this, [event, data, ...args]);
|
38 | } else {
|
39 | Reflect.apply(process.stdin.emit, this, [event, data, ...args]);
|
40 | }
|
41 | };
|
42 | }
|
43 |
|
44 | start() {
|
45 | this.requests++;
|
46 |
|
47 | if (this.requests === 1) {
|
48 | this.realStart();
|
49 | }
|
50 | }
|
51 |
|
52 | stop() {
|
53 | if (this.requests <= 0) {
|
54 | throw new Error('`stop` called more times than `start`');
|
55 | }
|
56 |
|
57 | this.requests--;
|
58 |
|
59 | if (this.requests === 0) {
|
60 | this.realStop();
|
61 | }
|
62 | }
|
63 |
|
64 | realStart() {
|
65 |
|
66 | if (process.platform === 'win32') {
|
67 | return;
|
68 | }
|
69 |
|
70 | this.rl = readline.createInterface({
|
71 | input: process.stdin,
|
72 | output: this.mutedStream
|
73 | });
|
74 |
|
75 | this.rl.on('SIGINT', () => {
|
76 | if (process.listenerCount('SIGINT') === 0) {
|
77 | process.emit('SIGINT');
|
78 | } else {
|
79 | this.rl.close();
|
80 | process.kill(process.pid, 'SIGINT');
|
81 | }
|
82 | });
|
83 | }
|
84 |
|
85 | realStop() {
|
86 | if (process.platform === 'win32') {
|
87 | return;
|
88 | }
|
89 |
|
90 | this.rl.close();
|
91 | this.rl = undefined;
|
92 | }
|
93 | }
|
94 |
|
95 | const stdinDiscarder = new StdinDiscarder();
|
96 |
|
97 | class Ora {
|
98 | constructor(options) {
|
99 | if (typeof options === 'string') {
|
100 | options = {
|
101 | text: options
|
102 | };
|
103 | }
|
104 |
|
105 | this.options = {
|
106 | text: '',
|
107 | color: 'cyan',
|
108 | stream: process.stderr,
|
109 | discardStdin: true,
|
110 | ...options
|
111 | };
|
112 |
|
113 | this.spinner = this.options.spinner;
|
114 |
|
115 | this.color = this.options.color;
|
116 | this.hideCursor = this.options.hideCursor !== false;
|
117 | this.interval = this.options.interval || this.spinner.interval || 100;
|
118 | this.stream = this.options.stream;
|
119 | this.id = undefined;
|
120 | this.isEnabled = typeof this.options.isEnabled === 'boolean' ? this.options.isEnabled : isInteractive({stream: this.stream});
|
121 |
|
122 |
|
123 | this.text = this.options.text;
|
124 | this.prefixText = this.options.prefixText;
|
125 | this.linesToClear = 0;
|
126 | this.indent = this.options.indent;
|
127 | this.discardStdin = this.options.discardStdin;
|
128 | this.isDiscardingStdin = false;
|
129 | }
|
130 |
|
131 | get indent() {
|
132 | return this._indent;
|
133 | }
|
134 |
|
135 | set indent(indent = 0) {
|
136 | if (!(indent >= 0 && Number.isInteger(indent))) {
|
137 | throw new Error('The `indent` option must be an integer from 0 and up');
|
138 | }
|
139 |
|
140 | this._indent = indent;
|
141 | }
|
142 |
|
143 | _updateInterval(interval) {
|
144 | if (interval !== undefined) {
|
145 | this.interval = interval;
|
146 | }
|
147 | }
|
148 |
|
149 | get spinner() {
|
150 | return this._spinner;
|
151 | }
|
152 |
|
153 | set spinner(spinner) {
|
154 | this.frameIndex = 0;
|
155 |
|
156 | if (typeof spinner === 'object') {
|
157 | if (spinner.frames === undefined) {
|
158 | throw new Error('The given spinner must have a `frames` property');
|
159 | }
|
160 |
|
161 | this._spinner = spinner;
|
162 | } else if (process.platform === 'win32') {
|
163 | this._spinner = cliSpinners.line;
|
164 | } else if (spinner === undefined) {
|
165 |
|
166 | this._spinner = cliSpinners.dots;
|
167 | } else if (cliSpinners[spinner]) {
|
168 | this._spinner = cliSpinners[spinner];
|
169 | } else {
|
170 | throw new Error(`There is no built-in spinner named '${spinner}'. See https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json for a full list.`);
|
171 | }
|
172 |
|
173 | this._updateInterval(this._spinner.interval);
|
174 | }
|
175 |
|
176 | get text() {
|
177 | return this[TEXT];
|
178 | }
|
179 |
|
180 | get prefixText() {
|
181 | return this[PREFIX_TEXT];
|
182 | }
|
183 |
|
184 | get isSpinning() {
|
185 | return this.id !== undefined;
|
186 | }
|
187 |
|
188 | updateLineCount() {
|
189 | const columns = this.stream.columns || 80;
|
190 | const fullPrefixText = (typeof this[PREFIX_TEXT] === 'string') ? this[PREFIX_TEXT] + '-' : '';
|
191 | this.lineCount = stripAnsi(fullPrefixText + '--' + this[TEXT]).split('\n').reduce((count, line) => {
|
192 | return count + Math.max(1, Math.ceil(wcwidth(line) / columns));
|
193 | }, 0);
|
194 | }
|
195 |
|
196 | set text(value) {
|
197 | this[TEXT] = value;
|
198 | this.updateLineCount();
|
199 | }
|
200 |
|
201 | set prefixText(value) {
|
202 | this[PREFIX_TEXT] = value;
|
203 | this.updateLineCount();
|
204 | }
|
205 |
|
206 | frame() {
|
207 | const {frames} = this.spinner;
|
208 | let frame = frames[this.frameIndex];
|
209 |
|
210 | if (this.color) {
|
211 | frame = chalk[this.color](frame);
|
212 | }
|
213 |
|
214 | this.frameIndex = ++this.frameIndex % frames.length;
|
215 | const fullPrefixText = (typeof this.prefixText === 'string' && this.prefixText !== '') ? this.prefixText + ' ' : '';
|
216 | const fullText = typeof this.text === 'string' ? ' ' + this.text : '';
|
217 |
|
218 | return fullPrefixText + frame + fullText;
|
219 | }
|
220 |
|
221 | clear() {
|
222 | if (!this.isEnabled || !this.stream.isTTY) {
|
223 | return this;
|
224 | }
|
225 |
|
226 | for (let i = 0; i < this.linesToClear; i++) {
|
227 | if (i > 0) {
|
228 | this.stream.moveCursor(0, -1);
|
229 | }
|
230 |
|
231 | this.stream.clearLine();
|
232 | this.stream.cursorTo(this.indent);
|
233 | }
|
234 |
|
235 | this.linesToClear = 0;
|
236 |
|
237 | return this;
|
238 | }
|
239 |
|
240 | render() {
|
241 | this.clear();
|
242 | this.stream.write(this.frame());
|
243 | this.linesToClear = this.lineCount;
|
244 |
|
245 | return this;
|
246 | }
|
247 |
|
248 | start(text) {
|
249 | if (text) {
|
250 | this.text = text;
|
251 | }
|
252 |
|
253 | if (!this.isEnabled) {
|
254 | if (this.text) {
|
255 | this.stream.write(`- ${this.text}\n`);
|
256 | }
|
257 |
|
258 | return this;
|
259 | }
|
260 |
|
261 | if (this.isSpinning) {
|
262 | return this;
|
263 | }
|
264 |
|
265 | if (this.hideCursor) {
|
266 | cliCursor.hide(this.stream);
|
267 | }
|
268 |
|
269 | if (this.discardStdin && process.stdin.isTTY) {
|
270 | this.isDiscardingStdin = true;
|
271 | stdinDiscarder.start();
|
272 | }
|
273 |
|
274 | this.render();
|
275 | this.id = setInterval(this.render.bind(this), this.interval);
|
276 |
|
277 | return this;
|
278 | }
|
279 |
|
280 | stop() {
|
281 | if (!this.isEnabled) {
|
282 | return this;
|
283 | }
|
284 |
|
285 | clearInterval(this.id);
|
286 | this.id = undefined;
|
287 | this.frameIndex = 0;
|
288 | this.clear();
|
289 | if (this.hideCursor) {
|
290 | cliCursor.show(this.stream);
|
291 | }
|
292 |
|
293 | if (this.discardStdin && process.stdin.isTTY && this.isDiscardingStdin) {
|
294 | stdinDiscarder.stop();
|
295 | this.isDiscardingStdin = false;
|
296 | }
|
297 |
|
298 | return this;
|
299 | }
|
300 |
|
301 | succeed(text) {
|
302 | return this.stopAndPersist({symbol: logSymbols.success, text});
|
303 | }
|
304 |
|
305 | fail(text) {
|
306 | return this.stopAndPersist({symbol: logSymbols.error, text});
|
307 | }
|
308 |
|
309 | warn(text) {
|
310 | return this.stopAndPersist({symbol: logSymbols.warning, text});
|
311 | }
|
312 |
|
313 | info(text) {
|
314 | return this.stopAndPersist({symbol: logSymbols.info, text});
|
315 | }
|
316 |
|
317 | stopAndPersist(options = {}) {
|
318 | const prefixText = options.prefixText || this.prefixText;
|
319 | const fullPrefixText = (typeof prefixText === 'string' && prefixText !== '') ? prefixText + ' ' : '';
|
320 | const text = options.text || this.text;
|
321 | const fullText = (typeof text === 'string') ? ' ' + text : '';
|
322 |
|
323 | this.stop();
|
324 | this.stream.write(`${fullPrefixText}${options.symbol || ' '}${fullText}\n`);
|
325 |
|
326 | return this;
|
327 | }
|
328 | }
|
329 |
|
330 | const oraFactory = function (options) {
|
331 | return new Ora(options);
|
332 | };
|
333 |
|
334 | module.exports = oraFactory;
|
335 |
|
336 | module.exports.promise = (action, options) => {
|
337 |
|
338 | if (typeof action.then !== 'function') {
|
339 | throw new TypeError('Parameter `action` must be a Promise');
|
340 | }
|
341 |
|
342 | const spinner = new Ora(options);
|
343 | spinner.start();
|
344 |
|
345 | (async () => {
|
346 | try {
|
347 | await action;
|
348 | spinner.succeed();
|
349 | } catch (_) {
|
350 | spinner.fail();
|
351 | }
|
352 | })();
|
353 |
|
354 | return spinner;
|
355 | };
|