chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
106
third_party/pi/packages/tui/src/terminal.ts
vendored
106
third_party/pi/packages/tui/src/terminal.ts
vendored
@@ -1,20 +1,24 @@
|
||||
import * as fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import * as path from "node:path";
|
||||
import { setKittyProtocolActive } from "./keys.ts";
|
||||
import { isNativeModifierPressed } from "./native-modifiers.ts";
|
||||
import { getNativeModuleCandidates } from "./native-module-path.ts";
|
||||
import { getNativePlatformHelper } from "./native-platform.ts";
|
||||
import {
|
||||
formatProgramStatus,
|
||||
isProgramStatusReply,
|
||||
PROGRAM_STATUS_QUERY,
|
||||
type ProgramStatus,
|
||||
} from "./program-status.ts";
|
||||
import { StdinBuffer } from "./stdin-buffer.ts";
|
||||
|
||||
const cjsRequire = createRequire(import.meta.url);
|
||||
|
||||
const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
|
||||
const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
|
||||
const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0\x07";
|
||||
const NATIVE_SHIFT_ENTER_SEQUENCE = "\x1b[13;2u";
|
||||
const DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS = 7;
|
||||
const KEYBOARD_PROTOCOL_RESPONSE_FRAGMENT_TIMEOUT_MS = 150;
|
||||
const KITTY_KEYBOARD_PROTOCOL_QUERY = `\x1b[>${DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS}u\x1b[?u\x1b[c`;
|
||||
const KITTY_KEYBOARD_PROTOCOL_QUERY = `\x1b[>${DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS}u\x1b[?u`;
|
||||
const DEVICE_ATTRIBUTES_QUERY = "\x1b[c";
|
||||
|
||||
export type KeyboardProtocolNegotiationSequence =
|
||||
| { type: "kitty-flags"; flags: number }
|
||||
@@ -113,6 +117,12 @@ export interface Terminal {
|
||||
|
||||
// Progress indicator (OSC 9;4)
|
||||
setProgress(active: boolean): void;
|
||||
|
||||
/**
|
||||
* Report what the program is doing (OSC 7501). Sent only to terminals that support it; the latest
|
||||
* status is re-sent when support is confirmed or the terminal restarts.
|
||||
*/
|
||||
setProgramStatus(status: ProgramStatus): void;
|
||||
}
|
||||
|
||||
const DEFAULT_ESCAPE_TIMEOUT_MS = 10;
|
||||
@@ -144,11 +154,19 @@ export class ProcessTerminal implements Terminal {
|
||||
private _kittyProtocolActive = false;
|
||||
private _modifyOtherKeysActive = false;
|
||||
private keyboardProtocolPushed = false;
|
||||
/** DA1 replies owed to keyboard protocol queries. Later DA1 replies answer other queries and are forwarded. */
|
||||
private pendingKeyboardProtocolDeviceAttributes = 0;
|
||||
private keyboardProtocolNegotiationBuffer = "";
|
||||
private keyboardProtocolBufferFlushTimer?: ReturnType<typeof setTimeout>;
|
||||
private stdinBuffer?: StdinBuffer;
|
||||
private stdinDataHandler?: (data: string) => void;
|
||||
private progressInterval?: ReturnType<typeof setInterval>;
|
||||
/** Latest program status, kept across stop() so start() can report it again. */
|
||||
private programStatus?: ProgramStatus;
|
||||
/** Whether the terminal confirmed OSC 7501 support since the last start(), or `PI_PROGRAM_STATUS=1`. */
|
||||
private programStatusSupported = false;
|
||||
/** The support query was sent and its DA1 sentinel has not arrived yet. */
|
||||
private programStatusQueryPending = false;
|
||||
private writeLogPath = (() => {
|
||||
const env = process.env.PI_TUI_WRITE_LOG || "";
|
||||
if (!env) return "";
|
||||
@@ -218,16 +236,24 @@ export class ProcessTerminal implements Terminal {
|
||||
|
||||
// Forward individual sequences to the input handler
|
||||
this.stdinBuffer.on("data", (sequence) => {
|
||||
const negotiationSequence = this.readKeyboardProtocolNegotiationSequence(sequence);
|
||||
if (negotiationSequence === "pending") {
|
||||
if (isProgramStatusReply(sequence)) {
|
||||
if (this.programStatusQueryPending) {
|
||||
this.programStatusQueryPending = false;
|
||||
this.programStatusSupported = true;
|
||||
this.writeProgramStatus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const negotiation = this.readKeyboardProtocolNegotiationSequence(sequence);
|
||||
if (negotiation === "pending") {
|
||||
this.scheduleKeyboardProtocolNegotiationBufferFlush();
|
||||
return; // Wait briefly for the rest of a split Kitty response.
|
||||
}
|
||||
if (this.handleKeyboardProtocolNegotiationSequence(negotiationSequence)) {
|
||||
if (negotiation && this.handleKeyboardProtocolNegotiationSequence(negotiation.parsed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.forwardInputSequence(sequence);
|
||||
this.forwardInputSequence(negotiation?.sequence ?? sequence);
|
||||
});
|
||||
|
||||
// Re-wrap paste content with bracketed paste markers for existing editor handling
|
||||
@@ -255,20 +281,35 @@ export class ProcessTerminal implements Terminal {
|
||||
* - 1 = disambiguate escape codes
|
||||
* - 2 = report event types (press/repeat/release)
|
||||
* - 4 = report alternate keys (shifted key, base layout key)
|
||||
*
|
||||
* The OSC 7501 program status query shares the DA sentinel: a terminal that supports it replies
|
||||
* before DA. `PI_PROGRAM_STATUS=1` or `0` skips the query.
|
||||
*/
|
||||
private queryAndEnableKittyProtocol(): void {
|
||||
this.setupStdinBuffer();
|
||||
process.stdin.on("data", this.stdinDataHandler!);
|
||||
this.keyboardProtocolPushed = true;
|
||||
this.pendingKeyboardProtocolDeviceAttributes += 1;
|
||||
this.clearKeyboardProtocolNegotiationBuffer();
|
||||
process.stdout.write(KITTY_KEYBOARD_PROTOCOL_QUERY);
|
||||
const programStatusOverride = process.env.PI_PROGRAM_STATUS;
|
||||
this.programStatusSupported = programStatusOverride === "1";
|
||||
this.programStatusQueryPending = programStatusOverride !== "1" && programStatusOverride !== "0";
|
||||
const programStatusQuery = this.programStatusQueryPending ? PROGRAM_STATUS_QUERY : "";
|
||||
process.stdout.write(`${KITTY_KEYBOARD_PROTOCOL_QUERY}${programStatusQuery}${DEVICE_ATTRIBUTES_QUERY}`);
|
||||
this.writeProgramStatus();
|
||||
}
|
||||
|
||||
private handleKeyboardProtocolNegotiationSequence(
|
||||
negotiationSequence: KeyboardProtocolNegotiationSequence | undefined,
|
||||
negotiationSequence: KeyboardProtocolNegotiationSequence,
|
||||
): boolean {
|
||||
if (!negotiationSequence) return false;
|
||||
this.clearKeyboardProtocolNegotiationBuffer();
|
||||
if (negotiationSequence.type === "device-attributes") {
|
||||
if (this.pendingKeyboardProtocolDeviceAttributes === 0) return false;
|
||||
this.pendingKeyboardProtocolDeviceAttributes -= 1;
|
||||
// The last owed DA answers the latest query, which got no program status reply first. Earlier DA
|
||||
// replies belong to queries from before a restart.
|
||||
if (this.pendingKeyboardProtocolDeviceAttributes === 0) this.programStatusQueryPending = false;
|
||||
}
|
||||
if (negotiationSequence.type === "kitty-flags") {
|
||||
if (negotiationSequence.flags !== 0) {
|
||||
this.disableModifyOtherKeys();
|
||||
@@ -288,15 +329,16 @@ export class ProcessTerminal implements Terminal {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns the parsed negotiation reply with its full (possibly reassembled) sequence. */
|
||||
private readKeyboardProtocolNegotiationSequence(
|
||||
sequence: string,
|
||||
): KeyboardProtocolNegotiationSequence | "pending" | undefined {
|
||||
): { parsed: KeyboardProtocolNegotiationSequence; sequence: string } | "pending" | undefined {
|
||||
if (this.keyboardProtocolNegotiationBuffer) {
|
||||
const bufferedSequence = this.keyboardProtocolNegotiationBuffer + sequence;
|
||||
const negotiationSequence = parseKeyboardProtocolNegotiationSequence(bufferedSequence);
|
||||
if (negotiationSequence) {
|
||||
this.clearKeyboardProtocolNegotiationBuffer();
|
||||
return negotiationSequence;
|
||||
return { parsed: negotiationSequence, sequence: bufferedSequence };
|
||||
}
|
||||
if (isKeyboardProtocolNegotiationSequencePrefix(bufferedSequence)) {
|
||||
this.setKeyboardProtocolNegotiationBuffer(bufferedSequence);
|
||||
@@ -306,7 +348,7 @@ export class ProcessTerminal implements Terminal {
|
||||
}
|
||||
|
||||
const negotiationSequence = parseKeyboardProtocolNegotiationSequence(sequence);
|
||||
if (negotiationSequence) return negotiationSequence;
|
||||
if (negotiationSequence) return { parsed: negotiationSequence, sequence };
|
||||
if (isKeyboardProtocolNegotiationSequencePrefix(sequence)) {
|
||||
this.setKeyboardProtocolNegotiationBuffer(sequence);
|
||||
return "pending";
|
||||
@@ -378,22 +420,7 @@ export class ProcessTerminal implements Terminal {
|
||||
private enableWindowsVTInput(): void {
|
||||
if (process.platform !== "win32") return;
|
||||
try {
|
||||
const arch = process.arch;
|
||||
if (arch !== "x64" && arch !== "arm64") return;
|
||||
|
||||
// Dynamic require so non-Windows and bundled/browser paths never load the
|
||||
// native helper. Installed packages resolve it from pi-tui; standalone
|
||||
// binaries resolve the copy next to the executable.
|
||||
const nativePath = path.join("native", "win32", "prebuilds", `win32-${arch}`, "win32-console-mode.node");
|
||||
for (const modulePath of getNativeModuleCandidates(nativePath)) {
|
||||
try {
|
||||
const helper = cjsRequire(modulePath) as { enableVirtualTerminalInput?: () => boolean };
|
||||
helper.enableVirtualTerminalInput?.();
|
||||
return;
|
||||
} catch {
|
||||
// Try the next possible packaging location.
|
||||
}
|
||||
}
|
||||
getNativePlatformHelper()?.enableVirtualTerminalInput?.();
|
||||
} catch {
|
||||
// Native helper not available — Shift+Tab won't be distinguishable from Tab.
|
||||
}
|
||||
@@ -441,6 +468,12 @@ export class ProcessTerminal implements Terminal {
|
||||
if (this.clearProgressInterval()) {
|
||||
process.stdout.write(TERMINAL_PROGRESS_CLEAR_SEQUENCE);
|
||||
}
|
||||
// Remove the status while stopped, for example after exit or while suspended. start() reports it again.
|
||||
if (this.programStatusSupported && this.programStatus) {
|
||||
process.stdout.write(formatProgramStatus({ state: "clear" }));
|
||||
}
|
||||
this.programStatusSupported = false;
|
||||
this.programStatusQueryPending = false;
|
||||
|
||||
// Disable bracketed paste mode
|
||||
process.stdout.write("\x1b[?2004l");
|
||||
@@ -540,6 +573,17 @@ export class ProcessTerminal implements Terminal {
|
||||
process.stdout.write(`\x1b]0;${title}\x07`);
|
||||
}
|
||||
|
||||
setProgramStatus(status: ProgramStatus): void {
|
||||
this.programStatus = status.state === "clear" ? undefined : status;
|
||||
if (this.programStatusSupported) process.stdout.write(formatProgramStatus(status));
|
||||
}
|
||||
|
||||
private writeProgramStatus(): void {
|
||||
if (this.programStatusSupported && this.programStatus) {
|
||||
process.stdout.write(formatProgramStatus(this.programStatus));
|
||||
}
|
||||
}
|
||||
|
||||
setProgress(active: boolean): void {
|
||||
if (active) {
|
||||
// OSC 9;4;3 - indeterminate progress
|
||||
|
||||
Reference in New Issue
Block a user