diff --git a/packages/simulation/src/frontend/actions.ts b/packages/simulation/src/frontend/actions.ts index 19baf6a732..64e9fd1cd8 100644 --- a/packages/simulation/src/frontend/actions.ts +++ b/packages/simulation/src/frontend/actions.ts @@ -110,6 +110,25 @@ export function matches(harness: Pick, text: string) { return harness.screen().includes(text) } +export const capture = Effect.fn("SimulationActions.capture")(function* (harness: Harness) { + yield* Effect.tryPromise(() => harness.renderOnce()) + const buffer = harness.renderer.currentRenderBuffer + return { + cols: buffer.width, + rows: buffer.height, + cursor: [0, 0] as const, + lines: buffer.getSpanLines().map((line) => ({ + spans: line.spans.map((span) => ({ + text: span.text, + fg: span.fg.toInts(), + bg: span.bg.toInts(), + attributes: span.attributes, + width: span.width, + })), + })), + } satisfies SimulationProtocol.Frontend.CapturedFrame +}) + export const screenshot = Effect.fn("SimulationActions.screenshot")(function* (harness: Harness, name?: string) { const filename = name ?? `screenshot-${crypto.randomUUID()}` if (!filename || filename.includes("/") || filename.includes("\\") || extname(filename)) diff --git a/packages/simulation/src/frontend/server.ts b/packages/simulation/src/frontend/server.ts index 991234b5b3..533ddeb3c6 100644 --- a/packages/simulation/src/frontend/server.ts +++ b/packages/simulation/src/frontend/server.ts @@ -6,6 +6,8 @@ import { SimulationRenderer } from "./renderer" function handle(harness: Harness, request: SimulationProtocol.Frontend.Request) { switch (request.method) { + case "ui.capture": + return SimulationActions.capture(harness) case "ui.screenshot": return SimulationActions.screenshot(harness, request.params?.name) case "ui.state": diff --git a/packages/simulation/src/protocol/index.ts b/packages/simulation/src/protocol/index.ts index 7ea6263b08..55f08733bb 100644 --- a/packages/simulation/src/protocol/index.ts +++ b/packages/simulation/src/protocol/index.ts @@ -95,6 +95,29 @@ export namespace Frontend { export const Screenshot = Schema.String export type Screenshot = Schema.Schema.Type + export const Color = Schema.Tuple([Schema.Number, Schema.Number, Schema.Number, Schema.Number]) + export type Color = Schema.Schema.Type + + export const CapturedFrame = Schema.Struct({ + cols: Schema.Number, + rows: Schema.Number, + cursor: Schema.Tuple([Schema.Number, Schema.Number]), + lines: Schema.Array( + Schema.Struct({ + spans: Schema.Array( + Schema.Struct({ + text: Schema.String, + fg: Color, + bg: Color, + attributes: Schema.Number, + width: Schema.Number, + }), + ), + }), + ), + }) + export interface CapturedFrame extends Schema.Schema.Type {} + export const RecordingFinish = Schema.String export type RecordingFinish = Schema.Schema.Type @@ -142,6 +165,7 @@ export namespace Frontend { ...JsonRpc.RequestFields, method: Schema.Literals(["ui.enter", "ui.state", "ui.recording.finish"]), }), + Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.capture") }), ]) export type Request = Schema.Schema.Type export const decodeRequest = Schema.decodeUnknownSync(Request) diff --git a/packages/simulation/test/frontend-server.test.ts b/packages/simulation/test/frontend-server.test.ts index 80cdde4962..451675d2e2 100644 --- a/packages/simulation/test/frontend-server.test.ts +++ b/packages/simulation/test/frontend-server.test.ts @@ -25,6 +25,17 @@ test("scopes the frontend control server and reports malformed JSON", async () = result: { focused: { editor: false }, elements: [] }, }) + socket.send(JSON.stringify({ jsonrpc: "2.0", id: 2, method: "ui.capture" })) + expect(yield* Queue.take(messages)).toMatchObject({ + id: 2, + result: { + cols: 100, + rows: 40, + cursor: [0, 0], + lines: expect.any(Array), + }, + }) + socket.send("{") expect(yield* Queue.take(messages)).toMatchObject({ id: null,