Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8122d24

Browse filesBrowse files
committed
readline: introduce promise-based API
PR-URL: #37947 Fixes: #37287 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Robert Nagy <ronagy@icloud.com>
1 parent 592d1c3 commit 8122d24
Copy full SHA for 8122d24

File tree

Expand file treeCollapse file tree

8 files changed

+1963
-52
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

8 files changed

+1963
-52
lines changed
Open diff view settings
Collapse file

‎doc/api/readline.md‎

Copy file name to clipboardExpand all lines: doc/api/readline.md
+417-50Lines changed: 417 additions & 50 deletions
  • Display the source diff
  • Display the rich diff
Large diffs are not rendered by default.
Collapse file

‎lib/internal/readline/promises.js‎

Copy file name to clipboard
+131Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
'use strict';
2+
3+
const {
4+
ArrayPrototypeJoin,
5+
ArrayPrototypePush,
6+
Promise,
7+
} = primordials;
8+
9+
const { CSI } = require('internal/readline/utils');
10+
const { validateInteger } = require('internal/validators');
11+
const { isWritable } = require('internal/streams/utils');
12+
const { codes: { ERR_INVALID_ARG_TYPE } } = require('internal/errors');
13+
14+
const {
15+
kClearToLineBeginning,
16+
kClearToLineEnd,
17+
kClearLine,
18+
kClearScreenDown,
19+
} = CSI;
20+
21+
class Readline {
22+
#stream;
23+
#todo = [];
24+
25+
constructor(stream) {
26+
if (!isWritable(stream))
27+
throw new ERR_INVALID_ARG_TYPE('stream', 'Writable', stream);
28+
this.#stream = stream;
29+
}
30+
31+
/**
32+
* Moves the cursor to the x and y coordinate on the given stream.
33+
* @param {integer} x
34+
* @param {integer} [y]
35+
* @returns {Readline} this
36+
*/
37+
cursorTo(x, y = undefined) {
38+
validateInteger(x, 'x');
39+
if (y != null) validateInteger(y, 'y');
40+
41+
ArrayPrototypePush(
42+
this.#todo,
43+
y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`
44+
);
45+
46+
return this;
47+
}
48+
49+
/**
50+
* Moves the cursor relative to its current location.
51+
* @param {integer} dx
52+
* @param {integer} dy
53+
* @returns {Readline} this
54+
*/
55+
moveCursor(dx, dy) {
56+
if (dx || dy) {
57+
validateInteger(dx, 'dx');
58+
validateInteger(dy, 'dy');
59+
60+
let data = '';
61+
62+
if (dx < 0) {
63+
data += CSI`${-dx}D`;
64+
} else if (dx > 0) {
65+
data += CSI`${dx}C`;
66+
}
67+
68+
if (dy < 0) {
69+
data += CSI`${-dy}A`;
70+
} else if (dy > 0) {
71+
data += CSI`${dy}B`;
72+
}
73+
ArrayPrototypePush(this.#todo, data);
74+
}
75+
return this;
76+
}
77+
78+
/**
79+
* Clears the current line the cursor is on.
80+
* @param {-1|0|1} dir Direction to clear:
81+
* -1 for left of the cursor
82+
* +1 for right of the cursor
83+
* 0 for the entire line
84+
* @returns {Readline} this
85+
*/
86+
clearLine(dir) {
87+
validateInteger(dir, 'dir', -1, 1);
88+
89+
ArrayPrototypePush(
90+
this.#todo,
91+
dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine
92+
);
93+
return this;
94+
}
95+
96+
/**
97+
* Clears the screen from the current position of the cursor down.
98+
* @returns {Readline} this
99+
*/
100+
clearScreenDown() {
101+
ArrayPrototypePush(this.#todo, kClearScreenDown);
102+
return this;
103+
}
104+
105+
/**
106+
* Sends all the pending actions to the associated `stream` and clears the
107+
* internal list of pending actions.
108+
* @returns {Promise<void>} Resolves when all pending actions have been
109+
* flushed to the associated `stream`.
110+
*/
111+
commit() {
112+
return new Promise((resolve) => {
113+
this.#stream.write(ArrayPrototypeJoin(this.#todo, ''), resolve);
114+
this.#todo = [];
115+
});
116+
}
117+
118+
/**
119+
* Clears the internal list of pending actions without sending it to the
120+
* associated `stream`.
121+
* @returns {Readline} this
122+
*/
123+
rollback() {
124+
this.#todo = [];
125+
return this;
126+
}
127+
}
128+
129+
module.exports = {
130+
Readline,
131+
};
Collapse file

‎lib/readline.js‎

Copy file name to clipboardExpand all lines: lib/readline.js
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const {
3939
moveCursor,
4040
} = require('internal/readline/callbacks');
4141
const emitKeypressEvents = require('internal/readline/emitKeypressEvents');
42+
const promises = require('readline/promises');
4243

4344
const {
4445
AbortError,
@@ -462,5 +463,6 @@ module.exports = {
462463
createInterface,
463464
cursorTo,
464465
emitKeypressEvents,
465-
moveCursor
466+
moveCursor,
467+
promises,
466468
};
Collapse file

‎lib/readline/promises.js‎

Copy file name to clipboard
+51Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
3+
const {
4+
Promise,
5+
} = primordials;
6+
7+
const {
8+
Readline,
9+
} = require('internal/readline/promises');
10+
11+
const {
12+
Interface: _Interface,
13+
kQuestionCancel,
14+
} = require('internal/readline/interface');
15+
16+
const {
17+
AbortError,
18+
} = require('internal/errors');
19+
20+
class Interface extends _Interface {
21+
// eslint-disable-next-line no-useless-constructor
22+
constructor(input, output, completer, terminal) {
23+
super(input, output, completer, terminal);
24+
}
25+
question(query, options = {}) {
26+
return new Promise((resolve, reject) => {
27+
if (options.signal) {
28+
if (options.signal.aborted) {
29+
return reject(new AbortError());
30+
}
31+
32+
options.signal.addEventListener('abort', () => {
33+
this[kQuestionCancel]();
34+
reject(new AbortError());
35+
}, { once: true });
36+
}
37+
38+
super.question(query, resolve);
39+
});
40+
}
41+
}
42+
43+
function createInterface(input, output, completer, terminal) {
44+
return new Interface(input, output, completer, terminal);
45+
}
46+
47+
module.exports = {
48+
Interface,
49+
Readline,
50+
createInterface,
51+
};
Collapse file
+163Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Flags: --expose-internals
2+
3+
4+
import '../common/index.mjs';
5+
import assert from 'assert';
6+
import { Readline } from 'readline/promises';
7+
import { Writable } from 'stream';
8+
9+
import utils from 'internal/readline/utils';
10+
const { CSI } = utils;
11+
12+
const INVALID_ARG = {
13+
name: 'TypeError',
14+
code: 'ERR_INVALID_ARG_TYPE',
15+
};
16+
17+
class TestWritable extends Writable {
18+
data = '';
19+
_write(chunk, encoding, callback) {
20+
this.data += chunk.toString();
21+
callback();
22+
}
23+
}
24+
25+
[
26+
undefined, null,
27+
0, 1, 1n, 1.1, NaN, Infinity,
28+
true, false,
29+
Symbol(),
30+
'', '1',
31+
[], {}, () => {},
32+
].forEach((arg) =>
33+
assert.throws(() => new Readline(arg), INVALID_ARG)
34+
);
35+
36+
{
37+
const writable = new TestWritable();
38+
const readline = new Readline(writable);
39+
40+
await readline.clearScreenDown().commit();
41+
assert.deepStrictEqual(writable.data, CSI.kClearScreenDown);
42+
await readline.clearScreenDown().commit();
43+
44+
writable.data = '';
45+
await readline.clearScreenDown().rollback();
46+
assert.deepStrictEqual(writable.data, '');
47+
48+
writable.data = '';
49+
await readline.clearLine(-1).commit();
50+
assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);
51+
52+
writable.data = '';
53+
await readline.clearLine(1).commit();
54+
assert.deepStrictEqual(writable.data, CSI.kClearToLineEnd);
55+
56+
writable.data = '';
57+
await readline.clearLine(0).commit();
58+
assert.deepStrictEqual(writable.data, CSI.kClearLine);
59+
60+
writable.data = '';
61+
await readline.clearLine(-1).commit();
62+
assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);
63+
64+
await readline.clearLine(0, null).commit();
65+
66+
// Nothing is written when moveCursor 0, 0
67+
for (const set of
68+
[
69+
[0, 0, ''],
70+
[1, 0, '\x1b[1C'],
71+
[-1, 0, '\x1b[1D'],
72+
[0, 1, '\x1b[1B'],
73+
[0, -1, '\x1b[1A'],
74+
[1, 1, '\x1b[1C\x1b[1B'],
75+
[-1, 1, '\x1b[1D\x1b[1B'],
76+
[-1, -1, '\x1b[1D\x1b[1A'],
77+
[1, -1, '\x1b[1C\x1b[1A'],
78+
]) {
79+
writable.data = '';
80+
await readline.moveCursor(set[0], set[1]).commit();
81+
assert.deepStrictEqual(writable.data, set[2]);
82+
writable.data = '';
83+
await readline.moveCursor(set[0], set[1]).commit();
84+
assert.deepStrictEqual(writable.data, set[2]);
85+
}
86+
87+
88+
await readline.moveCursor(1, 1, null).commit();
89+
90+
writable.data = '';
91+
[
92+
undefined, null,
93+
true, false,
94+
Symbol(),
95+
'', '1',
96+
[], {}, () => {},
97+
].forEach((arg) =>
98+
assert.throws(() => readline.cursorTo(arg), INVALID_ARG)
99+
);
100+
assert.strictEqual(writable.data, '');
101+
102+
writable.data = '';
103+
assert.throws(() => readline.cursorTo('a', 'b'), INVALID_ARG);
104+
assert.strictEqual(writable.data, '');
105+
106+
writable.data = '';
107+
assert.throws(() => readline.cursorTo('a', 1), INVALID_ARG);
108+
assert.strictEqual(writable.data, '');
109+
110+
writable.data = '';
111+
assert.throws(() => readline.cursorTo(1, 'a'), INVALID_ARG);
112+
assert.strictEqual(writable.data, '');
113+
114+
writable.data = '';
115+
await readline.cursorTo(1).commit();
116+
assert.strictEqual(writable.data, '\x1b[2G');
117+
118+
writable.data = '';
119+
await readline.cursorTo(1, 2).commit();
120+
assert.strictEqual(writable.data, '\x1b[3;2H');
121+
122+
writable.data = '';
123+
await readline.cursorTo(1, 2).commit();
124+
assert.strictEqual(writable.data, '\x1b[3;2H');
125+
126+
writable.data = '';
127+
await readline.cursorTo(1).cursorTo(1, 2).commit();
128+
assert.strictEqual(writable.data, '\x1b[2G\x1b[3;2H');
129+
130+
writable.data = '';
131+
await readline.cursorTo(1).commit();
132+
assert.strictEqual(writable.data, '\x1b[2G');
133+
134+
// Verify that cursorTo() rejects if x or y is NaN.
135+
[1.1, NaN, Infinity].forEach((arg) => {
136+
assert.throws(() => readline.cursorTo(arg), {
137+
code: 'ERR_OUT_OF_RANGE',
138+
name: 'RangeError',
139+
});
140+
});
141+
142+
[1.1, NaN, Infinity].forEach((arg) => {
143+
assert.throws(() => readline.cursorTo(1, arg), {
144+
code: 'ERR_OUT_OF_RANGE',
145+
name: 'RangeError',
146+
});
147+
});
148+
149+
assert.throws(() => readline.cursorTo(NaN, NaN), {
150+
code: 'ERR_OUT_OF_RANGE',
151+
name: 'RangeError',
152+
});
153+
}
154+
155+
{
156+
const error = new Error();
157+
const writable = new class extends Writable {
158+
_write() { throw error; }
159+
}();
160+
const readline = new Readline(writable);
161+
162+
await assert.rejects(readline.cursorTo(1).commit(), error);
163+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.