Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

readline: key interval delay for \r & \n #8109

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/api/readline.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ added: v0.1.98
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.
* `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`
instance.
Expand Down
29 changes: 19 additions & 10 deletions lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
'use strict';

const kHistorySize = 30;
const kMincrlfDelay = 100;
const kMaxcrlfDelay = 2000;

const util = require('util');
const debug = util.debuglog('readline');
Expand Down Expand Up @@ -39,12 +41,13 @@ function Interface(input, output, completer, terminal) {
return self;
}

this._sawReturn = false;
this._sawReturnAt = 0;
this.isCompletionEnabled = true;
this._previousKey = null;

EventEmitter.call(this);
var historySize;
let crlfDelay;
let prompt = '> ';

if (arguments.length === 1) {
Expand All @@ -56,6 +59,7 @@ function Interface(input, output, completer, terminal) {
if (input.prompt !== undefined) {
prompt = input.prompt;
}
crlfDelay = input.crlfDelay;
input = input.input;
}

Expand Down Expand Up @@ -84,6 +88,8 @@ function Interface(input, output, completer, terminal) {
this.output = output;
this.input = input;
this.historySize = historySize;
this.crlfDelay = Math.max(kMincrlfDelay,
Math.min(kMaxcrlfDelay, crlfDelay >>> 0));

// Check arity, 2 - for async, 1 for sync
if (typeof completer === 'function') {
Expand Down Expand Up @@ -341,9 +347,10 @@ Interface.prototype._normalWrite = function(b) {
return;
}
var string = this._decoder.write(b);
if (this._sawReturn) {
if (this._sawReturnAt &&
Date.now() - this._sawReturnAt <= this.crlfDelay) {
string = string.replace(/^\n/, '');
this._sawReturn = false;
this._sawReturnAt = 0;
}

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

// got one or more newlines; process into "line" events
var lines = string.split(lineEnding);
Expand Down Expand Up @@ -842,20 +849,22 @@ Interface.prototype._ttyWrite = function(s, key) {
/* No modifier keys used */

// \r bookkeeping is only relevant if a \n comes right after.
if (this._sawReturn && key.name !== 'enter')
this._sawReturn = false;
if (this._sawReturnAt && key.name !== 'enter')
this._sawReturnAt = 0;

switch (key.name) {
case 'return': // carriage return, i.e. \r
this._sawReturn = true;
this._sawReturnAt = Date.now();
this._line();
break;

case 'enter':
if (this._sawReturn)
this._sawReturn = false;
else
// When key interval > crlfDelay
if (this._sawReturnAt === 0 ||
Date.now() - this._sawReturnAt > this.crlfDelay) {
this._line();
}
this._sawReturnAt = 0;
break;

case 'backspace':
Expand Down
51 changes: 51 additions & 0 deletions test/parallel/test-readline-interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,34 @@ function isWarned(emitter) {
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) {
var fi;
var rli;
Expand Down Expand Up @@ -199,6 +227,29 @@ function isWarned(emitter) {
assert.equal(callCount, expectedLines.length);
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(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap this function with 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
// character
fi = new FakeInput();
Expand Down