Skip to content

Commit 8122d24

Browse files
committed
readline: introduce promise-based API
PR-URL: #37947Fixes: #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

8 files changed

Lines changed: 1963 additions & 52 deletions

File tree

‎doc/api/readline.md‎

Lines changed: 417 additions & 50 deletions
Large diffs are not rendered by default.

‎lib/internal/readline/promises.js‎

Lines 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+
classReadline{
22+
#stream;
23+
#todo =[];
24+
25+
constructor(stream){
26+
if(!isWritable(stream))
27+
thrownewERR_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+
returnthis;
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+
letdata='';
61+
62+
if(dx<0){
63+
data+=CSI`${-dx}D`;
64+
}elseif(dx>0){
65+
data+=CSI`${dx}C`;
66+
}
67+
68+
if(dy<0){
69+
data+=CSI`${-dy}A`;
70+
}elseif(dy>0){
71+
data+=CSI`${dy}B`;
72+
}
73+
ArrayPrototypePush(this.#todo,data);
74+
}
75+
returnthis;
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+
returnthis;
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+
returnthis;
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+
returnnewPromise((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+
returnthis;
126+
}
127+
}
128+
129+
module.exports={
130+
Readline,
131+
};

‎lib/readline.js‎

Lines 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
constemitKeypressEvents=require('internal/readline/emitKeypressEvents');
42+
constpromises=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
};

‎lib/readline/promises.js‎

Lines 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+
classInterfaceextends_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+
returnnewPromise((resolve,reject)=>{
27+
if(options.signal){
28+
if(options.signal.aborted){
29+
returnreject(newAbortError());
30+
}
31+
32+
options.signal.addEventListener('abort',()=>{
33+
this[kQuestionCancel]();
34+
reject(newAbortError());
35+
},{once: true});
36+
}
37+
38+
super.question(query,resolve);
39+
});
40+
}
41+
}
42+
43+
functioncreateInterface(input,output,completer,terminal){
44+
returnnewInterface(input,output,completer,terminal);
45+
}
46+
47+
module.exports={
48+
Interface,
49+
Readline,
50+
createInterface,
51+
};
Lines 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+
importassertfrom'assert';
6+
import{Readline}from'readline/promises';
7+
import{Writable}from'stream';
8+
9+
importutilsfrom'internal/readline/utils';
10+
const{CSI}=utils;
11+
12+
constINVALID_ARG={
13+
name: 'TypeError',
14+
code: 'ERR_INVALID_ARG_TYPE',
15+
};
16+
17+
classTestWritableextendsWritable{
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(()=>newReadline(arg),INVALID_ARG)
34+
);
35+
36+
{
37+
constwritable=newTestWritable();
38+
constreadline=newReadline(writable);
39+
40+
awaitreadline.clearScreenDown().commit();
41+
assert.deepStrictEqual(writable.data,CSI.kClearScreenDown);
42+
awaitreadline.clearScreenDown().commit();
43+
44+
writable.data='';
45+
awaitreadline.clearScreenDown().rollback();
46+
assert.deepStrictEqual(writable.data,'');
47+
48+
writable.data='';
49+
awaitreadline.clearLine(-1).commit();
50+
assert.deepStrictEqual(writable.data,CSI.kClearToLineBeginning);
51+
52+
writable.data='';
53+
awaitreadline.clearLine(1).commit();
54+
assert.deepStrictEqual(writable.data,CSI.kClearToLineEnd);
55+
56+
writable.data='';
57+
awaitreadline.clearLine(0).commit();
58+
assert.deepStrictEqual(writable.data,CSI.kClearLine);
59+
60+
writable.data='';
61+
awaitreadline.clearLine(-1).commit();
62+
assert.deepStrictEqual(writable.data,CSI.kClearToLineBeginning);
63+
64+
awaitreadline.clearLine(0,null).commit();
65+
66+
// Nothing is written when moveCursor 0, 0
67+
for(constsetof
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+
awaitreadline.moveCursor(set[0],set[1]).commit();
81+
assert.deepStrictEqual(writable.data,set[2]);
82+
writable.data='';
83+
awaitreadline.moveCursor(set[0],set[1]).commit();
84+
assert.deepStrictEqual(writable.data,set[2]);
85+
}
86+
87+
88+
awaitreadline.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+
awaitreadline.cursorTo(1).commit();
116+
assert.strictEqual(writable.data,'\x1b[2G');
117+
118+
writable.data='';
119+
awaitreadline.cursorTo(1,2).commit();
120+
assert.strictEqual(writable.data,'\x1b[3;2H');
121+
122+
writable.data='';
123+
awaitreadline.cursorTo(1,2).commit();
124+
assert.strictEqual(writable.data,'\x1b[3;2H');
125+
126+
writable.data='';
127+
awaitreadline.cursorTo(1).cursorTo(1,2).commit();
128+
assert.strictEqual(writable.data,'\x1b[2G\x1b[3;2H');
129+
130+
writable.data='';
131+
awaitreadline.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+
consterror=newError();
157+
constwritable=newclassextendsWritable{
158+
_write(){throwerror;}
159+
}();
160+
constreadline=newReadline(writable);
161+
162+
awaitassert.rejects(readline.cursorTo(1).commit(),error);
163+
}

0 commit comments

Comments
 (0)