Skip to content

Commit

Permalink
readline: add readline.promises
Browse files Browse the repository at this point in the history
This commit exposes a Promise based version of the readline API.
  • Loading branch information
cjihrig committed Jul 10, 2019
1 parent 93d3cf8 commit 11c8737
Show file tree
Hide file tree
Showing 8 changed files with 2,086 additions and 76 deletions.
472 changes: 472 additions & 0 deletions doc/api/readline.md

Large diffs are not rendered by default.

164 changes: 88 additions & 76 deletions lib/internal/readline/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ function Interface(input, output, completer, terminal) {
if (!(this instanceof Interface))
return new Interface(input, output, completer, terminal);

if (StringDecoder === undefined)
StringDecoder = require('string_decoder').StringDecoder;

EventEmitter.call(this);

const opts = normalizeInterfaceArguments(input, output, completer, terminal);
Expand All @@ -47,78 +44,7 @@ function Interface(input, output, completer, terminal) {
};
}

this.input = opts.input;
this.output = opts.output;
this.completer = opts.completer;
this.terminal = opts.terminal;
this.historySize = opts.historySize;
this.setPrompt(opts.prompt);
this.crlfDelay = opts.crlfDelay;
this.removeHistoryDuplicates = opts.removeHistoryDuplicates;
this.escapeCodeTimeout = opts.escapeCodeTimeout;
this.isCompletionEnabled = true;
this._sawReturnAt = 0;
this._sawKeyPress = false;
this._previousKey = null;
this[kLineObjectStream] = undefined;

if (process.env.TERM === 'dumb')
this._ttyWrite = ttyWriteDumb.bind(this);

input = this.input;

if (!this.terminal) {
const ondata = onData.bind(this);
const onend = onEnd.bind(this);

function onInterfaceCloseWithoutTerminal() {
input.removeListener('data', ondata);
input.removeListener('end', onend);
}

input.on('data', ondata);
input.on('end', onend);
this.once('close', onInterfaceCloseWithoutTerminal);
this._decoder = new StringDecoder('utf8');
} else {
const onkeypress = onKeyPress.bind(this);
const ontermend = onTermEnd.bind(this);
const output = this.output;
const onresize = output == null ? null : onResize.bind(this);

function onInterfaceCloseWithTerminal() {
input.removeListener('keypress', onkeypress);
input.removeListener('end', ontermend);

if (onresize !== null)
output.removeListener('resize', onresize);
}

emitKeypressEvents(input, this);

// `input` usually refers to stdin.
input.on('keypress', onkeypress);
input.on('end', ontermend);

// The current line.
this.line = '';

this._setRawMode(true);
this.terminal = true;

// The cursor position on the line.
this.cursor = 0;

this.history = [];
this.historyIndex = -1;

if (onresize !== null)
output.on('resize', onresize);

this.once('close', onInterfaceCloseWithTerminal);
}

input.resume();
initializeInterface(this, opts);
}

Object.setPrototypeOf(Interface.prototype, EventEmitter.prototype);
Expand Down Expand Up @@ -1105,4 +1031,90 @@ function normalizeInterfaceArguments(input, output, completer, terminal) {
}


module.exports = { createInterface, Interface };
function initializeInterface(iface, options) {
iface.input = options.input;
iface.output = options.output;
iface.completer = options.completer;
iface.terminal = options.terminal;
iface.historySize = options.historySize;
iface.setPrompt(options.prompt);
iface.crlfDelay = options.crlfDelay;
iface.removeHistoryDuplicates = options.removeHistoryDuplicates;
iface.escapeCodeTimeout = options.escapeCodeTimeout;
iface.isCompletionEnabled = true;
iface._sawReturnAt = 0;
iface._sawKeyPress = false;
iface._previousKey = null;
iface[kLineObjectStream] = undefined;

if (process.env.TERM === 'dumb')
iface._ttyWrite = ttyWriteDumb.bind(iface);

const input = iface.input;

if (!iface.terminal) {
const ondata = onData.bind(iface);
const onend = onEnd.bind(iface);

function onInterfaceCloseWithoutTerminal() {
input.removeListener('data', ondata);
input.removeListener('end', onend);
}

input.on('data', ondata);
input.on('end', onend);
iface.once('close', onInterfaceCloseWithoutTerminal);

if (StringDecoder === undefined)
StringDecoder = require('string_decoder').StringDecoder;

iface._decoder = new StringDecoder('utf8');
} else {
const onkeypress = onKeyPress.bind(iface);
const ontermend = onTermEnd.bind(iface);
const output = iface.output;
const onresize = output == null ? null : onResize.bind(iface);

function onInterfaceCloseWithTerminal() {
input.removeListener('keypress', onkeypress);
input.removeListener('end', ontermend);

if (onresize !== null)
output.removeListener('resize', onresize);
}

emitKeypressEvents(input, iface);

// `input` usually refers to stdin.
input.on('keypress', onkeypress);
input.on('end', ontermend);

// The current line.
iface.line = '';

iface._setRawMode(true);
iface.terminal = true;

// The cursor position on the line.
iface.cursor = 0;

iface.history = [];
iface.historyIndex = -1;

if (onresize !== null)
output.on('resize', onresize);

iface.once('close', onInterfaceCloseWithTerminal);
}

input.resume();
}


module.exports = {
createInterface,
initializeInterface,
onTabComplete,
normalizeInterfaceArguments,
Interface
};
72 changes: 72 additions & 0 deletions lib/internal/readline/promises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';
const { Object } = primordials;
const EventEmitter = require('events');
const {
Interface: CallbackInterface,
initializeInterface,
onTabComplete,
normalizeInterfaceArguments
} = require('internal/readline/interface');


class Interface extends EventEmitter {
constructor(input, output, completer, terminal) {
super();
const opts = normalizeInterfaceArguments(
input, output, completer, terminal);
initializeInterface(this, opts);
}

question(query) {
let resolve;
const promise = new Promise((res) => {
resolve = res;
});

if (!this._questionCallback) {
this._oldPrompt = this._prompt;
this.setPrompt(query);
this._questionCallback = resolve;
}

this.prompt();
return promise;
}

async _tabComplete(lastKeypressWasTab) {
this.pause();

try {
const line = this.line.slice(0, this.cursor);
const results = await this.completer(line);
onTabComplete(null, results, this, lastKeypressWasTab);
} catch (err) {
onTabComplete(err, null, this, lastKeypressWasTab);
}
}
}

// Copy the rest of the callback interface over to this interface.
Object.keys(CallbackInterface.prototype).forEach((keyName) => {
if (Interface.prototype[keyName] === undefined)
Interface.prototype[keyName] = CallbackInterface.prototype[keyName];
});

Object.defineProperty(Interface.prototype, 'columns', {
configurable: true,
enumerable: true,
get() {
return this.output && this.output.columns ? this.output.columns : Infinity;
}
});

Interface.prototype[Symbol.asyncIterator] =
CallbackInterface.prototype[Symbol.asyncIterator];


function createInterface(input, output, completer, terminal) {
return new Interface(input, output, completer, terminal);
}


module.exports = { createInterface, Interface };
32 changes: 32 additions & 0 deletions lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

'use strict';

const { Object } = primordials;
const { createInterface, Interface } = require('internal/readline/interface');
const {
clearLine,
Expand All @@ -45,3 +46,34 @@ module.exports = {
emitKeypressEvents,
moveCursor
};

let promises;

Object.defineProperties(module.exports, {
promises: {
configurable: true,
enumerable: false,
get() {
if (promises === undefined) {
const {
Interface,
createInterface
} = require('internal/readline/promises');

promises = {
Interface,
clearLine,
clearScreenDown,
createInterface,
cursorTo,
emitKeypressEvents,
moveCursor
};

process.emitWarning('The readline.promises API is experimental',
'ExperimentalWarning');
}
return promises;
}
}
});
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
'lib/internal/process/task_queues.js',
'lib/internal/querystring.js',
'lib/internal/readline/interface.js',
'lib/internal/readline/promises.js',
'lib/internal/readline/utils.js',
'lib/internal/repl.js',
'lib/internal/repl/await.js',
Expand Down
Loading

0 comments on commit 11c8737

Please sign in to comment.