feat(simulation): expose normalized terminal frames

This commit is contained in:
Kit Langton
2026-07-15 11:09:53 -04:00
parent 83cfafc884
commit 04fdf59db8
4 changed files with 56 additions and 0 deletions
@@ -110,6 +110,25 @@ export function matches(harness: Pick<Harness, "screen">, 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))
@@ -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":
+24
View File
@@ -95,6 +95,29 @@ export namespace Frontend {
export const Screenshot = Schema.String
export type Screenshot = Schema.Schema.Type<typeof Screenshot>
export const Color = Schema.Tuple([Schema.Number, Schema.Number, Schema.Number, Schema.Number])
export type Color = Schema.Schema.Type<typeof Color>
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<typeof CapturedFrame> {}
export const RecordingFinish = Schema.String
export type RecordingFinish = Schema.Schema.Type<typeof RecordingFinish>
@@ -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<typeof Request>
export const decodeRequest = Schema.decodeUnknownSync(Request)
@@ -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,