Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

Readline proposal and bugfixes. #2757

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
130 changes: 97 additions & 33 deletions doc/api/readline.markdown
Expand Up @@ -4,8 +4,8 @@ To use this module, do `require('readline')`. Readline allows reading of a
stream (such as STDIN) on a line-by-line basis.

Note that once you've invoked this module, your node program will not
terminate until you've closed the interface, and the STDIN stream. Here's how
to allow your program to gracefully terminate:
terminate until you've paused the interface. Here's how to allow your
Copy link
Member

Choose a reason for hiding this comment

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

Can you change last sentence? It's meaningless now (you're stating same thing as in first sent. )

program to gracefully pause:

var rl = require('readline');

Expand All @@ -14,10 +14,7 @@ to allow your program to gracefully terminate:
// TODO: Log the answer in a database
console.log("Thank you for your valuable feedback.");

// These two lines together allow the program to terminate. Without
// them, it would run forever.
i.close();
process.stdin.destroy();
i.pause();
});

### rl.createInterface(input, output, completer)
Expand Down Expand Up @@ -48,6 +45,9 @@ Sets the prompt, for example when you run `node` on the command line, you see
Readies readline for input from the user, putting the current `setPrompt`
options on a new line, giving the user a new spot to write.

This will also resume the `in` stream used with `createInterface` if it has
been paused.

<!-- ### rl.getColumns() Not available? -->

### rl.question(query, callback)
Expand All @@ -56,27 +56,29 @@ Prepends the prompt with `query` and invokes `callback` with the user's
response. Displays the query to the user, and then invokes `callback` with the
user's response after it has been typed.

This will also resume the `in` stream used with `createInterface` if it has
been paused.

Example usage:

interface.question('What is your favorite food?', function(answer) {
console.log('Oh, so your favorite food is ' + answer);
});

### rl.close()

Closes tty.

### rl.pause()

Pauses tty.
Pauses the readline `in` stream, allowing it to be resumed later if needed.

### rl.resume()

Resumes tty.
Resumes the readline `in` stream.

### rl.write()

Writes to tty.
Writes to tty.

This will also resume the `in` stream used with `createInterface` if it has
been paused.

### Event: 'line'

Expand All @@ -91,27 +93,98 @@ Example of listening for `line`:
console.log('You just typed: '+cmd);
});

### Event: 'close'
### Event: 'pause'

`function () {}`

Emitted whenever the `in` stream receives a `^C` or `^D`, respectively known
as `SIGINT` and `EOT`. This is a good way to know the user is finished using
your program.
Emitted whenever the `in` stream is paused or receives `^D`, respectively known
as `EOT`. This event is also called if there is no `SIGINT` event listener
present when the `in` stream receives a `^C`, respectively known as `SIGINT`.

Example of listening for `close`, and exiting the program afterward:
Also emitted whenever the `in` stream is not paused and receives the `SIGCONT`
event. (See events `SIGTSTP` and `SIGCONT`)

rl.on('close', function() {
console.log('goodbye!');
process.exit(0);
Example of listening for `pause`:

rl.on('pause', function() {
console.log('Readline paused.');
});

### Event: 'resume'

`function () {}`

Emitted whenever the `in` stream is resumed.

Example of listening for `resume`:

rl.on('resume', function() {
console.log('Readline resumed.');
});

### Event: 'SIGINT'

`function () {}`

Emitted whenever the `in` stream receives a `^C`, respectively known as
`SIGINT`. If there is no `SIGINT` event listener present when the `in` stream
receives a `SIGINT`, `pause` will be triggered.

Example of listening for `SIGINT`:

rl.on('SIGINT', function() {
rl.question('Are you sure you want to exit?', function(answer) {
if (answer.match(/^y(es)?$/i)) rl.pause();
});
});
Copy link
Author

Choose a reason for hiding this comment

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

These examples were removed because they are no longer valid with the API change.


### Event: 'SIGTSTP'

`function () {}`

Emitted whenever the `in` stream receives a `^Z`, respectively known as
`SIGTSTP`. If there is no `SIGTSTP` event listener present when the `in` stream
receives a `SIGTSTP`, the program will be sent to the background.

When the program is resumed with `fg`, the `pause` and `SIGCONT` events will be
emitted. You can use either to resume the stream.

The `pause` and `SIGCONT` events will not be triggered if the stream was paused
before the program was sent to the background.

Example of listening for `SIGTSTP`:

rl.on('SIGTSTP', function() {
// This will override SIGTSTP and prevent the program from going to the
// background.
console.log('Caught SIGTSTP.');
});

### Event: 'SIGCONT'

`function () {}`

Emitted whenever the `in` stream is sent to the background with `^Z`,
respectively known as `SIGTSTP`, and then continued with `fg`. This event only
emits if the stream was not paused before sending the program to the
background.

Example of listening for `SIGCONT`:

rl.on('SIGCONT', function() {
// `prompt` will automatically resume the stream
rl.prompt();
});


Here's an example of how to use all these together to craft a tiny command
line interface:

var readline = require('readline'),
rl = readline.createInterface(process.stdin, process.stdout),
prefix = 'OHAI> ';
rl = readline.createInterface(process.stdin, process.stdout);

rl.setPrompt('OHAI> ');
rl.prompt();

rl.on('line', function(line) {
switch(line.trim()) {
Expand All @@ -122,18 +195,9 @@ line interface:
console.log('Say what? I might have heard `' + line.trim() + '`');
break;
}
rl.setPrompt(prefix, prefix.length);
rl.prompt();
}).on('close', function() {
}).on('pause', function() {
console.log('Have a great day!');
process.exit(0);
});
console.log(prefix + 'Good to see you. Try typing stuff.');
rl.setPrompt(prefix, prefix.length);
rl.prompt();


Take a look at this slightly more complicated
[example](https://gist.github.com/901104), and
[http-console](https://github.com/cloudhead/http-console) for a real-life use
case.
61 changes: 29 additions & 32 deletions lib/readline.js
Expand Up @@ -126,6 +126,7 @@ Interface.prototype.setPrompt = function(prompt, length) {


Interface.prototype.prompt = function(preserveCursor) {
if (this.paused) this.resume();
if (this.enabled) {
if (!preserveCursor) this.cursor = 0;
this._refreshLine();
Expand All @@ -136,16 +137,13 @@ Interface.prototype.prompt = function(preserveCursor) {


Interface.prototype.question = function(query, cb) {
if (cb) {
this.resume();
if (typeof cb === 'function') {
if (this._questionCallback) {
this.output.write('\n');
this.prompt();
} else {
this._oldPrompt = this._prompt;
this.setPrompt(query);
this._questionCallback = cb;
this.output.write('\n');
this.prompt();
}
}
Expand Down Expand Up @@ -181,8 +179,6 @@ Interface.prototype._addHistory = function() {


Interface.prototype._refreshLine = function() {
if (this._closed) return;

// Cursor to left edge.
this.output.cursorTo(0);

Expand All @@ -198,33 +194,29 @@ Interface.prototype._refreshLine = function() {
};


Interface.prototype.close = function(d) {
if (this._closing) return;
this._closing = true;
if (this.enabled) {
tty.setRawMode(false);
}
this.emit('close');
this._closed = true;
};


Interface.prototype.pause = function() {
if (this.paused) return;
if (this.enabled) {
tty.setRawMode(false);
}
this.input.pause();
this.paused = true;
this.emit('pause');
};


Interface.prototype.resume = function() {
this.input.resume();
if (this.enabled) {
tty.setRawMode(true);
}
this.paused = false;
this.emit('resume');
};


Interface.prototype.write = function(d, key) {
if (this._closed) return;
if (this.paused) this.resume();
this.enabled ? this._ttyWrite(d, key) : this._normalWrite(d, key);
};

Expand Down Expand Up @@ -454,16 +446,6 @@ Interface.prototype._historyPrev = function() {
};


Interface.prototype._attemptClose = function() {
if (this.listeners('attemptClose').length) {
// User is to call interface.close() manually.
this.emit('attemptClose');
} else {
this.close();
}
};


// handle a write from the tty
Interface.prototype._ttyWrite = function(s, key) {
var next_word, next_non_word, previous_word, previous_non_word;
Expand All @@ -489,8 +471,8 @@ Interface.prototype._ttyWrite = function(s, key) {
if (this.listeners('SIGINT').length) {
this.emit('SIGINT');
} else {
// default behavior, end the readline
this._attemptClose();
// Pause the stream
this.pause();
}
break;

Expand All @@ -500,7 +482,7 @@ Interface.prototype._ttyWrite = function(s, key) {

case 'd': // delete right or EOF
if (this.cursor === 0 && this.line.length === 0) {
this._attemptClose();
this.pause();
} else if (this.cursor < this.line.length) {
this._deleteRight();
}
Expand Down Expand Up @@ -549,7 +531,22 @@ Interface.prototype._ttyWrite = function(s, key) {
break;

case 'z':
process.kill(process.pid, 'SIGTSTP');
if (this.listeners('SIGTSTP').length) {
this.emit('SIGTSTP');
} else {
process.once('SIGCONT', (function(self) {
return function() {
// Don't raise events if stream has already been abandoned.
if (!self.paused) {
// Stream must be paused and resumed after SIGCONT to catch
// SIGINT, SIGTSTP, and EOF.
self.pause();
self.emit('SIGCONT');
}
};
})(this));
process.kill(process.pid, 'SIGTSTP');
}
return;

case 'w': // delete backwards to a word boundary
Expand Down
4 changes: 2 additions & 2 deletions lib/repl.js
Expand Up @@ -130,7 +130,7 @@ function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) {
var sawSIGINT = false;
rli.on('SIGINT', function() {
if (sawSIGINT) {
rli.close();
rli.pause();
process.exit();
}

Expand Down Expand Up @@ -733,7 +733,7 @@ function defineDefaultCommands(repl) {
repl.defineCommand('exit', {
help: 'Exit the repl',
action: function() {
this.rli.close();
this.rli.pause();
}
});

Expand Down