From a4b225352329b68aa871c968f79f4f61191e29d3 Mon Sep 17 00:00:00 2001 From: Yanis Benson Date: Wed, 13 Nov 2019 12:50:28 +0300 Subject: [PATCH] Fix `discardStdin` option (#135) --- example.js | 8 +++ index.d.ts | 2 + index.js | 140 ++++++++++++++++++++++++++++++++------------------- package.json | 1 + readme.md | 2 + 5 files changed, 101 insertions(+), 52 deletions(-) diff --git a/example.js b/example.js index 1a6585d..9d04be3 100644 --- a/example.js +++ b/example.js @@ -15,6 +15,14 @@ const spinnerDiscardingStdin = new Ora({ spinnerDiscardingStdin.start(); +setTimeout(() => { + spinnerDiscardingStdin.succeed(); +}, 1000); + +setTimeout(() => { + spinnerDiscardingStdin.start(); +}, 2000); + setTimeout(() => { spinnerDiscardingStdin.succeed(); spinner.start(); diff --git a/index.d.ts b/index.d.ts index e478463..e0f3f4a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -94,6 +94,8 @@ declare namespace ora { /** Discard stdin input (except Ctrl+C) while running if it's TTY. This prevents the spinner from twitching on input, outputting broken lines on `Enter` key presses, and prevents buffering of input while the spinner is running. + + This has no effect on Windows as there's no good way to implement discarding stdin properly there. @default true */ diff --git a/index.js b/index.js index ba61ae2..35f903a 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ 'use strict'; +const readline = require('readline'); const chalk = require('chalk'); const cliCursor = require('cli-cursor'); const cliSpinners = require('cli-spinners'); @@ -6,14 +7,93 @@ const logSymbols = require('log-symbols'); const stripAnsi = require('strip-ansi'); const wcwidth = require('wcwidth'); const isInteractive = require('is-interactive'); +const MuteStream = require('mute-stream'); const TEXT = Symbol('text'); const PREFIX_TEXT = Symbol('prefixText'); -const noop = () => {}; - const ASCII_ETX_CODE = 0x03; // Ctrl+C emits this code +class StdinDiscarder { + constructor() { + this.requests = 0; + + this.mutedStream = new MuteStream(); + this.mutedStream.pipe(process.stdout); + this.mutedStream.mute(); + + const self = this; + this.ourEmit = function (event, data, ...args) { + const {stdin} = process; + if (self.requests > 0 || stdin.emit === self.ourEmit) { + if (event === 'keypress') { // Fixes readline behavior + return; + } + + if (event === 'data' && data.includes(ASCII_ETX_CODE)) { + process.emit('SIGINT'); + } + + self.oldEmit.apply(this, [event, data, ...args]); + } else { + process.stdin.emit.apply(this, [event, data, ...args]); + } + }; + } + + start() { + this.requests++; + + if (this.requests === 1) { + this.realStart(); + } + } + + stop() { + if (this.requests <= 0) { + throw new Error('`stop` called more times than `start`'); + } + + this.requests--; + + if (this.requests === 0) { + this.realStop(); + } + } + + realStart() { + // No known way to make it work reliably on Windows + if (process.platform === 'win32') { + return; + } + + this.rl = readline.createInterface({ + input: process.stdin, + output: this.mutedStream + }); + + this.rl.on('SIGINT', () => { + if (process.listenerCount('SIGINT') === 0) { + process.emit('SIGINT'); + } else { + this.rl.close(); + process.kill(process.pid, 'SIGINT'); + } + }); + } + + realStop() { + if (process.platform === 'win32') { + return; + } + + this.rl.close(); + this.rl = undefined; + } +} + +const stdinDiscarder = new StdinDiscarder(); + class Ora { constructor(options) { if (typeof options === 'string') { @@ -45,6 +125,7 @@ class Ora { this.linesToClear = 0; this.indent = this.options.indent; this.discardStdin = this.options.discardStdin; + this.isDiscardingStdin = false; } get indent() { @@ -183,7 +264,8 @@ class Ora { } if (this.discardStdin && process.stdin.isTTY) { - this.startDiscardingStdin(); + this.isDiscardingStdin = true; + stdinDiscarder.start(); } this.render(); @@ -205,60 +287,14 @@ class Ora { cliCursor.show(this.stream); } - if (this.discardStdin && process.stdin.isTTY) { - this.stopDiscardingStdin(); + if (this.discardStdin && process.stdin.isTTY && this.isDiscardingStdin) { + stdinDiscarder.stop(); + this.isDiscardingStdin = false; } return this; } - startDiscardingStdin() { - const {stdin} = process; - - this._stdinOldRawMode = stdin.isRaw; - this._stdinOldEmit = stdin.emit; - this._stdinOldEmitOwnProperty = Object.prototype.hasOwnProperty.call(stdin, 'emit'); - - stdin.setRawMode(true); - stdin.on('data', noop); - stdin.resume(); - - const self = this; - stdin.emit = function (event, data, ...args) { - if (event === 'keypress') { // Fixes readline behavior - return; - } - - if (event === 'data' && data.includes(ASCII_ETX_CODE)) { - process.emit('SIGINT'); - } - - self._stdinOldEmit.apply(this, [event, data, ...args]); - }; - } - - stopDiscardingStdin() { - if (this._stdinOldEmit !== undefined) { - const {stdin} = process; - stdin.setRawMode(this._stdinOldRawMode); - stdin.removeListener('data', noop); - - if (stdin.listenerCount('data') === 0) { - stdin.pause(); - } - - if (this._stdinOldEmitOwnProperty) { - stdin.emit = this._stdinOldEmit; - } else { - delete stdin.emit; - } - - this._stdinOldRawMode = undefined; - this._stdinOldEmit = undefined; - this._stdinOldEmitOwnProperty = undefined; - } - } - succeed(text) { return this.stopAndPersist({symbol: logSymbols.success, text}); } diff --git a/package.json b/package.json index 1880f1e..c7c784c 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "cli-spinners": "^2.2.0", "is-interactive": "^1.0.0", "log-symbols": "^3.0.0", + "mute-stream": "0.0.8", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" }, diff --git a/readme.md b/readme.md index c42b8fe..6aa4038 100644 --- a/readme.md +++ b/readme.md @@ -124,6 +124,8 @@ Default: `true` Discard stdin input (except Ctrl+C) while running if it's TTY. This prevents the spinner from twitching on input, outputting broken lines on Enter key presses, and prevents buffering of input while the spinner is running. +This has no effect on Windows as there's no good way to implement discarding stdin properly there. + ### Instance #### .start(text?)