Browse Source

readline: key interval delay for \r & \n

Emit two line events when there is a delay between
CR('\r') and LF('\n').

Introduced a new option `crlfDelay`. If the delay between \r and \n
exceeds `crlfDelay` milliseconds, both \r and \n will be treated as
separate end-of-line input. Default to 100 milliseconds.
`crlfDelay` will be coerced to [100, 2000] range.

PR-URL: https://github.com/nodejs/node/pull/8109
Reviewed-By: Yorkie Liu <yorkiefixer@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Brian White <mscdex@mscdex.net>
v7.x
Prince J Wesley 8 years ago
parent
commit
a634554fca
No known key found for this signature in database GPG Key ID: 9D76A33B02A4C46B
  1. 4
      doc/api/readline.md
  2. 29
      lib/readline.js
  3. 51
      test/parallel/test-readline-interface.js

4
doc/api/readline.md

@ -361,6 +361,10 @@ added: v0.1.98
only if `terminal` is set to `true` by the user or by an internal `output` only if `terminal` is set to `true` by the user or by an internal `output`
check, otherwise the history caching mechanism is not initialized at all. check, otherwise the history caching mechanism is not initialized at all.
* `prompt` - the prompt string to use. Default: `'> '` * `prompt` - the prompt string to use. Default: `'> '`
* `crlfDelay` {number} If the delay between `\r` and `\n` exceeds
`crlfDelay` milliseconds, both `\r` and `\n` will be treated as separate
end-of-line input. Default to `100` milliseconds.
`crlfDelay` will be coerced to `[100, 2000]` range.
The `readline.createInterface()` method creates a new `readline.Interface` The `readline.createInterface()` method creates a new `readline.Interface`
instance. instance.

29
lib/readline.js

@ -7,6 +7,8 @@
'use strict'; 'use strict';
const kHistorySize = 30; const kHistorySize = 30;
const kMincrlfDelay = 100;
const kMaxcrlfDelay = 2000;
const util = require('util'); const util = require('util');
const debug = util.debuglog('readline'); const debug = util.debuglog('readline');
@ -39,13 +41,14 @@ function Interface(input, output, completer, terminal) {
return self; return self;
} }
this._sawReturn = false; this._sawReturnAt = 0;
this.isCompletionEnabled = true; this.isCompletionEnabled = true;
this._sawKeyPress = false; this._sawKeyPress = false;
this._previousKey = null; this._previousKey = null;
EventEmitter.call(this); EventEmitter.call(this);
var historySize; var historySize;
let crlfDelay;
let prompt = '> '; let prompt = '> ';
if (arguments.length === 1) { if (arguments.length === 1) {
@ -57,6 +60,7 @@ function Interface(input, output, completer, terminal) {
if (input.prompt !== undefined) { if (input.prompt !== undefined) {
prompt = input.prompt; prompt = input.prompt;
} }
crlfDelay = input.crlfDelay;
input = input.input; input = input.input;
} }
@ -85,6 +89,8 @@ function Interface(input, output, completer, terminal) {
this.output = output; this.output = output;
this.input = input; this.input = input;
this.historySize = historySize; this.historySize = historySize;
this.crlfDelay = Math.max(kMincrlfDelay,
Math.min(kMaxcrlfDelay, crlfDelay >>> 0));
// Check arity, 2 - for async, 1 for sync // Check arity, 2 - for async, 1 for sync
if (typeof completer === 'function') { if (typeof completer === 'function') {
@ -345,9 +351,10 @@ Interface.prototype._normalWrite = function(b) {
return; return;
} }
var string = this._decoder.write(b); var string = this._decoder.write(b);
if (this._sawReturn) { if (this._sawReturnAt &&
Date.now() - this._sawReturnAt <= this.crlfDelay) {
string = string.replace(/^\n/, ''); string = string.replace(/^\n/, '');
this._sawReturn = false; this._sawReturnAt = 0;
} }
// Run test() on the new string chunk, not on the entire line buffer. // Run test() on the new string chunk, not on the entire line buffer.
@ -358,7 +365,7 @@ Interface.prototype._normalWrite = function(b) {
this._line_buffer = null; this._line_buffer = null;
} }
if (newPartContainsEnding) { if (newPartContainsEnding) {
this._sawReturn = string.endsWith('\r'); this._sawReturnAt = string.endsWith('\r') ? Date.now() : 0;
// got one or more newlines; process into "line" events // got one or more newlines; process into "line" events
var lines = string.split(lineEnding); var lines = string.split(lineEnding);
@ -846,20 +853,22 @@ Interface.prototype._ttyWrite = function(s, key) {
/* No modifier keys used */ /* No modifier keys used */
// \r bookkeeping is only relevant if a \n comes right after. // \r bookkeeping is only relevant if a \n comes right after.
if (this._sawReturn && key.name !== 'enter') if (this._sawReturnAt && key.name !== 'enter')
this._sawReturn = false; this._sawReturnAt = 0;
switch (key.name) { switch (key.name) {
case 'return': // carriage return, i.e. \r case 'return': // carriage return, i.e. \r
this._sawReturn = true; this._sawReturnAt = Date.now();
this._line(); this._line();
break; break;
case 'enter': case 'enter':
if (this._sawReturn) // When key interval > crlfDelay
this._sawReturn = false; if (this._sawReturnAt === 0 ||
else Date.now() - this._sawReturnAt > this.crlfDelay) {
this._line(); this._line();
}
this._sawReturnAt = 0;
break; break;
case 'backspace': case 'backspace':

51
test/parallel/test-readline-interface.js

@ -26,6 +26,34 @@ function isWarned(emitter) {
return false; return false;
} }
{
// Default crlfDelay is 100ms
const fi = new FakeInput();
const rli = new readline.Interface({ input: fi, output: fi });
assert.strictEqual(rli.crlfDelay, 100);
rli.close();
}
{
// Minimum crlfDelay is 100ms
const fi = new FakeInput();
const rli = new readline.Interface({ input: fi, output: fi, crlfDelay: 0});
assert.strictEqual(rli.crlfDelay, 100);
rli.close();
}
{
// Maximum crlfDelay is 2000ms
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
crlfDelay: 1 << 30
});
assert.strictEqual(rli.crlfDelay, 2000);
rli.close();
}
[ true, false ].forEach(function(terminal) { [ true, false ].forEach(function(terminal) {
var fi; var fi;
var rli; var rli;
@ -199,6 +227,29 @@ function isWarned(emitter) {
assert.equal(callCount, expectedLines.length); assert.equal(callCount, expectedLines.length);
rli.close(); rli.close();
// Emit two line events when the delay
// between \r and \n exceeds crlfDelay
{
const fi = new FakeInput();
const delay = 200;
const rli = new readline.Interface({
input: fi,
output: fi,
terminal: terminal,
crlfDelay: delay
});
let callCount = 0;
rli.on('line', function(line) {
callCount++;
});
fi.emit('data', '\r');
setTimeout(common.mustCall(() => {
fi.emit('data', '\n');
assert.equal(callCount, 2);
rli.close();
}), delay * 2);
}
// \t when there is no completer function should behave like an ordinary // \t when there is no completer function should behave like an ordinary
// character // character
fi = new FakeInput(); fi = new FakeInput();

Loading…
Cancel
Save