forked from Mirror/pi
fix(tui): apply PI_TUI_ESC_TIMEOUT only to lone ESC
Split StdinBuffer's incomplete-sequence wait from the lone-ESC wait so SSH and PI_TUI_ESC_TIMEOUT can reassemble split Alt+Enter without delaying Escape or mouse/CSI buffering.
This commit is contained in:
@@ -87,7 +87,7 @@ These variables are read by Pi itself:
|
||||
| `PI_CACHE_RETENTION` | Set to `long` for extended provider prompt caching where supported |
|
||||
| `PI_SHARE_VIEWER_URL` | Override the base URL used by `/share` |
|
||||
| `PI_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor; see [Terminal setup](terminal-setup.md) |
|
||||
| `PI_TUI_ESC_TIMEOUT` | Escape-sequence timeout in milliseconds; defaults to `100` over SSH and `10` otherwise. Increase if Alt-key input is misread as Escape |
|
||||
| `PI_TUI_ESC_TIMEOUT` | How long to wait after a lone ESC before treating it as Escape, in milliseconds; defaults to `100` over SSH and `10` otherwise. Increase if Alt-key input is misread as Escape |
|
||||
| `VISUAL`, `EDITOR` | External editor fallback when `externalEditor` is unset |
|
||||
| `HTTP_PROXY`, `HTTPS_PROXY` | Proxy outbound HTTP requests |
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
const ESC = "\x1b";
|
||||
const LONE_ESCAPE_TIMEOUT_MS = 10;
|
||||
const DEFAULT_SEQUENCE_TIMEOUT_MS = 50;
|
||||
const DEFAULT_ESCAPE_TIMEOUT_MS = 10;
|
||||
const BRACKETED_PASTE_START = "\x1b[200~";
|
||||
const BRACKETED_PASTE_END = "\x1b[201~";
|
||||
|
||||
@@ -257,10 +258,15 @@ function extractCompleteSequences(buffer: string): { sequences: string[]; remain
|
||||
|
||||
export type StdinBufferOptions = {
|
||||
/**
|
||||
* Maximum time to wait for sequence completion (default: 50ms).
|
||||
* A lone Escape uses at most 10ms to preserve keyboard responsiveness.
|
||||
* Maximum time to wait for an incomplete sequence such as CSI or mouse
|
||||
* (default: 50ms).
|
||||
*/
|
||||
timeout?: number;
|
||||
/**
|
||||
* Maximum time to wait after a lone ESC before treating it as Escape
|
||||
* (default: 10ms). Increase for high-latency Alt+key input (SSH).
|
||||
*/
|
||||
escapeTimeout?: number;
|
||||
};
|
||||
|
||||
export type StdinBufferEventMap = {
|
||||
@@ -276,13 +282,15 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
||||
private buffer: string = "";
|
||||
private timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private readonly timeoutMs: number;
|
||||
private readonly escapeTimeoutMs: number;
|
||||
private pasteMode: boolean = false;
|
||||
private pasteBuffer: string = "";
|
||||
private pendingKittyPrintableCodepoint: number | undefined;
|
||||
|
||||
constructor(options: StdinBufferOptions = {}) {
|
||||
super();
|
||||
this.timeoutMs = options.timeout ?? 50;
|
||||
this.timeoutMs = options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT_MS;
|
||||
this.escapeTimeoutMs = options.escapeTimeout ?? DEFAULT_ESCAPE_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
public process(data: string | Buffer): void {
|
||||
@@ -377,7 +385,7 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
||||
}
|
||||
|
||||
if (this.buffer.length > 0) {
|
||||
const timeoutMs = this.buffer === ESC ? Math.min(this.timeoutMs, LONE_ESCAPE_TIMEOUT_MS) : this.timeoutMs;
|
||||
const timeoutMs = this.buffer === ESC ? this.escapeTimeoutMs : this.timeoutMs;
|
||||
this.timeout = setTimeout(() => {
|
||||
const flushed = this.flush();
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ export class ProcessTerminal implements Terminal {
|
||||
* to handle the case where the response arrives split across multiple events.
|
||||
*/
|
||||
private setupStdinBuffer(): void {
|
||||
this.stdinBuffer = new StdinBuffer({ timeout: resolveEscapeTimeoutMs() });
|
||||
this.stdinBuffer = new StdinBuffer({ escapeTimeout: resolveEscapeTimeoutMs() });
|
||||
|
||||
// Forward individual sequences to the input handler
|
||||
this.stdinBuffer.on("data", (sequence) => {
|
||||
|
||||
@@ -149,6 +149,21 @@ describe("StdinBuffer", () => {
|
||||
});
|
||||
|
||||
it("should merge ESC + CR split across chunks within a larger timeout", async () => {
|
||||
buffer = new StdinBuffer({ escapeTimeout: 100 });
|
||||
emittedSequences = [];
|
||||
buffer.on("data", (sequence) => {
|
||||
emittedSequences.push(sequence);
|
||||
});
|
||||
|
||||
processInput("\x1b");
|
||||
await wait(20); // > 10ms default escapeTimeout, < 100ms configured escapeTimeout
|
||||
processInput("\r");
|
||||
|
||||
assert.deepStrictEqual(emittedSequences, ["\x1b\r"]);
|
||||
assert.equal(matchesKey(emittedSequences[0] ?? "", "alt+enter"), true);
|
||||
});
|
||||
|
||||
it("does not apply the sequence timeout to a lone ESC", async () => {
|
||||
buffer = new StdinBuffer({ timeout: 100 });
|
||||
emittedSequences = [];
|
||||
buffer.on("data", (sequence) => {
|
||||
@@ -156,11 +171,11 @@ describe("StdinBuffer", () => {
|
||||
});
|
||||
|
||||
processInput("\x1b");
|
||||
await wait(20); // > 10ms old default, < 100ms configured timeout
|
||||
await wait(20);
|
||||
processInput("\r");
|
||||
|
||||
assert.deepStrictEqual(emittedSequences, ["\x1b\r"]);
|
||||
assert.equal(matchesKey(emittedSequences[0] ?? "", "alt+enter"), true);
|
||||
assert.deepStrictEqual(emittedSequences, ["\x1b", "\r"]);
|
||||
assert.equal(matchesKey(emittedSequences[0] ?? "", "escape"), true);
|
||||
});
|
||||
|
||||
it("keeps fragmented mouse sequences buffered across delayed chunks by default", async () => {
|
||||
|
||||
@@ -223,7 +223,7 @@ describe("ProcessTerminal Kitty keyboard protocol negotiation", () => {
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send("\x1b[");
|
||||
mock.timers.tick(resolveEscapeTimeoutMs());
|
||||
mock.timers.tick(50); // StdinBuffer sequence timeout, not the lone-ESC timeout
|
||||
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user