-
Notifications
You must be signed in to change notification settings - Fork 30.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
readline: introduce promise-based API
- Loading branch information
Showing
9 changed files
with
1,903 additions
and
48 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); |
Oops, something went wrong.