From e5e57b20312ed6986d4fe8347ea7661ad639e5fe Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 29 Apr 2017 13:33:35 -0700 Subject: [PATCH 1/5] readline: use module.exports = {} --- lib/readline.js | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index d32456155663ec..141a2200517466 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -43,7 +43,7 @@ const isFullWidthCodePoint = internalReadline.isFullWidthCodePoint; const stripVTControlCharacters = internalReadline.stripVTControlCharacters; -exports.createInterface = function(input, output, completer, terminal) { +function createInterface(input, output, completer, terminal) { return new Interface(input, output, completer, terminal); }; @@ -311,13 +311,13 @@ Interface.prototype._refreshLine = function() { // first move to the bottom of the current line, based on cursor pos var prevRows = this.prevRows || 0; if (prevRows > 0) { - exports.moveCursor(this.output, 0, -prevRows); + moveCursor(this.output, 0, -prevRows); } // Cursor to left edge. - exports.cursorTo(this.output, 0); + cursorTo(this.output, 0); // erase data - exports.clearScreenDown(this.output); + clearScreenDown(this.output); // Write the prompt and the current buffer content. this._writeToOutput(line); @@ -328,11 +328,11 @@ Interface.prototype._refreshLine = function() { } // Move cursor to original position. - exports.cursorTo(this.output, cursorPos.cols); + cursorTo(this.output, cursorPos.cols); var diff = lineRows - cursorPos.rows; if (diff > 0) { - exports.moveCursor(this.output, 0, -diff); + moveCursor(this.output, 0, -diff); } this.prevRows = cursorPos.rows; @@ -716,7 +716,7 @@ Interface.prototype._moveCursor = function(dx) { this.line.substring(this.cursor, oldcursor) ); } - exports.moveCursor(this.output, diffWidth, 0); + moveCursor(this.output, diffWidth, 0); this.prevRows = newPos.rows; } else { this._refreshLine(); @@ -798,8 +798,8 @@ Interface.prototype._ttyWrite = function(s, key) { break; case 'l': // clear the whole screen - exports.cursorTo(this.output, 0, 0); - exports.clearScreenDown(this.output); + cursorTo(this.output, 0, 0); + clearScreenDown(this.output); this._refreshLine(); break; @@ -957,10 +957,6 @@ Interface.prototype._ttyWrite = function(s, key) { } }; - -exports.Interface = Interface; - - /** * accepts a readable Stream instance and makes it emit "keypress" events */ @@ -1036,8 +1032,6 @@ function emitKeypressEvents(stream, iface) { stream.on('newListener', onNewListener); } } -exports.emitKeypressEvents = emitKeypressEvents; - /** * moves the cursor to the x and y coordinate on the given stream @@ -1059,8 +1053,6 @@ function cursorTo(stream, x, y) { stream.write('\x1b[' + (y + 1) + ';' + (x + 1) + 'H'); } } -exports.cursorTo = cursorTo; - /** * moves the cursor relative to its current location @@ -1082,8 +1074,6 @@ function moveCursor(stream, dx, dy) { stream.write('\x1b[' + dy + 'B'); } } -exports.moveCursor = moveCursor; - /** * clears the current line the cursor is on: @@ -1107,8 +1097,6 @@ function clearLine(stream, dir) { stream.write('\x1b[2K'); } } -exports.clearLine = clearLine; - /** * clears the screen from the current position of the cursor down @@ -1120,4 +1108,13 @@ function clearScreenDown(stream) { stream.write('\x1b[0J'); } -exports.clearScreenDown = clearScreenDown; + +module.exports = { + Interface, + clearLine, + clearScreenDown, + createInterface, + cursorTo, + emitKeypressEvents, + moveCursor +}; From f5f254b9edec3ba2735aa77e17beb62427992a74 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 29 Apr 2017 13:43:43 -0700 Subject: [PATCH 2/5] readline: use module.exports = {} on internal/readline --- lib/internal/readline.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/internal/readline.js b/lib/internal/readline.js index 9f1884ad0ea9e1..4474b0234575d5 100644 --- a/lib/internal/readline.js +++ b/lib/internal/readline.js @@ -7,15 +7,12 @@ const ansi = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; - -module.exports = { - emitKeys, - stripVTControlCharacters -}; +var getStringWidth; +var isFullWidthCodePoint; if (process.binding('config').hasIntl) { const icu = process.binding('icu'); - module.exports.getStringWidth = function getStringWidth(str, options) { + getStringWidth = function getStringWidth(str, options) { options = options || {}; if (!Number.isInteger(str)) str = stripVTControlCharacters(String(str)); @@ -23,7 +20,7 @@ if (process.binding('config').hasIntl) { Boolean(options.ambiguousAsFullWidth), Boolean(options.expandEmojiSequence)); }; - module.exports.isFullWidthCodePoint = + isFullWidthCodePoint = function isFullWidthCodePoint(code, options) { if (typeof code !== 'number') return false; @@ -33,9 +30,9 @@ if (process.binding('config').hasIntl) { /** * Returns the number of columns required to display the given string. */ - module.exports.getStringWidth = function getStringWidth(str) { + getStringWidth = function getStringWidth(str) { if (Number.isInteger(str)) - return module.exports.isFullWidthCodePoint(str) ? 2 : 1; + return isFullWidthCodePoint(str) ? 2 : 1; let width = 0; @@ -48,7 +45,7 @@ if (process.binding('config').hasIntl) { i++; } - if (module.exports.isFullWidthCodePoint(code)) { + if (isFullWidthCodePoint(code)) { width += 2; } else { width++; @@ -62,7 +59,7 @@ if (process.binding('config').hasIntl) { * Returns true if the character represented by a given * Unicode code point is full-width. Otherwise returns false. */ - module.exports.isFullWidthCodePoint = function isFullWidthCodePoint(code) { + isFullWidthCodePoint = function isFullWidthCodePoint(code) { if (!Number.isInteger(code)) { return false; } @@ -407,3 +404,10 @@ function* emitKeys(stream) { /* Unrecognized or broken escape sequence, don't emit anything */ } } + +module.exports = { + emitKeys, + getStringWidth, + isFullWidthCodePoint, + stripVTControlCharacters +}; From 88eb5cd5a82675acfadaa9cc9038c90d17a67054 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 29 Apr 2017 13:46:26 -0700 Subject: [PATCH 3/5] readline: multiple code cleanups Variety of code maintenance updates, cleanups --- lib/readline.js | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index 141a2200517466..cc7ffc40c30780 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -27,25 +27,32 @@ 'use strict'; +const { debug, inherits } = require('util'); +const Buffer = require('buffer').Buffer; +const EventEmitter = require('events'); +const { + emitKeys, + getStringWidth, + isFullWidthCodePoint, + stripVTControlCharacters +} = require('internal/readline'); + const kHistorySize = 30; const kMincrlfDelay = 100; const kMaxcrlfDelay = 2000; +// \r\n, \n, or \r followed by something other than \n +const lineEnding = /\r?\n|\r(?!\n)/; -const util = require('util'); -const debug = util.debuglog('readline'); -const inherits = util.inherits; -const Buffer = require('buffer').Buffer; -const EventEmitter = require('events'); -const internalReadline = require('internal/readline'); -const emitKeys = internalReadline.emitKeys; -const getStringWidth = internalReadline.getStringWidth; -const isFullWidthCodePoint = internalReadline.isFullWidthCodePoint; -const stripVTControlCharacters = internalReadline.stripVTControlCharacters; +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; function createInterface(input, output, completer, terminal) { return new Interface(input, output, completer, terminal); -}; +} function Interface(input, output, completer, terminal) { @@ -373,8 +380,6 @@ Interface.prototype.write = function(d, key) { this.terminal ? this._ttyWrite(d, key) : this._normalWrite(d); }; -// \r\n, \n, or \r followed by something other than \n -const lineEnding = /\r?\n|\r(?!\n)/; Interface.prototype._normalWrite = function(b) { if (b === undefined) { return; @@ -961,12 +966,6 @@ Interface.prototype._ttyWrite = function(s, key) { * accepts a readable Stream instance and makes it emit "keypress" events */ -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; - function emitKeypressEvents(stream, iface) { if (stream[KEYPRESS_DECODER]) return; var StringDecoder = require('string_decoder').StringDecoder; // lazy load From 291a464528af0ab0c52aeff527c8a21965c6bfc4 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 29 Apr 2017 17:05:04 -0700 Subject: [PATCH 4/5] readline: move escape codes into internal/readline Moves escape codes into internal/readline for easier management. --- lib/internal/readline.js | 27 +++++++++++++++++++++++---- lib/readline.js | 31 ++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/lib/internal/readline.js b/lib/internal/readline.js index 4474b0234575d5..96da5cc82f2af5 100644 --- a/lib/internal/readline.js +++ b/lib/internal/readline.js @@ -7,9 +7,27 @@ const ansi = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; +const kEscape = '\x1b'; + var getStringWidth; var isFullWidthCodePoint; +function CSI(strings, ...args) { + let ret = `${kEscape}[`; + for (var n = 0; n < strings.length; n++) { + ret += strings[n]; + if (n < args.length) + ret += args[n]; + } + return ret; +} + +CSI.kEscape = kEscape; +CSI.kClearToBeginning = CSI`1K`; +CSI.kClearToEnd = CSI`0K`; +CSI.kClearLine = CSI`2K`; +CSI.kClearScreenDown = CSI`0J`; + if (process.binding('config').hasIntl) { const icu = process.binding('icu'); getStringWidth = function getStringWidth(str, options) { @@ -151,11 +169,11 @@ function* emitKeys(stream) { shift: false }; - if (ch === '\x1b') { + if (ch === kEscape) { escaped = true; s += (ch = yield); - if (ch === '\x1b') { + if (ch === kEscape) { s += (ch = yield); } } @@ -370,7 +388,7 @@ function* emitKeys(stream) { // backspace or ctrl+h key.name = 'backspace'; key.meta = escaped; - } else if (ch === '\x1b') { + } else if (ch === kEscape) { // escape key key.name = 'escape'; key.meta = escaped; @@ -409,5 +427,6 @@ module.exports = { emitKeys, getStringWidth, isFullWidthCodePoint, - stripVTControlCharacters + stripVTControlCharacters, + CSI }; diff --git a/lib/readline.js b/lib/readline.js index cc7ffc40c30780..9a5c2e87870c1f 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -31,12 +31,21 @@ const { debug, inherits } = require('util'); const Buffer = require('buffer').Buffer; const EventEmitter = require('events'); const { + CSI, emitKeys, getStringWidth, isFullWidthCodePoint, stripVTControlCharacters } = require('internal/readline'); +const { + kEscape, + kClearToBeginning, + kClearToEnd, + kClearLine, + kClearScreenDown +} = CSI; + const kHistorySize = 30; const kMincrlfDelay = 100; const kMaxcrlfDelay = 2000; @@ -995,7 +1004,7 @@ function emitKeypressEvents(stream, iface) { try { stream[ESCAPE_DECODER].next(r[i]); // Escape letter at the tail position - if (r[i] === '\x1b' && i + 1 === r.length) { + if (r[i] === kEscape && i + 1 === r.length) { timeoutId = setTimeout(escapeCodeTimeout, ESCAPE_CODE_TIMEOUT); } } catch (err) { @@ -1047,9 +1056,9 @@ function cursorTo(stream, x, y) { throw new Error('Can\'t set cursor row without also setting it\'s column'); if (typeof y !== 'number') { - stream.write('\x1b[' + (x + 1) + 'G'); + stream.write(CSI`${x + 1}G`); } else { - stream.write('\x1b[' + (y + 1) + ';' + (x + 1) + 'H'); + stream.write(CSI`${y + 1};${x + 1}H`); } } @@ -1062,15 +1071,15 @@ function moveCursor(stream, dx, dy) { return; if (dx < 0) { - stream.write('\x1b[' + (-dx) + 'D'); + stream.write(CSI`${-dx}D`); } else if (dx > 0) { - stream.write('\x1b[' + dx + 'C'); + stream.write(CSI`${dx}C`); } if (dy < 0) { - stream.write('\x1b[' + (-dy) + 'A'); + stream.write(CSI`${-dy}A`); } else if (dy > 0) { - stream.write('\x1b[' + dy + 'B'); + stream.write(CSI`${dy}B`); } } @@ -1087,13 +1096,13 @@ function clearLine(stream, dir) { if (dir < 0) { // to the beginning - stream.write('\x1b[1K'); + stream.write(kClearToBeginning); } else if (dir > 0) { // to the end - stream.write('\x1b[0K'); + stream.write(kClearToEnd); } else { // entire line - stream.write('\x1b[2K'); + stream.write(kClearLine); } } @@ -1105,7 +1114,7 @@ function clearScreenDown(stream) { if (stream === null || stream === undefined) return; - stream.write('\x1b[0J'); + stream.write(kClearScreenDown); } module.exports = { From a5521a67a4c6b8c1e9a58570508647e1362285cc Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 29 Apr 2017 20:42:45 -0700 Subject: [PATCH 5/5] test: expand test coverage of readline --- test/parallel/test-readline-csi.js | 90 ++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 test/parallel/test-readline-csi.js diff --git a/test/parallel/test-readline-csi.js b/test/parallel/test-readline-csi.js new file mode 100644 index 00000000000000..bde37138e3b3dd --- /dev/null +++ b/test/parallel/test-readline-csi.js @@ -0,0 +1,90 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const readline = require('readline'); +const { Writable } = require('stream'); +const { CSI } = require('internal/readline'); + +{ + assert(CSI); + assert.strictEqual(CSI.kClearToBeginning, '\x1b[1K'); + assert.strictEqual(CSI.kClearToEnd, '\x1b[0K'); + assert.strictEqual(CSI.kClearLine, '\x1b[2K'); + assert.strictEqual(CSI.kClearScreenDown, '\x1b[0J'); + assert.strictEqual(CSI`1${2}3`, '\x1b[123'); +} + +class TestWritable extends Writable { + constructor() { + super(); + this.data = ''; + } + _write(chunk, encoding, callback) { + this.data += chunk.toString(); + callback(); + } +} + +const writable = new TestWritable(); + +readline.clearScreenDown(writable); +assert.deepStrictEqual(writable.data, CSI.kClearScreenDown); + +writable.data = ''; +readline.clearLine(writable, -1); +assert.deepStrictEqual(writable.data, CSI.kClearToBeginning); + +writable.data = ''; +readline.clearLine(writable, 1); +assert.deepStrictEqual(writable.data, CSI.kClearToEnd); + +writable.data = ''; +readline.clearLine(writable, 0); +assert.deepStrictEqual(writable.data, CSI.kClearLine); + +// 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]); + assert.deepStrictEqual(writable.data, set[2]); +}); + +assert.doesNotThrow(() => readline.cursorTo(null)); +assert.doesNotThrow(() => readline.cursorTo()); + +writable.data = ''; +assert.doesNotThrow(() => readline.cursorTo(writable, 'a')); +assert.strictEqual(writable.data, ''); + +writable.data = ''; +assert.doesNotThrow(() => readline.cursorTo(writable, 'a', 'b')); +assert.strictEqual(writable.data, ''); + +writable.data = ''; +assert.throws( + () => readline.cursorTo(writable, 'a', 1), + common.expectsError({ + type: Error, + message: /^Can't set cursor row without also setting it's column$/ + })); +assert.strictEqual(writable.data, ''); + +writable.data = ''; +assert.doesNotThrow(() => readline.cursorTo(writable, 1, 'a')); +assert.strictEqual(writable.data, '\x1b[2G'); + +writable.data = ''; +assert.doesNotThrow(() => readline.cursorTo(writable, 1, 2)); +assert.strictEqual(writable.data, '\x1b[3;2H');