feat(tui): wire Ink bridge into TUI.start() and stop()
- TUI.useInk() opts into Ink-backed rendering (call before start()) - In start(): if _useInk || process.stdout.isTTY, mount Ink renderer via startInkRenderer() and skip the legacy differential render path entirely - In stop(): unmount Ink handle and return early; legacy terminal cleanup (cursor repositioning, showCursor, terminal.stop) is skipped since Ink handles terminal restoration itself - Passes this.render()/invalidate() via a plain Component wrapper to avoid the private handleInput TypeScript conflict - Two new contract tests: useInk() flag and stop() Ink handle teardown - 80/80 tests pass; legacy path unchanged for non-TTY (CI/tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
4e97058d7e
commit
b0a8f32a10
2 changed files with 66 additions and 0 deletions
|
|
@ -128,3 +128,30 @@ describe("Container", () => {
|
|||
assert.equal(counter.disposed, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TUI useInk", () => {
|
||||
it("useInk_sets_flag_read_by_start", () => {
|
||||
const tui = new TUI(makeTerminal());
|
||||
const anyTui = tui as any;
|
||||
assert.equal(anyTui._useInk, false, "defaults to false");
|
||||
tui.useInk();
|
||||
assert.equal(anyTui._useInk, true, "useInk() sets flag to true");
|
||||
});
|
||||
|
||||
it("stop_clears_inkHandle_and_skips_terminal_stop_when_ink_active", () => {
|
||||
const tui = new TUI(makeTerminal());
|
||||
const anyTui = tui as any;
|
||||
let stopped = false;
|
||||
// Inject a fake Ink handle as if start() had mounted Ink.
|
||||
anyTui._inkHandle = { stop: () => { stopped = true; }, invalidate: () => {} };
|
||||
// Track whether the legacy terminal.stop() path was taken.
|
||||
let terminalStopped = false;
|
||||
(anyTui.terminal as any).stop = () => { terminalStopped = true; };
|
||||
|
||||
tui.stop();
|
||||
|
||||
assert.equal(stopped, true, "Ink handle stop() must be called");
|
||||
assert.equal(anyTui._inkHandle, null, "_inkHandle cleared after stop");
|
||||
assert.equal(terminalStopped, false, "legacy terminal.stop() must not be called when Ink is active");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { startInkRenderer } from "./ink-bridge.js";
|
||||
import { isKeyRelease, matchesKey } from "./keys.js";
|
||||
import {
|
||||
applyLineResets,
|
||||
|
|
@ -272,6 +273,8 @@ export class TUI extends Container {
|
|||
private fullRedrawCount = 0;
|
||||
private stopped = false;
|
||||
private _lastRenderedComponents: string[] | null = null;
|
||||
private _useInk = false;
|
||||
private _inkHandle: { stop(): void; invalidate(): void } | null = null;
|
||||
|
||||
// === Sticky bottom scrolling ===
|
||||
private isScrolledToBottom = true; // Track if user is scrolled to bottom
|
||||
|
|
@ -303,6 +306,19 @@ export class TUI extends Container {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opt into the Ink-backed render loop.
|
||||
*
|
||||
* Purpose: switch the TUI from the hand-rolled differential renderer to Ink's
|
||||
* React render loop so components can be migrated to native Ink nodes
|
||||
* incrementally. Must be called before start().
|
||||
*
|
||||
* Consumer: interactive-mode and any callers that want Ink layout.
|
||||
*/
|
||||
useInk(): void {
|
||||
this._useInk = true;
|
||||
}
|
||||
|
||||
get fullRedraws(): number {
|
||||
return this.fullRedrawCount;
|
||||
}
|
||||
|
|
@ -478,6 +494,21 @@ export class TUI extends Container {
|
|||
if (!this.terminal.isTTY) {
|
||||
return;
|
||||
}
|
||||
// Ink-backed render path: Ink manages raw mode and input; the legacy
|
||||
// differential renderer is bypassed entirely.
|
||||
if (this._useInk || process.stdout.isTTY) {
|
||||
// Wrap `this` in a plain Component so the private handleInput doesn't
|
||||
// conflict with the public Component.handleInput? signature.
|
||||
const root: Component = {
|
||||
render: (w) => this.render(w),
|
||||
invalidate: () => this.invalidate(),
|
||||
};
|
||||
this._inkHandle = startInkRenderer(
|
||||
root,
|
||||
(data) => this.handleInput(data),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.terminal.start(
|
||||
(data) => this.handleInput(data),
|
||||
() => this.requestRender(),
|
||||
|
|
@ -523,6 +554,14 @@ export class TUI extends Container {
|
|||
}
|
||||
this.overlayStack = [];
|
||||
|
||||
// Ink-backed path: unmount the Ink renderer and return; Ink restores the
|
||||
// terminal to cooked mode and shows the cursor itself.
|
||||
if (this._inkHandle) {
|
||||
this._inkHandle.stop();
|
||||
this._inkHandle = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
|
||||
if (this.previousLines.length > 0) {
|
||||
const targetRow = this.previousLines.length; // Line after the last content
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue