Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
d703d44f52 fix(readline): batch kRefreshLine writes into single write call
kRefreshLine previously made 4-7 separate stream.write() calls for
escape sequences and content during each line refresh. On Windows,
each write is flushed individually to the console, causing visible
flicker and cursor jumping. This batches all escape sequences and
line content into a single string written with one write() call.

Closes #27461

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 11:11:23 +00:00
2 changed files with 94 additions and 7 deletions

View File

@@ -1347,33 +1347,39 @@ var _Interface = class Interface extends InterfaceConstructor {
// cursor position
var cursorPos = this.getCursorPos();
// Build the entire output as a single string to avoid flicker
// from multiple individual write() calls (especially on Windows).
var data = "";
// First move to the bottom of the current line, based on cursor pos
var prevRows = this.prevRows || 0;
if (prevRows > 0) {
moveCursor(this.output, 0, -prevRows);
data += CSI`${prevRows}A`;
}
// Cursor to left edge.
cursorTo(this.output, 0);
data += CSI`${1}G`;
// erase data
clearScreenDown(this.output);
data += kClearScreenDown;
// Write the prompt and the current buffer content.
this[kWriteToOutput](line);
data += line;
// Force terminal to allocate a new line
if (lineCols === 0) {
this[kWriteToOutput](" ");
data += " ";
}
// Move cursor to original position.
cursorTo(this.output, cursorPos.cols);
data += CSI`${cursorPos.cols + 1}G`;
var diff = lineRows - cursorPos.rows;
if (diff > 0) {
moveCursor(this.output, 0, -diff);
data += CSI`${diff}A`;
}
this[kWriteToOutput](data);
this.prevRows = cursorPos.rows;
}

View File

@@ -0,0 +1,81 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/27461
// kRefreshLine should batch all escape sequences and content into a single
// write() call to avoid flicker and cursor jumping on Windows.
test("readline kRefreshLine batches output into a single write call", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const { Writable } = require("node:stream");
const readline = require("node:readline");
let writeCount = 0;
const chunks = [];
const output = new Writable({
write(chunk, encoding, callback) {
writeCount++;
chunks.push(chunk.toString());
callback();
},
});
output.columns = 80;
output.rows = 24;
const input = new (require("node:stream").PassThrough)();
const rl = readline.createInterface({
input,
output,
terminal: true,
prompt: "> ",
});
rl.prompt();
// Reset write count after the initial prompt
writeCount = 0;
chunks.length = 0;
// Set up some line content and trigger a refresh
rl.line = "hello world";
rl.cursor = 5;
rl._refreshLine();
// With the fix, all escape sequences and content should be batched
// into a single write() call instead of 4-7 separate calls.
if (writeCount !== 1) {
console.log("FAIL: expected 1 write call, got " + writeCount);
console.log("chunks: " + JSON.stringify(chunks));
process.exit(1);
}
// Verify the single write contains both escape sequences and content
const written = chunks[0];
if (!written.includes("> hello world")) {
console.log("FAIL: output missing prompt + line content");
process.exit(1);
}
if (!written.includes("\\x1b[")) {
console.log("FAIL: output missing escape sequences");
process.exit(1);
}
console.log("OK");
rl.close();
process.exit(0);
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("OK");
expect(stderr).not.toContain("FAIL");
expect(exitCode).toBe(0);
});