refactor(tui): keep fake renderer setup out of app code

Move fake renderer creation into simulation/renderer.ts, which keeps the
TestRendererSetup in a module-side WeakMap. createHarness looks it up by
renderer, so app.tsx only picks which renderer factory to call.
This commit is contained in:
James Long 2026-07-02 18:26:39 +00:00
commit 7a76efc4f8
3 changed files with 54 additions and 46 deletions

View file

@ -8,8 +8,7 @@ import { ClipboardProvider, useClipboard } from "./context/clipboard"
import { ExitProvider, useExit } from "./context/exit" import { ExitProvider, useExit } from "./context/exit"
import { EpilogueProvider } from "./context/epilogue" import { EpilogueProvider } from "./context/epilogue"
import * as Selection from "./util/selection" import * as Selection from "./util/selection"
import { createCliRenderer, MouseButton, type CliRenderer } from "@opentui/core" import { createCliRenderer, MouseButton } from "@opentui/core"
import type { TestRendererSetup } from "@opentui/core/testing"
import { RouteProvider, useRoute } from "./context/route" import { RouteProvider, useRoute } from "./context/route"
import { import {
Switch, Switch,
@ -184,43 +183,31 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
const simulationEnabled = process.env.OPENCODE_SIMULATION === "1" || process.env.OPENCODE_SIMULATION === "true" const simulationEnabled = process.env.OPENCODE_SIMULATION === "1" || process.env.OPENCODE_SIMULATION === "true"
const result = yield* Effect.scoped( const result = yield* Effect.scoped(
Effect.gen(function* () { Effect.gen(function* () {
const acquired = yield* Effect.acquireRelease( const renderer = yield* Effect.acquireRelease(
Effect.tryPromise({ Effect.tryPromise({
try: async (): Promise<{ renderer: CliRenderer; setup?: TestRendererSetup }> => { try: () =>
// The fake renderer is a real CliRenderer backed by an in-memory simulationEnabled && process.env.OPENCODE_SIMULATION_RENDERER === "fake"
// screen buffer; everything downstream (keymap, harness, render) ? import("./simulation/renderer").then(({ SimulationRenderer }) => SimulationRenderer.create())
// treats both renderer kinds identically. : createCliRenderer({
if (simulationEnabled && process.env.OPENCODE_SIMULATION_RENDERER === "fake") { externalOutputMode: "passthrough",
const { createTestRenderer } = await import("@opentui/core/testing") targetFps: 60,
const setup = await createTestRenderer({ gatherStats: false,
width: Number(process.env.OPENCODE_SIMULATION_TUI_WIDTH) || 100, exitOnCtrlC: false,
height: Number(process.env.OPENCODE_SIMULATION_TUI_HEIGHT) || 40, useKittyKeyboard: {},
}) autoFocus: false,
return { renderer: setup.renderer, setup } openConsoleOnError: false,
} useMouse: !Flag.OPENCODE_DISABLE_MOUSE && input.config.mouse,
const renderer = await createCliRenderer({ consoleOptions: {
externalOutputMode: "passthrough", keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
targetFps: 60, },
gatherStats: false, }),
exitOnCtrlC: false,
useKittyKeyboard: {},
autoFocus: false,
openConsoleOnError: false,
useMouse: !Flag.OPENCODE_DISABLE_MOUSE && input.config.mouse,
consoleOptions: {
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
},
})
return { renderer }
},
catch: (error) => (error instanceof Error ? error : new Error(String(error))), catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}), }),
(acquired) => (renderer) =>
Effect.sync(() => { Effect.sync(() => {
destroyRenderer(acquired.renderer) destroyRenderer(renderer)
}), }),
) )
const renderer = acquired.renderer
win32DisableProcessedInput() win32DisableProcessedInput()
const keymap = createDefaultOpenTuiKeymap(renderer) const keymap = createDefaultOpenTuiKeymap(renderer)
yield* Effect.acquireRelease( yield* Effect.acquireRelease(
@ -250,7 +237,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
const simulation = yield* Effect.promise(async () => { const simulation = yield* Effect.promise(async () => {
const { SimulationActions } = await import("./simulation/actions") const { SimulationActions } = await import("./simulation/actions")
const { SimulationServer } = await import("./simulation/server") const { SimulationServer } = await import("./simulation/server")
return SimulationServer.start(SimulationActions.createHarness(renderer, acquired.setup)) return SimulationServer.start(SimulationActions.createHarness(renderer))
}) })
if (simulation) { if (simulation) {
process.stderr.write(`opencode simulation websocket: ${simulation.url}\n`) process.stderr.write(`opencode simulation websocket: ${simulation.url}\n`)

View file

@ -1,11 +1,6 @@
import type { CliRenderer, Renderable } from "@opentui/core" import type { CliRenderer, Renderable } from "@opentui/core"
import { import { createMockKeys, createMockMouse, type MockInput, type MockMouse } from "@opentui/core/testing"
createMockKeys, import { SimulationRenderer } from "./renderer"
createMockMouse,
type MockInput,
type MockMouse,
type TestRendererSetup,
} from "@opentui/core/testing"
import { SimulationTrace } from "./trace" import { SimulationTrace } from "./trace"
export interface KeyModifiers { export interface KeyModifiers {
@ -77,12 +72,13 @@ function hit(renderer: CliRenderer, renderable: Renderable) {
/** /**
* Builds the harness the simulation server drives. * Builds the harness the simulation server drives.
* *
* When the renderer came from `createTestRenderer` (fake renderer), pass its * When the renderer is the fake simulation renderer, its TestRendererSetup
* `TestRendererSetup` so the harness uses the supported testing APIs. Without * provides the supported testing APIs. For the visible terminal renderer the
* it (visible terminal renderer) the harness falls back to `requestRender` + * harness falls back to `requestRender` + `idle` and reading the private
* `idle` and reading the private `currentRenderBuffer`. * `currentRenderBuffer`.
*/ */
export function createHarness(renderer: CliRenderer, setup?: TestRendererSetup): Harness { export function createHarness(renderer: CliRenderer): Harness {
const setup = SimulationRenderer.setupFor(renderer)
return { return {
renderer, renderer,
mockInput: setup?.mockInput ?? createMockKeys(renderer), mockInput: setup?.mockInput ?? createMockKeys(renderer),

View file

@ -0,0 +1,25 @@
import type { CliRenderer } from "@opentui/core"
import { createTestRenderer, type TestRendererSetup } from "@opentui/core/testing"
const setups = new WeakMap<CliRenderer, TestRendererSetup>()
/**
* Creates the fake simulation renderer: a real CliRenderer backed by an
* in-memory screen buffer instead of a terminal. The TestRendererSetup is
* kept module-side (keyed by renderer) so the harness can use the supported
* testing APIs without app code carrying it around.
*/
export async function create(): Promise<CliRenderer> {
const setup = await createTestRenderer({
width: Number(process.env.OPENCODE_SIMULATION_TUI_WIDTH) || 100,
height: Number(process.env.OPENCODE_SIMULATION_TUI_HEIGHT) || 40,
})
setups.set(setup.renderer, setup)
return setup.renderer
}
export function setupFor(renderer: CliRenderer): TestRendererSetup | undefined {
return setups.get(renderer)
}
export * as SimulationRenderer from "./renderer"