Skip to content

Commit a634554

Browse files
committed
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: #8109 Reviewed-By: Yorkie Liu <yorkiefixer@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Brian White <mscdex@mscdex.net>
1 parent da0651a commit a634554

File tree

3 files changed

+74
-10
lines changed

3 files changed

+74
-10
lines changed

doc/api/readline.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,10 @@ added: v0.1.98
361361
only if `terminal` is set to `true` by the user or by an internal `output`
362362
check, otherwise the history caching mechanism is not initialized at all.
363363
* `prompt` - the prompt string to use. Default: `'> '`
364+
* `crlfDelay` {number} If the delay between `\r` and `\n` exceeds
365+
`crlfDelay` milliseconds, both `\r` and `\n` will be treated as separate
366+
end-of-line input. Default to `100` milliseconds.
367+
`crlfDelay` will be coerced to `[100, 2000]` range.
364368

365369
The `readline.createInterface()` method creates a new `readline.Interface`
366370
instance.

lib/readline.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
'use strict';
88

99
const kHistorySize = 30;
10+
const kMincrlfDelay = 100;
11+
const kMaxcrlfDelay = 2000;
1012

1113
const util = require('util');
1214
const debug = util.debuglog('readline');
@@ -39,13 +41,14 @@ function Interface(input, output, completer, terminal) {
3941
return self;
4042
}
4143

42-
this._sawReturn = false;
44+
this._sawReturnAt = 0;
4345
this.isCompletionEnabled = true;
4446
this._sawKeyPress = false;
4547
this._previousKey = null;
4648

4749
EventEmitter.call(this);
4850
var historySize;
51+
let crlfDelay;
4952
let prompt = '> ';
5053

5154
if (arguments.length === 1) {
@@ -57,6 +60,7 @@ function Interface(input, output, completer, terminal) {
5760
if (input.prompt !== undefined) {
5861
prompt = input.prompt;
5962
}
63+
crlfDelay = input.crlfDelay;
6064
input = input.input;
6165
}
6266

@@ -85,6 +89,8 @@ function Interface(input, output, completer, terminal) {
8589
this.output = output;
8690
this.input = input;
8791
this.historySize = historySize;
92+
this.crlfDelay = Math.max(kMincrlfDelay,
93+
Math.min(kMaxcrlfDelay, crlfDelay >>> 0));
8894

8995
// Check arity, 2 - for async, 1 for sync
9096
if (typeof completer === 'function') {
@@ -345,9 +351,10 @@ Interface.prototype._normalWrite = function(b) {
345351
return;
346352
}
347353
var string = this._decoder.write(b);
348-
if (this._sawReturn) {
354+
if (this._sawReturnAt &&
355+
Date.now() - this._sawReturnAt <= this.crlfDelay) {
349356
string = string.replace(/^\n/, '');
350-
this._sawReturn = false;
357+
this._sawReturnAt = 0;
351358
}
352359

353360
// Run test() on the new string chunk, not on the entire line buffer.
@@ -358,7 +365,7 @@ Interface.prototype._normalWrite = function(b) {
358365
this._line_buffer = null;
359366
}
360367
if (newPartContainsEnding) {
361-
this._sawReturn = string.endsWith('\r');
368+
this._sawReturnAt = string.endsWith('\r') ? Date.now() : 0;
362369

363370
// got one or more newlines; process into "line" events
364371
var lines = string.split(lineEnding);
@@ -846,20 +853,22 @@ Interface.prototype._ttyWrite = function(s, key) {
846853
/* No modifier keys used */
847854

848855
// \r bookkeeping is only relevant if a \n comes right after.
849-
if (this._sawReturn && key.name !== 'enter')
850-
this._sawReturn = false;
856+
if (this._sawReturnAt && key.name !== 'enter')
857+
this._sawReturnAt = 0;
851858

852859
switch (key.name) {
853860
case 'return': // carriage return, i.e. \r
854-
this._sawReturn = true;
861+
this._sawReturnAt = Date.now();
855862
this._line();
856863
break;
857864

858865
case 'enter':
859-
if (this._sawReturn)
860-
this._sawReturn = false;
861-
else
866+
// When key interval > crlfDelay
867+
if (this._sawReturnAt === 0 ||
868+
Date.now() - this._sawReturnAt > this.crlfDelay) {
862869
this._line();
870+
}
871+
this._sawReturnAt = 0;
863872
break;
864873

865874
case 'backspace':

test/parallel/test-readline-interface.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,34 @@ function isWarned(emitter) {
2626
return false;
2727
}
2828

29+
{
30+
// Default crlfDelay is 100ms
31+
const fi = new FakeInput();
32+
const rli = new readline.Interface({ input: fi, output: fi });
33+
assert.strictEqual(rli.crlfDelay, 100);
34+
rli.close();
35+
}
36+
37+
{
38+
// Minimum crlfDelay is 100ms
39+
const fi = new FakeInput();
40+
const rli = new readline.Interface({ input: fi, output: fi, crlfDelay: 0});
41+
assert.strictEqual(rli.crlfDelay, 100);
42+
rli.close();
43+
}
44+
45+
{
46+
// Maximum crlfDelay is 2000ms
47+
const fi = new FakeInput();
48+
const rli = new readline.Interface({
49+
input: fi,
50+
output: fi,
51+
crlfDelay: 1 << 30
52+
});
53+
assert.strictEqual(rli.crlfDelay, 2000);
54+
rli.close();
55+
}
56+
2957
[ true, false ].forEach(function(terminal) {
3058
var fi;
3159
var rli;
@@ -199,6 +227,29 @@ function isWarned(emitter) {
199227
assert.equal(callCount, expectedLines.length);
200228
rli.close();
201229

230+
// Emit two line events when the delay
231+
// between \r and \n exceeds crlfDelay
232+
{
233+
const fi = new FakeInput();
234+
const delay = 200;
235+
const rli = new readline.Interface({
236+
input: fi,
237+
output: fi,
238+
terminal: terminal,
239+
crlfDelay: delay
240+
});
241+
let callCount = 0;
242+
rli.on('line', function(line) {
243+
callCount++;
244+
});
245+
fi.emit('data', '\r');
246+
setTimeout(common.mustCall(() => {
247+
fi.emit('data', '\n');
248+
assert.equal(callCount, 2);
249+
rli.close();
250+
}), delay * 2);
251+
}
252+
202253
// \t when there is no completer function should behave like an ordinary
203254
// character
204255
fi = new FakeInput();

0 commit comments

Comments
 (0)