readline: move utilities to internal modules

PR-URL: https://github.com/nodejs/node/pull/38466
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
This commit is contained in:
Antoine du Hamel 2021-04-27 16:10:15 +02:00 committed by Michaël Zasso
parent 69c57e99f0
commit 54dfdbcccf
No known key found for this signature in database
GPG Key ID: 770F7A9A5AE15600
6 changed files with 243 additions and 196 deletions

View File

@ -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);
}

View File

@ -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,
};

View File

@ -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;

View File

@ -41,7 +41,7 @@ const {
clearScreenDown,
cursorTo,
moveCursor,
} = require('readline');
} = require('internal/readline/callbacks');
const {
commonPrefix,

View File

@ -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,

View File

@ -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',