Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions doc/api/readline.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@ location at which to provide input.
When called, `rl.prompt()` will resume the `input` stream if it has been
paused.

To show the prompt on each new line of input, call `rl.prompt()` from your
`'line'` event listener (the built-in REPL does this after evaluating each
line).

If the `InterfaceConstructor` was created with `output` set to `null` or
`undefined` the prompt is not written.

Expand Down Expand Up @@ -710,6 +714,9 @@ added: v17.0.0
to the history list duplicates an older one, this removes the older line
from the list. **Default:** `false`.
* `prompt` {string} The prompt string to use. **Default:** `'> '`.
For TTY interfaces, the default prompt is not written when the line is
redrawn until `rl.prompt()` has been called at least once (see
[`rl.prompt()`][]). A non-default `prompt` is written on redraw as before.
* `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. `crlfDelay` will be coerced to a number no less than
Expand Down Expand Up @@ -975,6 +982,9 @@ changes:
to the history list duplicates an older one, this removes the older line
from the list. **Default:** `false`.
* `prompt` {string} The prompt string to use. **Default:** `'> '`.
For TTY interfaces, the default prompt is not written when the line is
redrawn until `rl.prompt()` has been called at least once (see
[`rl.prompt()`][]). A non-default `prompt` is written on redraw as before.
* `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. `crlfDelay` will be coerced to a number no less than
Expand Down Expand Up @@ -1492,4 +1502,5 @@ const { createInterface } = require('node:readline');
[`process.stdin`]: process.md#processstdin
[`process.stdout`]: process.md#processstdout
[`rl.close()`]: #rlclose
[`rl.prompt()`]: #rlpromptpreservecursor
[reading files]: #example-read-file-stream-line-by-line
44 changes: 39 additions & 5 deletions lib/internal/readline/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@

const kMultilinePrompt = Symbol('| ');

const kDefaultPrompt = '> ';

const kAddHistory = Symbol('_addHistory');
const kBeforeEdit = Symbol('_beforeEdit');
const kDecoder = Symbol('_decoder');
Expand All @@ -125,6 +127,8 @@
const kSetLine = Symbol('_setLine');
const kPreviousKey = Symbol('_previousKey');
const kPrompt = Symbol('_prompt');
const kPromptInvoked = Symbol('_promptInvoked');
const kEffectivePrompt = Symbol('_effectivePrompt');
const kPushToKillRing = Symbol('_pushToKillRing');
const kPushToUndoStack = Symbol('_pushToUndoStack');
const kQuestionCallback = Symbol('_questionCallback');
Expand Down Expand Up @@ -170,7 +174,7 @@
FunctionPrototypeCall(EventEmitter, this);

let crlfDelay;
let prompt = '> ';
let prompt = kDefaultPrompt;
let signal;

if (input?.input) {
Expand Down Expand Up @@ -252,6 +256,7 @@
this.completer = completer;

this.setPrompt(prompt);
this[kPromptInvoked] = false;

this.terminal = !!terminal;

Expand Down Expand Up @@ -353,6 +358,13 @@
this[kSetLine]('');

input.resume();

// If the default prompt prompt is used and the terminal is active, the prompt is automatically displayed.
if (prompt === kDefaultPrompt && this.terminal && output !== null && output !== undefined) {

Check failure on line 363 in lib/internal/readline/interface.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Trailing spaces not allowed
process.nextTick(() => {
this.prompt();
});
}
}

ObjectSetPrototypeOf(InterfaceConstructor.prototype, EventEmitter.prototype);
Expand Down Expand Up @@ -428,6 +440,7 @@
*/
prompt(preserveCursor) {
if (this.paused) this.resume();
this[kPromptInvoked] = true;
if (this.terminal && process.env.TERM !== 'dumb') {
if (!preserveCursor) this.cursor = 0;
this[kRefreshLine]();
Expand Down Expand Up @@ -463,6 +476,9 @@
cb(line);
} else {
this.emit('line', line);
if (this[kPrompt] === kDefaultPrompt && this.terminal && this.output !== null && this.output !== undefined) {

Check failure on line 479 in lib/internal/readline/interface.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Trailing spaces not allowed
this.prompt();

Check failure on line 480 in lib/internal/readline/interface.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Trailing spaces not allowed
}

Check failure on line 481 in lib/internal/readline/interface.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Trailing spaces not allowed
}
}

Expand Down Expand Up @@ -490,9 +506,20 @@
return this.historyManager.addHistory(this[kIsMultiline], this[kLastCommandErrored]);
}

[kEffectivePrompt]() {
if (this[kPromptInvoked]) {
return this[kPrompt];
}
// Issue #12606: default prompt is not painted until `prompt()`, but
// non-default prompts (including multi-line prompts) participate in
// layout/cursor math the same as before.
return this[kPrompt] === kDefaultPrompt ? '' : this[kPrompt];
}

[kRefreshLine]() {
// line length
const line = this[kPrompt] + this.line;
const promptPrefix = this[kEffectivePrompt]();
const line = promptPrefix + this.line;
const dispPos = this[kGetDisplayPos](line);
const lineCols = dispPos.cols;
const lineRows = dispPos.rows;
Expand All @@ -514,7 +541,7 @@
if (this[kIsMultiline]) {
const lines = StringPrototypeSplit(this.line, '\n');
// Write first line with normal prompt
this[kWriteToOutput](this[kPrompt] + lines[0]);
this[kWriteToOutput](promptPrefix + lines[0]);

// For continuation lines, add the "|" prefix
for (let i = 1; i < lines.length; i++) {
Expand Down Expand Up @@ -720,6 +747,10 @@
return;
}

// Tab completion redraws the whole line; treat it like `prompt()` for the
// purpose of painting the default prompt (see #12606).
this[kPromptInvoked] = true;

// If there is a common prefix to all matches, then apply that portion.
const prefix = commonPrefix(
ArrayPrototypeFilter(completions, (e) => e !== ''),
Expand Down Expand Up @@ -1020,7 +1051,9 @@
}

if (needsRewriteFirstLine) {
this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${kMultilinePrompt.description}`);
this[kWriteToOutput](
`${this[kEffectivePrompt]()}${beforeCursor}\n${kMultilinePrompt.description}`,
);
} else {
this[kWriteToOutput](kMultilinePrompt.description);
}
Expand Down Expand Up @@ -1221,7 +1254,8 @@
* }}
*/
getCursorPos() {
const strBeforeCursor = this[kPrompt] + StringPrototypeSlice(this.line, 0, this.cursor);
const strBeforeCursor = this[kEffectivePrompt]() +
StringPrototypeSlice(this.line, 0, this.cursor);

return this[kGetDisplayPos](strBeforeCursor);
}
Expand Down
52 changes: 52 additions & 0 deletions test/parallel/test-readline-interface.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
Expand Down Expand Up @@ -1271,6 +1271,58 @@
}
}

// Do not paint the configured prompt on refresh until prompt() is called.
// https://github.com/nodejs/node/issues/12606
if (terminal) {
const fi = new FakeInput();
fi.isTTY = true;
fi.columns = 80;
const output = [];
fi.write = (chunk) => {
output.push(chunk.toString());
return true;
};

const rli = readline.createInterface({
input: fi,
output: fi,
terminal: true,
});

rli.write('a');
output.length = 0;
rli.write(undefined, { name: 'backspace' });
assert.strictEqual(output.join('').includes('> '), false);
rli.close();
}

// gh-12606: redraw each new line by calling `prompt()` from `'line'` (REPL pattern).
if (terminal) {
const fi = new FakeInput();
fi.isTTY = true;
fi.columns = 80;
const output = [];
fi.write = (chunk) => {
output.push(chunk.toString());
return true;
};

const rli = readline.createInterface({
input: fi,
output: fi,
terminal: true,
});

rli.prompt(false);
rli.on('line', () => {
rli.prompt(false);
});
output.length = 0;
fi.emit('data', 'x\n');
assert.strictEqual(output.join('').includes('> '), true);
rli.close();
}

{
const expected = terminal ?
['\u001b[1G', '\u001b[0J', '$ ', '\u001b[3G'] :
Expand Down
Loading