Fix terminal DSR replies
This commit is contained in:
@@ -999,6 +999,33 @@ process.stdin.on("data", (chunk) => {
|
|||||||
process.stdout.write("\\x1b[c");
|
process.stdout.write("\\x1b[c");
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const DSR_HELPER_SCRIPT = `process.stdin.setRawMode(true);
|
||||||
|
process.stdin.resume();
|
||||||
|
const mode = process.argv[2] || "cursor";
|
||||||
|
const query = mode === "private-cursor" ? "\\x1b[?6n" : mode === "status" ? "\\x1b[5n" : "\\x1b[6n";
|
||||||
|
const pattern = mode === "private-cursor" ? /\\x1b\\[\\?(\\d+);(\\d+)R/ : mode === "status" ? /\\x1b\\[0n/ : /\\x1b\\[(\\d+);(\\d+)R/;
|
||||||
|
let buf = "";
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
process.stdout.write("DSR_TIMEOUT\\n");
|
||||||
|
process.exit(2);
|
||||||
|
}, 2500);
|
||||||
|
process.stdin.on("data", (chunk) => {
|
||||||
|
buf += chunk.toString("binary");
|
||||||
|
const match = buf.match(pattern);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (mode === "status") {
|
||||||
|
process.stdout.write("DSR_OK:status\\n");
|
||||||
|
} else {
|
||||||
|
process.stdout.write("DSR_OK:" + match[1] + ":" + match[2] + "\\n");
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
process.stdout.write(query);
|
||||||
|
`;
|
||||||
|
|
||||||
function writeDaHelper(prefix: string): string {
|
function writeDaHelper(prefix: string): string {
|
||||||
const dir = mkdtempSync(join(tmpdir(), prefix));
|
const dir = mkdtempSync(join(tmpdir(), prefix));
|
||||||
temporaryDirs.push(dir);
|
temporaryDirs.push(dir);
|
||||||
@@ -1007,14 +1034,30 @@ function writeDaHelper(prefix: string): string {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeDsrHelper(prefix: string): string {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), prefix));
|
||||||
|
temporaryDirs.push(dir);
|
||||||
|
const path = join(dir, "helper.cjs");
|
||||||
|
writeFileSync(path, DSR_HELPER_SCRIPT);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
function isDaOkLine(line: string): boolean {
|
function isDaOkLine(line: string): boolean {
|
||||||
return line.startsWith("DA_OK:");
|
return line.startsWith("DA_OK:");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDsrOkLine(line: string): boolean {
|
||||||
|
return line.startsWith("DSR_OK:");
|
||||||
|
}
|
||||||
|
|
||||||
function hasDaOkLine(state: ReturnType<TerminalSession["getState"]>): boolean {
|
function hasDaOkLine(state: ReturnType<TerminalSession["getState"]>): boolean {
|
||||||
return getLines(state).some(isDaOkLine);
|
return getLines(state).some(isDaOkLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasDsrOkLine(state: ReturnType<TerminalSession["getState"]>): boolean {
|
||||||
|
return getLines(state).some(isDsrOkLine);
|
||||||
|
}
|
||||||
|
|
||||||
function lastNonEmptyLineIsPrompt(state: ReturnType<TerminalSession["getState"]>): boolean {
|
function lastNonEmptyLineIsPrompt(state: ReturnType<TerminalSession["getState"]>): boolean {
|
||||||
const last =
|
const last =
|
||||||
getLines(state)
|
getLines(state)
|
||||||
@@ -1059,6 +1102,63 @@ describe("terminal protocol queries", () => {
|
|||||||
await waitForState(session, hasDaOkLine);
|
await waitForState(session, hasDaOkLine);
|
||||||
await waitForState(session, lastNonEmptyLineIsPrompt);
|
await waitForState(session, lastNonEmptyLineIsPrompt);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("delivers public DSR cursor-position replies to a foreground app on stdin", async () => {
|
||||||
|
const helperPath = writeDsrHelper("terminal-dsr-helper-");
|
||||||
|
|
||||||
|
const session = trackSession(
|
||||||
|
await createTerminal({
|
||||||
|
cwd: "/tmp",
|
||||||
|
shell: "/bin/sh",
|
||||||
|
env: { PS1: "$ " },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await waitForLines(session, ["$"]);
|
||||||
|
|
||||||
|
session.send({ type: "input", data: `${process.execPath} ${helperPath} cursor\r` });
|
||||||
|
await waitForState(session, hasDsrOkLine);
|
||||||
|
|
||||||
|
const ack = getLines(session.getState()).find(isDsrOkLine) ?? "";
|
||||||
|
expect(ack).toMatch(/^DSR_OK:\d+:\d+$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delivers private DSR cursor-position replies to a foreground app on stdin", async () => {
|
||||||
|
const helperPath = writeDsrHelper("terminal-dsr-private-helper-");
|
||||||
|
|
||||||
|
const session = trackSession(
|
||||||
|
await createTerminal({
|
||||||
|
cwd: "/tmp",
|
||||||
|
shell: "/bin/sh",
|
||||||
|
env: { PS1: "$ " },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await waitForLines(session, ["$"]);
|
||||||
|
|
||||||
|
session.send({ type: "input", data: `${process.execPath} ${helperPath} private-cursor\r` });
|
||||||
|
await waitForState(session, hasDsrOkLine);
|
||||||
|
|
||||||
|
const ack = getLines(session.getState()).find(isDsrOkLine) ?? "";
|
||||||
|
expect(ack).toMatch(/^DSR_OK:\d+:\d+$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delivers DSR terminal-status replies to a foreground app on stdin", async () => {
|
||||||
|
const helperPath = writeDsrHelper("terminal-dsr-status-helper-");
|
||||||
|
|
||||||
|
const session = trackSession(
|
||||||
|
await createTerminal({
|
||||||
|
cwd: "/tmp",
|
||||||
|
shell: "/bin/sh",
|
||||||
|
env: { PS1: "$ " },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await waitForLines(session, ["$"]);
|
||||||
|
|
||||||
|
session.send({ type: "input", data: `${process.execPath} ${helperPath} status\r` });
|
||||||
|
await waitForState(session, hasDsrOkLine);
|
||||||
|
|
||||||
|
const ack = getLines(session.getState()).find(isDsrOkLine) ?? "";
|
||||||
|
expect(ack).toBe("DSR_OK:status");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("stream snapshots", () => {
|
describe("stream snapshots", () => {
|
||||||
|
|||||||
@@ -670,6 +670,29 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
terminal.parser.registerCsiHandler({ final: "n" }, (params) => {
|
||||||
|
if (params.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (params[0] === 5) {
|
||||||
|
ptyProcess.write("\x1b[0n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (params[0] === 6) {
|
||||||
|
const buffer = terminal.buffer.active;
|
||||||
|
ptyProcess.write(`\x1b[${buffer.cursorY + 1};${buffer.cursorX + 1}R`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
terminal.parser.registerCsiHandler({ prefix: "?", final: "n" }, (params) => {
|
||||||
|
if (params.length !== 1 || params[0] !== 6) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const buffer = terminal.buffer.active;
|
||||||
|
ptyProcess.write(`\x1b[?${buffer.cursorY + 1};${buffer.cursorX + 1}R`);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
let disposeTitleChangeSubscription: { dispose(): void } | null = null;
|
let disposeTitleChangeSubscription: { dispose(): void } | null = null;
|
||||||
if (!lockedTitle) {
|
if (!lockedTitle) {
|
||||||
|
|||||||
Reference in New Issue
Block a user