mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
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:
parent
69c57e99f0
commit
54dfdbcccf
6 changed files with 243 additions and 196 deletions
|
@ -429,7 +429,10 @@ const consoleMethods = {
|
||||||
if (this._stdout.isTTY && process.env.TERM !== 'dumb') {
|
if (this._stdout.isTTY && process.env.TERM !== 'dumb') {
|
||||||
// The require is here intentionally to avoid readline being
|
// The require is here intentionally to avoid readline being
|
||||||
// required too early when console is first loaded.
|
// 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);
|
cursorTo(this._stdout, 0, 0);
|
||||||
clearScreenDown(this._stdout);
|
clearScreenDown(this._stdout);
|
||||||
}
|
}
|
||||||
|
|
132
lib/internal/readline/callbacks.js
Normal file
132
lib/internal/readline/callbacks.js
Normal 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,
|
||||||
|
};
|
96
lib/internal/readline/emitKeypressEvents.js
Normal file
96
lib/internal/readline/emitKeypressEvents.js
Normal 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;
|
|
@ -41,7 +41,7 @@ const {
|
||||||
clearScreenDown,
|
clearScreenDown,
|
||||||
cursorTo,
|
cursorTo,
|
||||||
moveCursor,
|
moveCursor,
|
||||||
} = require('readline');
|
} = require('internal/readline/callbacks');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
commonPrefix,
|
commonPrefix,
|
||||||
|
|
202
lib/readline.js
202
lib/readline.js
|
@ -65,6 +65,14 @@ const {
|
||||||
SafeStringIterator,
|
SafeStringIterator,
|
||||||
} = primordials;
|
} = primordials;
|
||||||
|
|
||||||
|
const {
|
||||||
|
clearLine,
|
||||||
|
clearScreenDown,
|
||||||
|
cursorTo,
|
||||||
|
moveCursor,
|
||||||
|
} = require('internal/readline/callbacks');
|
||||||
|
const emitKeypressEvents = require('internal/readline/emitKeypressEvents');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
AbortError,
|
AbortError,
|
||||||
codes
|
codes
|
||||||
|
@ -72,12 +80,10 @@ const {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ERR_INVALID_ARG_VALUE,
|
ERR_INVALID_ARG_VALUE,
|
||||||
ERR_INVALID_CURSOR_POS,
|
|
||||||
} = codes;
|
} = codes;
|
||||||
const {
|
const {
|
||||||
validateAbortSignal,
|
validateAbortSignal,
|
||||||
validateArray,
|
validateArray,
|
||||||
validateCallback,
|
|
||||||
validateString,
|
validateString,
|
||||||
validateUint32,
|
validateUint32,
|
||||||
} = require('internal/validators');
|
} = require('internal/validators');
|
||||||
|
@ -91,23 +97,11 @@ const {
|
||||||
charLengthAt,
|
charLengthAt,
|
||||||
charLengthLeft,
|
charLengthLeft,
|
||||||
commonPrefix,
|
commonPrefix,
|
||||||
CSI,
|
|
||||||
emitKeys,
|
|
||||||
kSubstringSearch,
|
kSubstringSearch,
|
||||||
} = require('internal/readline/utils');
|
} = require('internal/readline/utils');
|
||||||
|
|
||||||
const { promisify } = require('internal/util');
|
const { promisify } = require('internal/util');
|
||||||
|
|
||||||
const { clearTimeout, setTimeout } = require('timers');
|
|
||||||
const {
|
|
||||||
kEscape,
|
|
||||||
kClearToLineBeginning,
|
|
||||||
kClearToLineEnd,
|
|
||||||
kClearLine,
|
|
||||||
kClearScreenDown
|
|
||||||
} = CSI;
|
|
||||||
|
|
||||||
|
|
||||||
const { StringDecoder } = require('string_decoder');
|
const { StringDecoder } = require('string_decoder');
|
||||||
|
|
||||||
// Lazy load Readable for startup performance.
|
// Lazy load Readable for startup performance.
|
||||||
|
@ -121,9 +115,6 @@ const lineEnding = /\r?\n|\r(?!\n)/;
|
||||||
const kLineObjectStream = Symbol('line object stream');
|
const kLineObjectStream = Symbol('line object stream');
|
||||||
const kQuestionCancel = Symbol('kQuestionCancel');
|
const kQuestionCancel = Symbol('kQuestionCancel');
|
||||||
|
|
||||||
const KEYPRESS_DECODER = Symbol('keypress-decoder');
|
|
||||||
const ESCAPE_DECODER = Symbol('escape-decoder');
|
|
||||||
|
|
||||||
// GNU readline library - keyseq-timeout is 500ms (default)
|
// GNU readline library - keyseq-timeout is 500ms (default)
|
||||||
const ESCAPE_CODE_TIMEOUT = 500;
|
const ESCAPE_CODE_TIMEOUT = 500;
|
||||||
|
|
||||||
|
@ -1244,183 +1235,6 @@ Interface.prototype[SymbolAsyncIterator] = function() {
|
||||||
return this[kLineObjectStream][SymbolAsyncIterator]();
|
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 = {
|
module.exports = {
|
||||||
Interface,
|
Interface,
|
||||||
clearLine,
|
clearLine,
|
||||||
|
|
2
node.gyp
2
node.gyp
|
@ -218,6 +218,8 @@
|
||||||
'lib/internal/process/signal.js',
|
'lib/internal/process/signal.js',
|
||||||
'lib/internal/process/task_queues.js',
|
'lib/internal/process/task_queues.js',
|
||||||
'lib/internal/querystring.js',
|
'lib/internal/querystring.js',
|
||||||
|
'lib/internal/readline/callbacks.js',
|
||||||
|
'lib/internal/readline/emitKeypressEvents.js',
|
||||||
'lib/internal/readline/utils.js',
|
'lib/internal/readline/utils.js',
|
||||||
'lib/internal/repl.js',
|
'lib/internal/repl.js',
|
||||||
'lib/internal/repl/await.js',
|
'lib/internal/repl/await.js',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue