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);
|
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 fs from "node:fs";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
|
import { startInkRenderer } from "./ink-bridge.js";
|
||||||
import { isKeyRelease, matchesKey } from "./keys.js";
|
import { isKeyRelease, matchesKey } from "./keys.js";
|
||||||
import {
|
import {
|
||||||
applyLineResets,
|
applyLineResets,
|
||||||
|
|
@ -272,6 +273,8 @@ export class TUI extends Container {
|
||||||
private fullRedrawCount = 0;
|
private fullRedrawCount = 0;
|
||||||
private stopped = false;
|
private stopped = false;
|
||||||
private _lastRenderedComponents: string[] | null = null;
|
private _lastRenderedComponents: string[] | null = null;
|
||||||
|
private _useInk = false;
|
||||||
|
private _inkHandle: { stop(): void; invalidate(): void } | null = null;
|
||||||
|
|
||||||
// === Sticky bottom scrolling ===
|
// === Sticky bottom scrolling ===
|
||||||
private isScrolledToBottom = true; // Track if user is scrolled to bottom
|
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 {
|
get fullRedraws(): number {
|
||||||
return this.fullRedrawCount;
|
return this.fullRedrawCount;
|
||||||
}
|
}
|
||||||
|
|
@ -478,6 +494,21 @@ export class TUI extends Container {
|
||||||
if (!this.terminal.isTTY) {
|
if (!this.terminal.isTTY) {
|
||||||
return;
|
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(
|
this.terminal.start(
|
||||||
(data) => this.handleInput(data),
|
(data) => this.handleInput(data),
|
||||||
() => this.requestRender(),
|
() => this.requestRender(),
|
||||||
|
|
@ -523,6 +554,14 @@ export class TUI extends Container {
|
||||||
}
|
}
|
||||||
this.overlayStack = [];
|
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
|
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
|
||||||
if (this.previousLines.length > 0) {
|
if (this.previousLines.length > 0) {
|
||||||
const targetRow = this.previousLines.length; // Line after the last content
|
const targetRow = this.previousLines.length; // Line after the last content
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue