From 8bf457054a1437d387b8c3588e67e16a76d6dafb Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 27 Apr 2021 16:10:15 +0200 Subject: [PATCH] readline: move utilities to internal modules --- lib/internal/console/constructor.js | 5 +- lib/internal/readline/callbacks.js | 132 +++++++++++++ lib/internal/readline/emitKeypressEvents.js | 96 ++++++++++ lib/internal/repl/utils.js | 2 +- lib/readline.js | 202 +------------------- node.gyp | 2 + 6 files changed, 243 insertions(+), 196 deletions(-) create mode 100644 lib/internal/readline/callbacks.js create mode 100644 lib/internal/readline/emitKeypressEvents.js diff --git a/lib/internal/console/constructor.js b/lib/internal/console/constructor.js index c3716a8acda8b5..22b84e5e42b956 100644 --- a/lib/internal/console/constructor.js +++ b/lib/internal/console/constructor.js @@ -429,7 +429,10 @@ const consoleMethods = { if (this._stdout.isTTY && process.env.TERM !== 'dumb') { // The require is here intentionally to avoid readline being // required too early when console is first loaded. - const { cursorTo, clearScreenDown } = require('readline'); + const { + cursorTo, + clearScreenDown, + } = require('internal/readline/callbacks'); cursorTo(this._stdout, 0, 0); clearScreenDown(this._stdout); } diff --git a/lib/internal/readline/callbacks.js b/lib/internal/readline/callbacks.js new file mode 100644 index 00000000000000..ae7cf0c07dda0b --- /dev/null +++ b/lib/internal/readline/callbacks.js @@ -0,0 +1,132 @@ +'use strict'; + +const { + NumberIsNaN, +} = primordials; + +const { + codes: { + ERR_INVALID_ARG_VALUE, + ERR_INVALID_CURSOR_POS, + }, +} = require('internal/errors'); + +const { + validateCallback, +} = require('internal/validators'); +const { + CSI, +} = require('internal/readline/utils'); + +const { + kClearLine, + kClearScreenDown, + kClearToLineBeginning, + kClearToLineEnd, +} = CSI; + + +/** + * moves the cursor to the x and y coordinate on the given stream + */ + +function cursorTo(stream, x, y, callback) { + if (callback !== undefined) { + validateCallback(callback); + } + + if (typeof y === 'function') { + callback = y; + y = undefined; + } + + if (NumberIsNaN(x)) throw new ERR_INVALID_ARG_VALUE('x', x); + if (NumberIsNaN(y)) throw new ERR_INVALID_ARG_VALUE('y', y); + + if (stream == null || (typeof x !== 'number' && typeof y !== 'number')) { + if (typeof callback === 'function') process.nextTick(callback, null); + return true; + } + + if (typeof x !== 'number') throw new ERR_INVALID_CURSOR_POS(); + + const data = typeof y !== 'number' ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`; + return stream.write(data, callback); +} + +/** + * moves the cursor relative to its current location + */ + +function moveCursor(stream, dx, dy, callback) { + if (callback !== undefined) { + validateCallback(callback); + } + + if (stream == null || !(dx || dy)) { + if (typeof callback === 'function') process.nextTick(callback, null); + return true; + } + + 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 stream.write(data, callback); +} + +/** + * 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, callback) { + if (callback !== undefined) { + validateCallback(callback); + } + + if (stream === null || stream === undefined) { + if (typeof callback === 'function') process.nextTick(callback, null); + return true; + } + + const type = + dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine; + return stream.write(type, callback); +} + +/** + * clears the screen from the current position of the cursor down + */ + +function clearScreenDown(stream, callback) { + if (callback !== undefined) { + validateCallback(callback); + } + + if (stream === null || stream === undefined) { + if (typeof callback === 'function') process.nextTick(callback, null); + return true; + } + + return stream.write(kClearScreenDown, callback); +} + +module.exports = { + clearLine, + clearScreenDown, + cursorTo, + moveCursor, +}; diff --git a/lib/internal/readline/emitKeypressEvents.js b/lib/internal/readline/emitKeypressEvents.js new file mode 100644 index 00000000000000..9c5a2554de9d22 --- /dev/null +++ b/lib/internal/readline/emitKeypressEvents.js @@ -0,0 +1,96 @@ +'use strict'; + +const { + SafeStringIterator, + Symbol, +} = primordials; + +const { + charLengthAt, + CSI, + emitKeys, +} = require('internal/readline/utils'); + +const { clearTimeout, setTimeout } = require('timers'); +const { + kEscape, +} = CSI; + +const { StringDecoder } = require('string_decoder'); + +const KEYPRESS_DECODER = Symbol('keypress-decoder'); +const ESCAPE_DECODER = Symbol('escape-decoder'); + +// GNU readline library - keyseq-timeout is 500ms (default) +const ESCAPE_CODE_TIMEOUT = 500; + +/** + * accepts a readable Stream instance and makes it emit "keypress" events + */ + +function emitKeypressEvents(stream, iface = {}) { + if (stream[KEYPRESS_DECODER]) return; + + stream[KEYPRESS_DECODER] = new StringDecoder('utf8'); + + stream[ESCAPE_DECODER] = emitKeys(stream); + stream[ESCAPE_DECODER].next(); + + const triggerEscape = () => stream[ESCAPE_DECODER].next(''); + const { escapeCodeTimeout = ESCAPE_CODE_TIMEOUT } = iface; + let timeoutId; + + function onData(input) { + if (stream.listenerCount('keypress') > 0) { + const string = stream[KEYPRESS_DECODER].write(input); + if (string) { + clearTimeout(timeoutId); + + // This supports characters of length 2. + iface._sawKeyPress = charLengthAt(string, 0) === string.length; + iface.isCompletionEnabled = false; + + let length = 0; + for (const character of new SafeStringIterator(string)) { + length += character.length; + if (length === string.length) { + iface.isCompletionEnabled = true; + } + + try { + stream[ESCAPE_DECODER].next(character); + // Escape letter at the tail position + if (length === string.length && character === kEscape) { + timeoutId = setTimeout(triggerEscape, escapeCodeTimeout); + } + } catch (err) { + // If the generator throws (it could happen in the `keypress` + // event), we need to restart it. + stream[ESCAPE_DECODER] = emitKeys(stream); + stream[ESCAPE_DECODER].next(); + throw err; + } + } + } + } else { + // Nobody's watching anyway + stream.removeListener('data', onData); + stream.on('newListener', onNewListener); + } + } + + function onNewListener(event) { + if (event === 'keypress') { + stream.on('data', onData); + stream.removeListener('newListener', onNewListener); + } + } + + if (stream.listenerCount('keypress') > 0) { + stream.on('data', onData); + } else { + stream.on('newListener', onNewListener); + } +} + +module.exports = emitKeypressEvents; diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index 80c56144051e19..aff7dafe16e95a 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -41,7 +41,7 @@ const { clearScreenDown, cursorTo, moveCursor, -} = require('readline'); +} = require('internal/readline/callbacks'); const { commonPrefix, diff --git a/lib/readline.js b/lib/readline.js index 582fc26baf7a97..30762bb0bf71a9 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -65,6 +65,14 @@ const { SafeStringIterator, } = primordials; +const { + clearLine, + clearScreenDown, + cursorTo, + moveCursor, +} = require('internal/readline/callbacks'); +const emitKeypressEvents = require('internal/readline/emitKeypressEvents'); + const { AbortError, codes @@ -72,12 +80,10 @@ const { const { ERR_INVALID_ARG_VALUE, - ERR_INVALID_CURSOR_POS, } = codes; const { validateAbortSignal, validateArray, - validateCallback, validateString, validateUint32, } = require('internal/validators'); @@ -91,23 +97,11 @@ const { charLengthAt, charLengthLeft, commonPrefix, - CSI, - emitKeys, kSubstringSearch, } = require('internal/readline/utils'); const { promisify } = require('internal/util'); -const { clearTimeout, setTimeout } = require('timers'); -const { - kEscape, - kClearToLineBeginning, - kClearToLineEnd, - kClearLine, - kClearScreenDown -} = CSI; - - const { StringDecoder } = require('string_decoder'); // Lazy load Readable for startup performance. @@ -121,9 +115,6 @@ const lineEnding = /\r?\n|\r(?!\n)/; const kLineObjectStream = Symbol('line object stream'); const kQuestionCancel = Symbol('kQuestionCancel'); -const KEYPRESS_DECODER = Symbol('keypress-decoder'); -const ESCAPE_DECODER = Symbol('escape-decoder'); - // GNU readline library - keyseq-timeout is 500ms (default) const ESCAPE_CODE_TIMEOUT = 500; @@ -1244,183 +1235,6 @@ Interface.prototype[SymbolAsyncIterator] = function() { return this[kLineObjectStream][SymbolAsyncIterator](); }; -/** - * accepts a readable Stream instance and makes it emit "keypress" events - */ - -function emitKeypressEvents(stream, iface = {}) { - if (stream[KEYPRESS_DECODER]) return; - - stream[KEYPRESS_DECODER] = new StringDecoder('utf8'); - - stream[ESCAPE_DECODER] = emitKeys(stream); - stream[ESCAPE_DECODER].next(); - - const triggerEscape = () => stream[ESCAPE_DECODER].next(''); - const { escapeCodeTimeout = ESCAPE_CODE_TIMEOUT } = iface; - let timeoutId; - - function onData(input) { - if (stream.listenerCount('keypress') > 0) { - const string = stream[KEYPRESS_DECODER].write(input); - if (string) { - clearTimeout(timeoutId); - - // This supports characters of length 2. - iface._sawKeyPress = charLengthAt(string, 0) === string.length; - iface.isCompletionEnabled = false; - - let length = 0; - for (const character of new SafeStringIterator(string)) { - length += character.length; - if (length === string.length) { - iface.isCompletionEnabled = true; - } - - try { - stream[ESCAPE_DECODER].next(character); - // Escape letter at the tail position - if (length === string.length && character === kEscape) { - timeoutId = setTimeout(triggerEscape, escapeCodeTimeout); - } - } catch (err) { - // If the generator throws (it could happen in the `keypress` - // event), we need to restart it. - stream[ESCAPE_DECODER] = emitKeys(stream); - stream[ESCAPE_DECODER].next(); - throw err; - } - } - } - } else { - // Nobody's watching anyway - stream.removeListener('data', onData); - stream.on('newListener', onNewListener); - } - } - - function onNewListener(event) { - if (event === 'keypress') { - stream.on('data', onData); - stream.removeListener('newListener', onNewListener); - } - } - - if (stream.listenerCount('keypress') > 0) { - stream.on('data', onData); - } else { - stream.on('newListener', onNewListener); - } -} - -/** - * moves the cursor to the x and y coordinate on the given stream - */ - -function cursorTo(stream, x, y, callback) { - if (callback !== undefined) { - validateCallback(callback); - } - - if (typeof y === 'function') { - callback = y; - y = undefined; - } - - if (NumberIsNaN(x)) - throw new ERR_INVALID_ARG_VALUE('x', x); - if (NumberIsNaN(y)) - throw new ERR_INVALID_ARG_VALUE('y', y); - - if (stream == null || (typeof x !== 'number' && typeof y !== 'number')) { - if (typeof callback === 'function') - process.nextTick(callback, null); - return true; - } - - if (typeof x !== 'number') - throw new ERR_INVALID_CURSOR_POS(); - - const data = typeof y !== 'number' ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`; - return stream.write(data, callback); -} - -/** - * moves the cursor relative to its current location - */ - -function moveCursor(stream, dx, dy, callback) { - if (callback !== undefined) { - validateCallback(callback); - } - - if (stream == null || !(dx || dy)) { - if (typeof callback === 'function') - process.nextTick(callback, null); - return true; - } - - 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 stream.write(data, callback); -} - -/** - * 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, callback) { - if (callback !== undefined) { - validateCallback(callback); - } - - if (stream === null || stream === undefined) { - if (typeof callback === 'function') - process.nextTick(callback, null); - return true; - } - - const type = dir < 0 ? - kClearToLineBeginning : - dir > 0 ? - kClearToLineEnd : - kClearLine; - return stream.write(type, callback); -} - -/** - * clears the screen from the current position of the cursor down - */ - -function clearScreenDown(stream, callback) { - if (callback !== undefined) { - validateCallback(callback); - } - - if (stream === null || stream === undefined) { - if (typeof callback === 'function') - process.nextTick(callback, null); - return true; - } - - return stream.write(kClearScreenDown, callback); -} - module.exports = { Interface, clearLine, diff --git a/node.gyp b/node.gyp index 3308ecb432c34f..642a9767fb64d8 100644 --- a/node.gyp +++ b/node.gyp @@ -218,6 +218,8 @@ 'lib/internal/process/signal.js', 'lib/internal/process/task_queues.js', 'lib/internal/querystring.js', + 'lib/internal/readline/callbacks.js', + 'lib/internal/readline/emitKeypressEvents.js', 'lib/internal/readline/utils.js', 'lib/internal/repl.js', 'lib/internal/repl/await.js',