Skip to content

Commit 25328ea

Browse files
committed
readline: introduce promise-based API
1 parent 56143e4 commit 25328ea

File tree

9 files changed

+1903
-48
lines changed

9 files changed

+1903
-48
lines changed

doc/api/readline.md

Lines changed: 404 additions & 46 deletions
Large diffs are not rendered by default.

lib/internal/readline/promises.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use strict';
2+
3+
const {
4+
NumberIsNaN,
5+
Promise,
6+
PromiseReject,
7+
PromiseResolve,
8+
} = primordials;
9+
10+
const {
11+
codes: {
12+
ERR_INVALID_ARG_VALUE,
13+
ERR_INVALID_CURSOR_POS,
14+
},
15+
} = require('internal/errors');
16+
17+
const {
18+
CSI,
19+
} = require('internal/readline/utils');
20+
21+
const {
22+
kClearToLineBeginning,
23+
kClearToLineEnd,
24+
kClearLine,
25+
kClearScreenDown,
26+
} = CSI;
27+
28+
29+
/**
30+
* Moves the cursor to the x and y coordinate on the given stream.
31+
*/
32+
function cursorTo(stream, x, y = undefined) {
33+
if (NumberIsNaN(x)) return PromiseReject(new ERR_INVALID_ARG_VALUE('x', x));
34+
if (NumberIsNaN(y)) return PromiseReject(new ERR_INVALID_ARG_VALUE('y', y));
35+
36+
if (stream == null || (typeof x !== 'number' && typeof y !== 'number')) {
37+
return PromiseResolve();
38+
}
39+
40+
if (typeof x !== 'number') return PromiseReject(new ERR_INVALID_CURSOR_POS());
41+
42+
const data = typeof y !== 'number' ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
43+
return new Promise((done) => stream.write(data, done));
44+
}
45+
46+
/**
47+
* Moves the cursor relative to its current location.
48+
*/
49+
function moveCursor(stream, dx, dy) {
50+
if (stream == null || !(dx || dy)) {
51+
return PromiseResolve();
52+
}
53+
54+
let data = '';
55+
56+
if (dx < 0) {
57+
data += CSI`${-dx}D`;
58+
} else if (dx > 0) {
59+
data += CSI`${dx}C`;
60+
}
61+
62+
if (dy < 0) {
63+
data += CSI`${-dy}A`;
64+
} else if (dy > 0) {
65+
data += CSI`${dy}B`;
66+
}
67+
68+
return new Promise((done) => stream.write(data, done));
69+
}
70+
71+
/**
72+
* Clears the current line the cursor is on:
73+
* -1 for left of the cursor
74+
* +1 for right of the cursor
75+
* 0 for the entire line
76+
*/
77+
function clearLine(stream, dir) {
78+
if (stream == null) {
79+
return PromiseResolve();
80+
}
81+
82+
const type =
83+
dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine;
84+
return new Promise((done) => stream.write(type, done));
85+
}
86+
87+
/**
88+
* Clears the screen from the current position of the cursor down.
89+
*/
90+
function clearScreenDown(stream) {
91+
if (stream == null) {
92+
return PromiseResolve();
93+
}
94+
95+
return new Promise((done) => stream.write(kClearScreenDown, done));
96+
}
97+
98+
module.exports = {
99+
clearLine,
100+
clearScreenDown,
101+
cursorTo,
102+
moveCursor,
103+
};

lib/readline.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const {
4545
moveCursor,
4646
} = require('internal/readline/callbacks');
4747
const emitKeypressEvents = require('internal/readline/emitKeypressEvents');
48+
const promises = require('readline/promises');
4849

4950
const {
5051
promisify,
@@ -435,5 +436,6 @@ module.exports = {
435436
createInterface,
436437
cursorTo,
437438
emitKeypressEvents,
438-
moveCursor
439+
moveCursor,
440+
promises,
439441
};

lib/readline/promises.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
const {
4+
Promise,
5+
} = primordials;
6+
7+
const {
8+
clearLine,
9+
clearScreenDown,
10+
cursorTo,
11+
moveCursor,
12+
} = require('internal/readline/promises');
13+
14+
const {
15+
Interface: _Interface,
16+
kQuestionCancel,
17+
} = require('internal/readline/interface');
18+
19+
const {
20+
AbortError,
21+
} = require('internal/errors');
22+
23+
class Interface extends _Interface {
24+
// eslint-disable-next-line no-useless-constructor
25+
constructor(input, output, completer, terminal) {
26+
super(input, output, completer, terminal);
27+
}
28+
question(query, options = {}) {
29+
return new Promise((resolve, reject) => {
30+
if (options.signal) {
31+
if (options.signal.aborted) {
32+
return reject(new AbortError());
33+
}
34+
35+
options.signal.addEventListener('abort', () => {
36+
this[kQuestionCancel]();
37+
reject(new AbortError());
38+
}, { once: true });
39+
}
40+
41+
super.question(query, resolve);
42+
});
43+
}
44+
}
45+
46+
function createInterface(input, output, completer, terminal) {
47+
return new Interface(input, output, completer, terminal);
48+
}
49+
50+
module.exports = {
51+
Interface,
52+
clearLine,
53+
clearScreenDown,
54+
createInterface,
55+
cursorTo,
56+
moveCursor,
57+
};

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
'lib/punycode.js',
7878
'lib/querystring.js',
7979
'lib/readline.js',
80+
'lib/readline/promises.js',
8081
'lib/repl.js',
8182
'lib/stream.js',
8283
'lib/stream/promises.js',
@@ -221,6 +222,7 @@
221222
'lib/internal/readline/callbacks.js',
222223
'lib/internal/readline/emitKeypressEvents.js',
223224
'lib/internal/readline/interface.js',
225+
'lib/internal/readline/promises.js',
224226
'lib/internal/readline/utils.js',
225227
'lib/internal/repl.js',
226228
'lib/internal/repl/await.js',
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
const readline = require('readline/promises');
7+
const { Writable } = require('stream');
8+
const { CSI } = require('internal/readline/utils');
9+
10+
class TestWritable extends Writable {
11+
constructor() {
12+
super();
13+
this.data = '';
14+
}
15+
_write(chunk, encoding, callback) {
16+
this.data += chunk.toString();
17+
callback();
18+
}
19+
}
20+
21+
const writable = new TestWritable();
22+
23+
readline.clearScreenDown(writable).then(common.mustCall());
24+
assert.deepStrictEqual(writable.data, CSI.kClearScreenDown);
25+
readline.clearScreenDown(writable).then(common.mustCall());
26+
27+
readline.clearScreenDown(writable, null);
28+
29+
// Verify that clearScreenDown() does not throw on null or undefined stream.
30+
readline.clearScreenDown(null).then(common.mustCall());
31+
readline.clearScreenDown(undefined).then(common.mustCall());
32+
33+
writable.data = '';
34+
readline.clearLine(writable, -1).then(common.mustCall());
35+
assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);
36+
37+
writable.data = '';
38+
readline.clearLine(writable, 1).then(common.mustCall());
39+
assert.deepStrictEqual(writable.data, CSI.kClearToLineEnd);
40+
41+
writable.data = '';
42+
readline.clearLine(writable, 0).then(common.mustCall());
43+
assert.deepStrictEqual(writable.data, CSI.kClearLine);
44+
45+
writable.data = '';
46+
readline.clearLine(writable, -1).then(common.mustCall());
47+
assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);
48+
49+
readline.clearLine(writable, 0, null).then(common.mustCall());
50+
51+
// Verify that clearLine() does not throw on null or undefined stream.
52+
readline.clearLine(null, 0).then(common.mustCall());
53+
readline.clearLine(undefined, 0).then(common.mustCall());
54+
readline.clearLine(null, 0).then(common.mustCall());
55+
readline.clearLine(undefined, 0).then(common.mustCall());
56+
57+
// Nothing is written when moveCursor 0, 0
58+
[
59+
[0, 0, ''],
60+
[1, 0, '\x1b[1C'],
61+
[-1, 0, '\x1b[1D'],
62+
[0, 1, '\x1b[1B'],
63+
[0, -1, '\x1b[1A'],
64+
[1, 1, '\x1b[1C\x1b[1B'],
65+
[-1, 1, '\x1b[1D\x1b[1B'],
66+
[-1, -1, '\x1b[1D\x1b[1A'],
67+
[1, -1, '\x1b[1C\x1b[1A'],
68+
].forEach((set) => {
69+
writable.data = '';
70+
readline.moveCursor(writable, set[0], set[1]).then(common.mustCall());
71+
assert.deepStrictEqual(writable.data, set[2]);
72+
writable.data = '';
73+
readline.moveCursor(writable, set[0], set[1]).then(common.mustCall());
74+
assert.deepStrictEqual(writable.data, set[2]);
75+
});
76+
77+
readline.moveCursor(writable, 1, 1, null).then(common.mustCall());
78+
79+
// Verify that moveCursor() does not reject on null or undefined stream.
80+
readline.moveCursor(null, 1, 1).then(common.mustCall());
81+
readline.moveCursor(undefined, 1, 1).then(common.mustCall());
82+
readline.moveCursor(null, 1, 1).then(common.mustCall());
83+
readline.moveCursor(undefined, 1, 1).then(common.mustCall());
84+
85+
// Undefined or null as stream should not throw.
86+
readline.cursorTo(null).then(common.mustCall());
87+
readline.cursorTo().then(common.mustCall());
88+
readline.cursorTo(null, 1, 1).then(common.mustCall());
89+
readline.cursorTo(undefined, 1, 1).then(common.mustCall());
90+
91+
writable.data = '';
92+
readline.cursorTo(writable, 'a').then(common.mustCall());
93+
assert.strictEqual(writable.data, '');
94+
95+
writable.data = '';
96+
readline.cursorTo(writable, 'a', 'b').then(common.mustCall());
97+
assert.strictEqual(writable.data, '');
98+
99+
writable.data = '';
100+
assert.rejects(
101+
() => readline.cursorTo(writable, 'a', 1),
102+
{
103+
name: 'TypeError',
104+
code: 'ERR_INVALID_CURSOR_POS',
105+
message: 'Cannot set cursor row without setting its column'
106+
}).then(common.mustCall());
107+
assert.strictEqual(writable.data, '');
108+
109+
writable.data = '';
110+
readline.cursorTo(writable, 1, 'a').then(common.mustCall());
111+
assert.strictEqual(writable.data, '\x1b[2G');
112+
113+
writable.data = '';
114+
readline.cursorTo(writable, 1).then(common.mustCall());
115+
assert.strictEqual(writable.data, '\x1b[2G');
116+
117+
writable.data = '';
118+
readline.cursorTo(writable, 1, 2).then(common.mustCall());
119+
assert.strictEqual(writable.data, '\x1b[3;2H');
120+
121+
writable.data = '';
122+
readline.cursorTo(writable, 1, 2).then(common.mustCall());
123+
assert.strictEqual(writable.data, '\x1b[3;2H');
124+
125+
writable.data = '';
126+
readline.cursorTo(writable, 1).then(common.mustCall());
127+
assert.strictEqual(writable.data, '\x1b[2G');
128+
129+
// Verify that cursorTo() rejects if x or y is NaN.
130+
assert.rejects(() => readline.cursorTo(writable, NaN),
131+
{ code: 'ERR_INVALID_ARG_VALUE' }).then(common.mustCall());
132+
133+
assert.rejects(() => readline.cursorTo(writable, 1, NaN),
134+
{ code: 'ERR_INVALID_ARG_VALUE' }).then(common.mustCall());
135+
136+
assert.rejects(() => readline.cursorTo(writable, NaN, NaN),
137+
{ code: 'ERR_INVALID_ARG_VALUE' }).then(common.mustCall());

0 commit comments

Comments
 (0)