GeezerVison is showing github.com — readline: introduce promise-based API · nodejs/node@8122d24
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file 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 Copy file name to clipboard +131Lines changed: 131 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 Copy file name to clipboardExpand all lines: lib/readline.js +3-1Lines changed: 3 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -39,6 +39,7 @@ const { | 39 | 39 | moveCursor, | 40 | 40 | } = require('internal/readline/callbacks'); | 41 | 41 | const emitKeypressEvents = require('internal/readline/emitKeypressEvents'); | | 42 | +const promises = require('readline/promises'); | 42 | 43 |
| 43 | 44 | const { | 44 | 45 | AbortError, | @@ -462,5 +463,6 @@ module.exports = { | 462 | 463 | createInterface, | 463 | 464 | cursorTo, | 464 | 465 | emitKeypressEvents, | 465 | | - moveCursor | | 466 | + moveCursor, | | 467 | + promises, | 466 | 468 | }; |
Collapse file Copy file name to clipboard +51Lines changed: 51 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 Copy file name to clipboard +163Lines changed: 163 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
|