feat(simulation): expose normalized terminal frames (#37135)
This commit is contained in:
parent
83cfafc884
commit
35f14900ae
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)
|
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) {
|
export const screenshot = Effect.fn("SimulationActions.screenshot")(function* (harness: Harness, name?: string) {
|
||||||
const filename = name ?? `screenshot-${crypto.randomUUID()}`
|
const filename = name ?? `screenshot-${crypto.randomUUID()}`
|
||||||
if (!filename || filename.includes("/") || filename.includes("\\") || extname(filename))
|
if (!filename || filename.includes("/") || filename.includes("\\") || extname(filename))
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { SimulationRenderer } from "./renderer"
|
||||||
|
|
||||||
function handle(harness: Harness, request: SimulationProtocol.Frontend.Request) {
|
function handle(harness: Harness, request: SimulationProtocol.Frontend.Request) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
|
case "ui.capture":
|
||||||
|
return SimulationActions.capture(harness)
|
||||||
case "ui.screenshot":
|
case "ui.screenshot":
|
||||||
return SimulationActions.screenshot(harness, request.params?.name)
|
return SimulationActions.screenshot(harness, request.params?.name)
|
||||||
case "ui.state":
|
case "ui.state":
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,29 @@ export namespace Frontend {
|
||||||
export const Screenshot = Schema.String
|
export const Screenshot = Schema.String
|
||||||
export type Screenshot = Schema.Schema.Type<typeof Screenshot>
|
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 const RecordingFinish = Schema.String
|
||||||
export type RecordingFinish = Schema.Schema.Type<typeof RecordingFinish>
|
export type RecordingFinish = Schema.Schema.Type<typeof RecordingFinish>
|
||||||
|
|
||||||
|
|
@ -142,6 +165,7 @@ export namespace Frontend {
|
||||||
...JsonRpc.RequestFields,
|
...JsonRpc.RequestFields,
|
||||||
method: Schema.Literals(["ui.enter", "ui.state", "ui.recording.finish"]),
|
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 type Request = Schema.Schema.Type<typeof Request>
|
||||||
export const decodeRequest = Schema.decodeUnknownSync(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: [] },
|
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("{")
|
socket.send("{")
|
||||||
expect(yield* Queue.take(messages)).toMatchObject({
|
expect(yield* Queue.take(messages)).toMatchObject({
|
||||||
id: null,
|
id: null,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue