Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix discardStdin option #135

Merged
merged 6 commits into from Nov 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions example.js
Expand Up @@ -15,6 +15,14 @@ const spinnerDiscardingStdin = new Ora({

spinnerDiscardingStdin.start();

setTimeout(() => {
spinnerDiscardingStdin.succeed();
}, 1000);

setTimeout(() => {
spinnerDiscardingStdin.start();
}, 2000);

setTimeout(() => {
spinnerDiscardingStdin.succeed();
spinner.start();
Expand Down
2 changes: 2 additions & 0 deletions index.d.ts
Expand Up @@ -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
*/
Expand Down
140 changes: 88 additions & 52 deletions index.js
@@ -1,19 +1,99 @@
'use strict';
const readline = require('readline');
const chalk = require('chalk');
const cliCursor = require('cli-cursor');
const cliSpinners = require('cli-spinners');
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') {
Expand Down Expand Up @@ -45,6 +125,7 @@ class Ora {
this.linesToClear = 0;
this.indent = this.options.indent;
this.discardStdin = this.options.discardStdin;
this.isDiscardingStdin = false;
}

get indent() {
Expand Down Expand Up @@ -183,7 +264,8 @@ class Ora {
}

if (this.discardStdin && process.stdin.isTTY) {
this.startDiscardingStdin();
this.isDiscardingStdin = true;
stdinDiscarder.start();
}

this.render();
Expand All @@ -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});
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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"
},
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -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 <kbd>Enter</kbd> 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?)
Expand Down