82 lines
2.8 KiB
TypeScript
82 lines
2.8 KiB
TypeScript
import type { Instance } from "ink";
|
||
import type React from "react";
|
||
|
||
let inkRenderer: Instance | null = null;
|
||
|
||
// Track whether the clean‑up routine has already executed so repeat calls are
|
||
// silently ignored. This can happen when different exit paths (e.g. the raw
|
||
// Ctrl‑C handler and the process "exit" event) both attempt to tidy up.
|
||
let didRunOnExit = false;
|
||
|
||
export function setInkRenderer(renderer: Instance): void {
|
||
inkRenderer = renderer;
|
||
|
||
if (process.env["CODEX_FPS_DEBUG"]) {
|
||
let last = Date.now();
|
||
const logFrame = () => {
|
||
const now = Date.now();
|
||
// eslint-disable-next-line no-console
|
||
console.error(`[fps] frame in ${now - last}ms`);
|
||
last = now;
|
||
};
|
||
|
||
// Monkey‑patch the public rerender/unmount methods so we know when Ink
|
||
// flushes a new frame. React’s internal renders eventually call
|
||
// `rerender()` so this gives us a good approximation without poking into
|
||
// private APIs.
|
||
const origRerender = renderer.rerender.bind(renderer);
|
||
renderer.rerender = (node: React.ReactNode) => {
|
||
logFrame();
|
||
return origRerender(node);
|
||
};
|
||
|
||
const origClear = renderer.clear.bind(renderer);
|
||
renderer.clear = () => {
|
||
logFrame();
|
||
return origClear();
|
||
};
|
||
}
|
||
}
|
||
|
||
export function clearTerminal(): void {
|
||
if (process.env["CODEX_QUIET_MODE"] === "1") {
|
||
return;
|
||
}
|
||
|
||
// When using the alternate screen the content never scrolls, so we rarely
|
||
// need a full clear. Still expose the behaviour when explicitly requested
|
||
// (e.g. via Ctrl‑L) but avoid unnecessary clears on every render to minimise
|
||
// flicker.
|
||
if (inkRenderer) {
|
||
inkRenderer.clear();
|
||
}
|
||
}
|
||
|
||
export function onExit(): void {
|
||
// Ensure the clean‑up logic only runs once even if multiple exit signals
|
||
// (e.g. Ctrl‑C data handler *and* the process "exit" event) invoke this
|
||
// function. Re‑running the sequence is mostly harmless but can lead to
|
||
// duplicate log messages and increases the risk of confusing side‑effects
|
||
// should future clean‑up steps become non‑idempotent.
|
||
if (didRunOnExit) {
|
||
return;
|
||
}
|
||
|
||
didRunOnExit = true;
|
||
|
||
// First make sure Ink is properly unmounted so it can restore any terminal
|
||
// state it modified (e.g. raw‑mode on stdin). Failing to do so leaves the
|
||
// terminal in raw‑mode after the Node process has exited which looks like
|
||
// a “frozen” shell – no input is echoed and Ctrl‑C/Z no longer work. This
|
||
// regression was introduced when we switched from `inkRenderer.unmount()`
|
||
// to letting `process.exit` terminate the program a few commits ago. By
|
||
// explicitly unmounting here we ensure Ink performs its clean‑up logic
|
||
// *before* we restore the primary screen buffer.
|
||
if (inkRenderer) {
|
||
try {
|
||
inkRenderer.unmount();
|
||
} catch {
|
||
/* best‑effort – continue even if Ink throws */
|
||
}
|
||
}
|
||
}
|