Skip to content
Merged
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
30 changes: 27 additions & 3 deletions doc/api/readline.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,16 +346,24 @@ async function processLineByLine() {
}
```

## readline.clearLine(stream, dir)
## readline.clearLine(stream, dir[, callback])
<!-- YAML
added: v0.7.7
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/28674
description: The stream's write() callback and return value are exposed.
-->

* `stream` {stream.Writable}
* `dir` {number}
* `-1` - to the left from cursor
* `1` - to the right from cursor
* `0` - the entire line
* `callback` {Function} Invoked once the operation completes.
* Returns: {boolean} `false` if `stream` wishes for the calling code to wait for
the `'drain'` event to be emitted before continuing to write additional data;
otherwise `true`.

The `readline.clearLine()` method clears current line of given [TTY][] stream
in a specified direction identified by `dir`.
Expand Down Expand Up @@ -479,14 +487,22 @@ function completer(linePartial, callback) {
}
```

## readline.cursorTo(stream, x, y)
## readline.cursorTo(stream, x, y[, callback])
<!-- YAML
added: v0.7.7
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/28674
description: The stream's write() callback and return value are exposed.
-->

* `stream` {stream.Writable}
* `x` {number}
* `y` {number}
* `callback` {Function} Invoked once the operation completes.
* Returns: {boolean} `false` if `stream` wishes for the calling code to wait for
the `'drain'` event to be emitted before continuing to write additional data;
otherwise `true`.

The `readline.cursorTo()` method moves cursor to the specified position in a
given [TTY][] `stream`.
Expand Down Expand Up @@ -517,14 +533,22 @@ if (process.stdin.isTTY)
process.stdin.setRawMode(true);
```

## readline.moveCursor(stream, dx, dy)
## readline.moveCursor(stream, dx, dy[, callback])
<!-- YAML
added: v0.7.7
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/28674
description: The stream's write() callback and return value are exposed.
-->

* `stream` {stream.Writable}
* `dx` {number}
* `dy` {number}
* `callback` {Function} Invoked once the operation completes.
* Returns: {boolean} `false` if `stream` wishes for the calling code to wait for
the `'drain'` event to be emitted before continuing to write additional data;
otherwise `true`.

The `readline.moveCursor()` method moves the cursor *relative* to its current
position in a given [TTY][] `stream`.
Expand Down
66 changes: 37 additions & 29 deletions lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -1189,42 +1189,52 @@ function emitKeypressEvents(stream, iface) {
* moves the cursor to the x and y coordinate on the given stream
*/

function cursorTo(stream, x, y) {
if (stream === null || stream === undefined)
return;
function cursorTo(stream, x, y, callback) {
if (callback !== undefined && typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK(callback);

if (typeof x !== 'number' && typeof y !== 'number')
return;
if (stream == null || (typeof x !== 'number' && typeof y !== 'number')) {
if (typeof callback === 'function')
process.nextTick(callback);
return true;
}

if (typeof x !== 'number')
throw new ERR_INVALID_CURSOR_POS();

if (typeof y !== 'number') {
stream.write(CSI`${x + 1}G`);
} else {
stream.write(CSI`${y + 1};${x + 1}H`);
}
const data = typeof y !== 'number' ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
return stream.write(data, callback);
}

/**
* moves the cursor relative to its current location
*/

function moveCursor(stream, dx, dy) {
if (stream === null || stream === undefined)
return;
function moveCursor(stream, dx, dy, callback) {
if (callback !== undefined && typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK(callback);

if (stream == null || !(dx || dy)) {
if (typeof callback === 'function')
process.nextTick(callback);
return true;
}

let data = '';

if (dx < 0) {
stream.write(CSI`${-dx}D`);
data += CSI`${-dx}D`;
} else if (dx > 0) {
stream.write(CSI`${dx}C`);
data += CSI`${dx}C`;
}

if (dy < 0) {
stream.write(CSI`${-dy}A`);
data += CSI`${-dy}A`;
} else if (dy > 0) {
stream.write(CSI`${dy}B`);
data += CSI`${dy}B`;
}

return stream.write(data, callback);
}

/**
Expand All @@ -1234,20 +1244,18 @@ function moveCursor(stream, dx, dy) {
* 0 for the entire line
*/

function clearLine(stream, dir) {
if (stream === null || stream === undefined)
return;
function clearLine(stream, dir, callback) {
if (callback !== undefined && typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK(callback);

if (dir < 0) {
// to the beginning
stream.write(kClearToBeginning);
} else if (dir > 0) {
// to the end
stream.write(kClearToEnd);
} else {
// entire line
stream.write(kClearLine);
if (stream === null || stream === undefined) {
if (typeof callback === 'function')
process.nextTick(callback);
return true;
}

const type = dir < 0 ? kClearToBeginning : dir > 0 ? kClearToEnd : kClearLine;
return stream.write(type, callback);
}

/**
Expand Down
64 changes: 54 additions & 10 deletions test/parallel/test-readline-csi.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,32 @@ assert.strictEqual(readline.clearScreenDown(undefined, common.mustCall()),
true);

writable.data = '';
readline.clearLine(writable, -1);
assert.strictEqual(readline.clearLine(writable, -1), true);
assert.deepStrictEqual(writable.data, CSI.kClearToBeginning);

writable.data = '';
readline.clearLine(writable, 1);
assert.strictEqual(readline.clearLine(writable, 1), true);
assert.deepStrictEqual(writable.data, CSI.kClearToEnd);

writable.data = '';
readline.clearLine(writable, 0);
assert.strictEqual(readline.clearLine(writable, 0), true);
assert.deepStrictEqual(writable.data, CSI.kClearLine);

writable.data = '';
assert.strictEqual(readline.clearLine(writable, -1, common.mustCall()), true);
assert.deepStrictEqual(writable.data, CSI.kClearToBeginning);

// Verify that clearLine() throws on invalid callback.
assert.throws(() => {
readline.clearLine(writable, 0, null);
}, /ERR_INVALID_CALLBACK/);

// Verify that clearLine() does not throw on null or undefined stream.
assert.strictEqual(readline.clearLine(null, 0), true);
assert.strictEqual(readline.clearLine(undefined, 0), true);
assert.strictEqual(readline.clearLine(null, 0, common.mustCall()), true);
assert.strictEqual(readline.clearLine(undefined, 0, common.mustCall()), true);

// Nothing is written when moveCursor 0, 0
[
[0, 0, ''],
Expand All @@ -68,20 +83,40 @@ assert.deepStrictEqual(writable.data, CSI.kClearLine);
[1, -1, '\x1b[1C\x1b[1A'],
].forEach((set) => {
writable.data = '';
readline.moveCursor(writable, set[0], set[1]);
assert.strictEqual(readline.moveCursor(writable, set[0], set[1]), true);
assert.deepStrictEqual(writable.data, set[2]);
writable.data = '';
assert.strictEqual(
readline.moveCursor(writable, set[0], set[1], common.mustCall()),
true
);
assert.deepStrictEqual(writable.data, set[2]);
});

// Verify that moveCursor() throws on invalid callback.
assert.throws(() => {
readline.moveCursor(writable, 1, 1, null);
}, /ERR_INVALID_CALLBACK/);

// Verify that moveCursor() does not throw on null or undefined stream.
assert.strictEqual(readline.moveCursor(null, 1, 1), true);
assert.strictEqual(readline.moveCursor(undefined, 1, 1), true);
assert.strictEqual(readline.moveCursor(null, 1, 1, common.mustCall()), true);
assert.strictEqual(readline.moveCursor(undefined, 1, 1, common.mustCall()),
true);

// Undefined or null as stream should not throw.
readline.cursorTo(null);
readline.cursorTo();
assert.strictEqual(readline.cursorTo(null), true);
assert.strictEqual(readline.cursorTo(), true);
assert.strictEqual(readline.cursorTo(null, 1, 1, common.mustCall()), true);
assert.strictEqual(readline.cursorTo(undefined, 1, 1, common.mustCall()), true);

writable.data = '';
readline.cursorTo(writable, 'a');
assert.strictEqual(readline.cursorTo(writable, 'a'), true);
assert.strictEqual(writable.data, '');

writable.data = '';
readline.cursorTo(writable, 'a', 'b');
assert.strictEqual(readline.cursorTo(writable, 'a', 'b'), true);
assert.strictEqual(writable.data, '');

writable.data = '';
Expand All @@ -95,9 +130,18 @@ common.expectsError(
assert.strictEqual(writable.data, '');

writable.data = '';
readline.cursorTo(writable, 1, 'a');
assert.strictEqual(readline.cursorTo(writable, 1, 'a'), true);
assert.strictEqual(writable.data, '\x1b[2G');

writable.data = '';
readline.cursorTo(writable, 1, 2);
assert.strictEqual(readline.cursorTo(writable, 1, 2), true);
assert.strictEqual(writable.data, '\x1b[3;2H');

writable.data = '';
assert.strictEqual(readline.cursorTo(writable, 1, 2, common.mustCall()), true);
assert.strictEqual(writable.data, '\x1b[3;2H');

// Verify that cursorTo() throws on invalid callback.
assert.throws(() => {
readline.cursorTo(writable, 1, 1, null);
}, /ERR_INVALID_CALLBACK/);