Skip to content

Commit

Permalink
readline: introduce promise-based API
Browse files Browse the repository at this point in the history
  • Loading branch information
aduh95 committed Apr 27, 2021
1 parent 56143e4 commit 25328ea
Show file tree
Hide file tree
Showing 9 changed files with 1,903 additions and 48 deletions.
450 changes: 404 additions & 46 deletions doc/api/readline.md

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions lib/internal/readline/promises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use strict';

const {
NumberIsNaN,
Promise,
PromiseReject,
PromiseResolve,
} = primordials;

const {
codes: {
ERR_INVALID_ARG_VALUE,
ERR_INVALID_CURSOR_POS,
},
} = require('internal/errors');

const {
CSI,
} = require('internal/readline/utils');

const {
kClearToLineBeginning,
kClearToLineEnd,
kClearLine,
kClearScreenDown,
} = CSI;


/**
* Moves the cursor to the x and y coordinate on the given stream.
*/
function cursorTo(stream, x, y = undefined) {
if (NumberIsNaN(x)) return PromiseReject(new ERR_INVALID_ARG_VALUE('x', x));
if (NumberIsNaN(y)) return PromiseReject(new ERR_INVALID_ARG_VALUE('y', y));

if (stream == null || (typeof x !== 'number' && typeof y !== 'number')) {
return PromiseResolve();
}

if (typeof x !== 'number') return PromiseReject(new ERR_INVALID_CURSOR_POS());

const data = typeof y !== 'number' ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
return new Promise((done) => stream.write(data, done));
}

/**
* Moves the cursor relative to its current location.
*/
function moveCursor(stream, dx, dy) {
if (stream == null || !(dx || dy)) {
return PromiseResolve();
}

let data = '';

if (dx < 0) {
data += CSI`${-dx}D`;
} else if (dx > 0) {
data += CSI`${dx}C`;
}

if (dy < 0) {
data += CSI`${-dy}A`;
} else if (dy > 0) {
data += CSI`${dy}B`;
}

return new Promise((done) => stream.write(data, done));
}

/**
* Clears the current line the cursor is on:
* -1 for left of the cursor
* +1 for right of the cursor
* 0 for the entire line
*/
function clearLine(stream, dir) {
if (stream == null) {
return PromiseResolve();
}

const type =
dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine;
return new Promise((done) => stream.write(type, done));
}

/**
* Clears the screen from the current position of the cursor down.
*/
function clearScreenDown(stream) {
if (stream == null) {
return PromiseResolve();
}

return new Promise((done) => stream.write(kClearScreenDown, done));
}

module.exports = {
clearLine,
clearScreenDown,
cursorTo,
moveCursor,
};
4 changes: 3 additions & 1 deletion lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const {
moveCursor,
} = require('internal/readline/callbacks');
const emitKeypressEvents = require('internal/readline/emitKeypressEvents');
const promises = require('readline/promises');

const {
promisify,
Expand Down Expand Up @@ -435,5 +436,6 @@ module.exports = {
createInterface,
cursorTo,
emitKeypressEvents,
moveCursor
moveCursor,
promises,
};
57 changes: 57 additions & 0 deletions lib/readline/promises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use strict';

const {
Promise,
} = primordials;

const {
clearLine,
clearScreenDown,
cursorTo,
moveCursor,
} = require('internal/readline/promises');

const {
Interface: _Interface,
kQuestionCancel,
} = require('internal/readline/interface');

const {
AbortError,
} = require('internal/errors');

class Interface extends _Interface {
// eslint-disable-next-line no-useless-constructor
constructor(input, output, completer, terminal) {
super(input, output, completer, terminal);
}
question(query, options = {}) {
return new Promise((resolve, reject) => {
if (options.signal) {
if (options.signal.aborted) {
return reject(new AbortError());
}

options.signal.addEventListener('abort', () => {
this[kQuestionCancel]();
reject(new AbortError());
}, { once: true });
}

super.question(query, resolve);
});
}
}

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

module.exports = {
Interface,
clearLine,
clearScreenDown,
createInterface,
cursorTo,
moveCursor,
};
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
'lib/punycode.js',
'lib/querystring.js',
'lib/readline.js',
'lib/readline/promises.js',
'lib/repl.js',
'lib/stream.js',
'lib/stream/promises.js',
Expand Down Expand Up @@ -221,6 +222,7 @@
'lib/internal/readline/callbacks.js',
'lib/internal/readline/emitKeypressEvents.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
137 changes: 137 additions & 0 deletions test/parallel/test-readline-promises-csi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Flags: --expose-internals
'use strict';

const common = require('../common');
const assert = require('assert');
const readline = require('readline/promises');
const { Writable } = require('stream');
const { CSI } = require('internal/readline/utils');

class TestWritable extends Writable {
constructor() {
super();
this.data = '';
}
_write(chunk, encoding, callback) {
this.data += chunk.toString();
callback();
}
}

const writable = new TestWritable();

readline.clearScreenDown(writable).then(common.mustCall());
assert.deepStrictEqual(writable.data, CSI.kClearScreenDown);
readline.clearScreenDown(writable).then(common.mustCall());

readline.clearScreenDown(writable, null);

// Verify that clearScreenDown() does not throw on null or undefined stream.
readline.clearScreenDown(null).then(common.mustCall());
readline.clearScreenDown(undefined).then(common.mustCall());

writable.data = '';
readline.clearLine(writable, -1).then(common.mustCall());
assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);

writable.data = '';
readline.clearLine(writable, 1).then(common.mustCall());
assert.deepStrictEqual(writable.data, CSI.kClearToLineEnd);

writable.data = '';
readline.clearLine(writable, 0).then(common.mustCall());
assert.deepStrictEqual(writable.data, CSI.kClearLine);

writable.data = '';
readline.clearLine(writable, -1).then(common.mustCall());
assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);

readline.clearLine(writable, 0, null).then(common.mustCall());

// Verify that clearLine() does not throw on null or undefined stream.
readline.clearLine(null, 0).then(common.mustCall());
readline.clearLine(undefined, 0).then(common.mustCall());
readline.clearLine(null, 0).then(common.mustCall());
readline.clearLine(undefined, 0).then(common.mustCall());

// Nothing is written when moveCursor 0, 0
[
[0, 0, ''],
[1, 0, '\x1b[1C'],
[-1, 0, '\x1b[1D'],
[0, 1, '\x1b[1B'],
[0, -1, '\x1b[1A'],
[1, 1, '\x1b[1C\x1b[1B'],
[-1, 1, '\x1b[1D\x1b[1B'],
[-1, -1, '\x1b[1D\x1b[1A'],
[1, -1, '\x1b[1C\x1b[1A'],
].forEach((set) => {
writable.data = '';
readline.moveCursor(writable, set[0], set[1]).then(common.mustCall());
assert.deepStrictEqual(writable.data, set[2]);
writable.data = '';
readline.moveCursor(writable, set[0], set[1]).then(common.mustCall());
assert.deepStrictEqual(writable.data, set[2]);
});

readline.moveCursor(writable, 1, 1, null).then(common.mustCall());

// Verify that moveCursor() does not reject on null or undefined stream.
readline.moveCursor(null, 1, 1).then(common.mustCall());
readline.moveCursor(undefined, 1, 1).then(common.mustCall());
readline.moveCursor(null, 1, 1).then(common.mustCall());
readline.moveCursor(undefined, 1, 1).then(common.mustCall());

// Undefined or null as stream should not throw.
readline.cursorTo(null).then(common.mustCall());
readline.cursorTo().then(common.mustCall());
readline.cursorTo(null, 1, 1).then(common.mustCall());
readline.cursorTo(undefined, 1, 1).then(common.mustCall());

writable.data = '';
readline.cursorTo(writable, 'a').then(common.mustCall());
assert.strictEqual(writable.data, '');

writable.data = '';
readline.cursorTo(writable, 'a', 'b').then(common.mustCall());
assert.strictEqual(writable.data, '');

writable.data = '';
assert.rejects(
() => readline.cursorTo(writable, 'a', 1),
{
name: 'TypeError',
code: 'ERR_INVALID_CURSOR_POS',
message: 'Cannot set cursor row without setting its column'
}).then(common.mustCall());
assert.strictEqual(writable.data, '');

writable.data = '';
readline.cursorTo(writable, 1, 'a').then(common.mustCall());
assert.strictEqual(writable.data, '\x1b[2G');

writable.data = '';
readline.cursorTo(writable, 1).then(common.mustCall());
assert.strictEqual(writable.data, '\x1b[2G');

writable.data = '';
readline.cursorTo(writable, 1, 2).then(common.mustCall());
assert.strictEqual(writable.data, '\x1b[3;2H');

writable.data = '';
readline.cursorTo(writable, 1, 2).then(common.mustCall());
assert.strictEqual(writable.data, '\x1b[3;2H');

writable.data = '';
readline.cursorTo(writable, 1).then(common.mustCall());
assert.strictEqual(writable.data, '\x1b[2G');

// Verify that cursorTo() rejects if x or y is NaN.
assert.rejects(() => readline.cursorTo(writable, NaN),
{ code: 'ERR_INVALID_ARG_VALUE' }).then(common.mustCall());

assert.rejects(() => readline.cursorTo(writable, 1, NaN),
{ code: 'ERR_INVALID_ARG_VALUE' }).then(common.mustCall());

assert.rejects(() => readline.cursorTo(writable, NaN, NaN),
{ code: 'ERR_INVALID_ARG_VALUE' }).then(common.mustCall());
Loading

0 comments on commit 25328ea

Please sign in to comment.