فهرست منبع

feat(simulation): expose normalized terminal frames (#37135)

Kit Langton 1 ماه پیش
والد
کامیت
35f14900ae

+ 19 - 0
packages/simulation/src/frontend/actions.ts

@@ -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))

+ 2 - 0
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":

+ 24 - 0
packages/simulation/src/protocol/index.ts

@@ -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)

+ 11 - 0
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,