mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
fix(ssh-terminal): preserve input order during local echo reconciliation (#1505)
This commit is contained in:
1 parent
126a8c433b
commit
12a373e598
3 files changed
+90
-2
No files matched your search
@@ -24,13 +24,16 @@ export class TerminalLocalEcho {
|
|||||||
}
|
}
|
||||||
if (this.secretInput || !SAFE_INPUT.test(data)) return "";
|
if (this.secretInput || !SAFE_INPUT.test(data)) return "";
|
||||||
|
|
||||||
|
// Do not draw newer input ahead of characters still awaiting remote echo.
|
||||||
const predicted =
|
const predicted =
|
||||||
this.mode === "on" || (this.mode === "auto" && this.slowSamples >= 2);
|
(this.mode === "on" || (this.mode === "auto" && this.slowSamples >= 2)) &&
|
||||||
|
this.pending.every((item) => item.predicted);
|
||||||
this.pending.push({ value: data, sentAt: this.now(), predicted });
|
this.pending.push({ value: data, sentAt: this.now(), predicted });
|
||||||
return predicted ? data : "";
|
return predicted ? data : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOutput(data: string): string {
|
handleOutput(data: string): string {
|
||||||
|
if (!data) return "";
|
||||||
const plainOutput = this.stripAnsi(data);
|
const plainOutput = this.stripAnsi(data);
|
||||||
this.outputTail = (this.outputTail + plainOutput).slice(-128);
|
this.outputTail = (this.outputTail + plainOutput).slice(-128);
|
||||||
if (PASSWORD_PROMPT.test(this.outputTail)) {
|
if (PASSWORD_PROMPT.test(this.outputTail)) {
|
||||||
@@ -65,7 +68,8 @@ export class TerminalLocalEcho {
|
|||||||
if (consumed > 0) {
|
if (consumed > 0) {
|
||||||
const matched = this.pending.splice(0, consumed);
|
const matched = this.pending.splice(0, consumed);
|
||||||
const predictedCount = matched.filter((item) => item.predicted).length;
|
const predictedCount = matched.filter((item) => item.predicted).length;
|
||||||
return data.slice(predictedCount);
|
const rollback = consumed < data.length ? this.rollback() : "";
|
||||||
|
return rollback + data.slice(predictedCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.rollback() + data;
|
return this.rollback() + data;
|
||||||
|
|||||||
@@ -62,6 +62,37 @@ describe("TerminalLocalEcho", () => {
|
|||||||
expect(echo.handleInput("c")).toBe("c");
|
expect(echo.handleInput("c")).toBe("c");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("waits for outstanding unpredicted input before enabling auto prediction", () => {
|
||||||
|
let now = 0;
|
||||||
|
const echo = new TerminalLocalEcho("auto", () => now, 100);
|
||||||
|
expect(echo.handleInput("d")).toBe("");
|
||||||
|
now = 150;
|
||||||
|
expect(echo.handleOutput("d")).toBe("d");
|
||||||
|
expect(echo.handleInput("o")).toBe("");
|
||||||
|
expect(echo.handleInput("c")).toBe("");
|
||||||
|
now = 300;
|
||||||
|
expect(echo.handleOutput("o")).toBe("o");
|
||||||
|
expect(echo.handleInput("k")).toBe("");
|
||||||
|
expect(echo.handleOutput("ck")).toBe("ck");
|
||||||
|
expect(echo.handleInput("e")).toBe("e");
|
||||||
|
expect(echo.handleInput("r")).toBe("r");
|
||||||
|
expect(echo.handleOutput("er")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rolls back the unmatched suffix after a partially matching output chunk", () => {
|
||||||
|
const echo = new TerminalLocalEcho("on");
|
||||||
|
for (const character of "abc") echo.handleInput(character);
|
||||||
|
expect(echo.handleOutput("abZ")).toBe("\x1b[1D\x1b[1XZ");
|
||||||
|
expect(echo.handleOutput("c")).toBe("c");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps pending predictions across an empty output chunk", () => {
|
||||||
|
const echo = new TerminalLocalEcho("on");
|
||||||
|
expect(echo.handleInput("a")).toBe("a");
|
||||||
|
expect(echo.handleOutput("")).toBe("");
|
||||||
|
expect(echo.handleOutput("a")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
it("uses an explicit host mode before the global mode", () => {
|
it("uses an explicit host mode before the global mode", () => {
|
||||||
expect(resolveLocalEchoMode("off", "on")).toBe("off");
|
expect(resolveLocalEchoMode("off", "on")).toBe("off");
|
||||||
expect(resolveLocalEchoMode("default", "on")).toBe("on");
|
expect(resolveLocalEchoMode("default", "on")).toBe("on");
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Terminal } from "@xterm/xterm";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { TerminalLocalEcho } from "../../../src/frontend/lib/terminal-local-echo";
|
||||||
|
|
||||||
|
let terminal: Terminal | undefined;
|
||||||
|
afterEach(() => terminal?.dispose());
|
||||||
|
|
||||||
|
const write = (data: string) =>
|
||||||
|
new Promise<void>((resolve) => terminal!.write(data, resolve));
|
||||||
|
|
||||||
|
function line() {
|
||||||
|
return terminal!.buffer.active.getLine(0)?.translateToString(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("local echo with xterm", () => {
|
||||||
|
it("keeps fast typing in order when auto prediction activates with input in flight", async () => {
|
||||||
|
terminal = new Terminal({ cols: 80, rows: 5 });
|
||||||
|
let now = 0;
|
||||||
|
const echo = new TerminalLocalEcho("auto", () => now, 100);
|
||||||
|
const input = async (data: string) => {
|
||||||
|
for (const character of data) await write(echo.handleInput(character));
|
||||||
|
};
|
||||||
|
await input("d");
|
||||||
|
now = 150;
|
||||||
|
await write(echo.handleOutput("d"));
|
||||||
|
await input("oc");
|
||||||
|
now = 300;
|
||||||
|
await write(echo.handleOutput("o"));
|
||||||
|
await input("k");
|
||||||
|
await write(echo.handleOutput("ck"));
|
||||||
|
await input("er ps");
|
||||||
|
await write(echo.handleOutput("er ps"));
|
||||||
|
expect(line()).toBe("docker ps");
|
||||||
|
expect(terminal.buffer.active.cursorX).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["abZ", "ab\r\n$ "])(
|
||||||
|
"reconciles a matching prefix followed by remote output %j",
|
||||||
|
async (output) => {
|
||||||
|
terminal = new Terminal({ cols: 80, rows: 5 });
|
||||||
|
const echo = new TerminalLocalEcho("on");
|
||||||
|
for (const character of "abc") await write(echo.handleInput(character));
|
||||||
|
await write(echo.handleOutput(output));
|
||||||
|
expect(line()).toBe(output.startsWith("abZ") ? "abZ" : "ab");
|
||||||
|
if (output.includes("\n")) {
|
||||||
|
expect(terminal.buffer.active.getLine(1)?.translateToString(true)).toBe(
|
||||||
|
"$ ",
|
||||||
|
);
|
||||||
|
expect(terminal.buffer.active.cursorX).toBe(2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user