ai-remote 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/{src/index.ts → dist/index.d.ts} +0 -1
  2. package/dist/index.js +0 -1
  3. package/dist/protocols/index.d.ts +31 -0
  4. package/dist/protocols/index.js +0 -1
  5. package/dist/protocols/rdp/buffer.d.ts +38 -0
  6. package/dist/protocols/rdp/buffer.js +0 -1
  7. package/dist/protocols/rdp/caps.d.ts +49 -0
  8. package/dist/protocols/rdp/caps.js +0 -1
  9. package/dist/protocols/rdp/cert.d.ts +18 -0
  10. package/dist/protocols/rdp/cert.js +0 -1
  11. package/dist/protocols/rdp/client.d.ts +56 -0
  12. package/dist/protocols/rdp/client.js +0 -1
  13. package/dist/protocols/rdp/cliprdr.d.ts +27 -0
  14. package/dist/protocols/rdp/cliprdr.js +0 -1
  15. package/dist/protocols/rdp/credssp.d.ts +34 -0
  16. package/dist/protocols/rdp/credssp.js +0 -1
  17. package/dist/protocols/rdp/crypto.d.ts +63 -0
  18. package/dist/protocols/rdp/crypto.js +0 -1
  19. package/dist/protocols/rdp/display.d.ts +38 -0
  20. package/dist/protocols/rdp/display.js +0 -1
  21. package/dist/protocols/rdp/gcc.d.ts +23 -0
  22. package/dist/protocols/rdp/gcc.js +0 -1
  23. package/dist/protocols/rdp/keymap.d.ts +9 -0
  24. package/dist/protocols/rdp/keymap.js +0 -1
  25. package/dist/protocols/rdp/mcs.d.ts +40 -0
  26. package/dist/protocols/rdp/mcs.js +0 -1
  27. package/dist/protocols/rdp/ntlm.d.ts +61 -0
  28. package/dist/protocols/rdp/ntlm.js +0 -1
  29. package/dist/protocols/rdp/pdu.d.ts +155 -0
  30. package/dist/protocols/rdp/pdu.js +0 -1
  31. package/dist/protocols/rdp/rail.d.ts +119 -0
  32. package/dist/protocols/rdp/rail.js +0 -1
  33. package/dist/protocols/rdp/rle.d.ts +13 -0
  34. package/dist/protocols/rdp/rle.js +0 -1
  35. package/dist/protocols/rdp/sec.d.ts +59 -0
  36. package/dist/protocols/rdp/sec.js +0 -1
  37. package/dist/protocols/rdp/session.d.ts +62 -0
  38. package/dist/protocols/rdp/session.js +0 -1
  39. package/dist/protocols/rdp/tls.d.ts +18 -0
  40. package/dist/protocols/rdp/tls.js +0 -1
  41. package/dist/protocols/rdp/vchannel.d.ts +28 -0
  42. package/dist/protocols/rdp/vchannel.js +0 -1
  43. package/dist/protocols/rdp/x224.d.ts +40 -0
  44. package/dist/protocols/rdp/x224.js +0 -1
  45. package/dist/protocols/ssh/kex.d.ts +97 -0
  46. package/dist/protocols/ssh/kex.js +0 -1
  47. package/dist/protocols/ssh/messages.d.ts +50 -0
  48. package/dist/protocols/ssh/messages.js +0 -1
  49. package/dist/protocols/ssh/session.d.ts +66 -0
  50. package/dist/protocols/ssh/session.js +0 -1
  51. package/dist/protocols/ssh/terminal.d.ts +46 -0
  52. package/dist/protocols/ssh/terminal.js +0 -1
  53. package/dist/protocols/ssh/transport.d.ts +62 -0
  54. package/dist/protocols/ssh/transport.js +0 -1
  55. package/dist/protocols/ssh/wire.d.ts +52 -0
  56. package/dist/protocols/ssh/wire.js +0 -1
  57. package/dist/protocols/types.d.ts +127 -0
  58. package/dist/protocols/types.js +0 -1
  59. package/dist/protocols/vnc/input.d.ts +5 -0
  60. package/dist/protocols/vnc/input.js +0 -1
  61. package/dist/protocols/vnc/session.d.ts +12 -0
  62. package/dist/protocols/vnc/session.js +0 -1
  63. package/dist/shared/connection.d.ts +81 -0
  64. package/dist/shared/connection.js +0 -1
  65. package/dist/shared/device.d.ts +26 -0
  66. package/dist/shared/device.js +0 -1
  67. package/dist/shared/hosts.d.ts +63 -0
  68. package/dist/shared/hosts.js +0 -1
  69. package/dist/shared/icons.d.ts +46 -0
  70. package/dist/shared/icons.js +0 -1
  71. package/dist/shared/protocol.d.ts +28 -0
  72. package/dist/shared/protocol.js +0 -1
  73. package/dist/shared/signals.d.ts +37 -0
  74. package/dist/shared/signals.js +0 -1
  75. package/package.json +10 -11
  76. package/dist/index.js.map +0 -7
  77. package/dist/protocols/index.js.map +0 -7
  78. package/dist/protocols/rdp/buffer.js.map +0 -7
  79. package/dist/protocols/rdp/caps.js.map +0 -7
  80. package/dist/protocols/rdp/cert.js.map +0 -7
  81. package/dist/protocols/rdp/client.js.map +0 -7
  82. package/dist/protocols/rdp/cliprdr.js.map +0 -7
  83. package/dist/protocols/rdp/credssp.js.map +0 -7
  84. package/dist/protocols/rdp/crypto.js.map +0 -7
  85. package/dist/protocols/rdp/display.js.map +0 -7
  86. package/dist/protocols/rdp/gcc.js.map +0 -7
  87. package/dist/protocols/rdp/keymap.js.map +0 -7
  88. package/dist/protocols/rdp/mcs.js.map +0 -7
  89. package/dist/protocols/rdp/ntlm.js.map +0 -7
  90. package/dist/protocols/rdp/pdu.js.map +0 -7
  91. package/dist/protocols/rdp/rail.js.map +0 -7
  92. package/dist/protocols/rdp/rle.js.map +0 -7
  93. package/dist/protocols/rdp/sec.js.map +0 -7
  94. package/dist/protocols/rdp/session.js.map +0 -7
  95. package/dist/protocols/rdp/tls.js.map +0 -7
  96. package/dist/protocols/rdp/vchannel.js.map +0 -7
  97. package/dist/protocols/rdp/x224.js.map +0 -7
  98. package/dist/protocols/ssh/kex.js.map +0 -7
  99. package/dist/protocols/ssh/messages.js.map +0 -7
  100. package/dist/protocols/ssh/session.js.map +0 -7
  101. package/dist/protocols/ssh/terminal.js.map +0 -7
  102. package/dist/protocols/ssh/transport.js.map +0 -7
  103. package/dist/protocols/ssh/wire.js.map +0 -7
  104. package/dist/protocols/types.js.map +0 -7
  105. package/dist/protocols/vnc/input.js.map +0 -7
  106. package/dist/protocols/vnc/session.js.map +0 -7
  107. package/dist/shared/connection.js.map +0 -7
  108. package/dist/shared/device.js.map +0 -7
  109. package/dist/shared/hosts.js.map +0 -7
  110. package/dist/shared/icons.js.map +0 -7
  111. package/dist/shared/protocol.js.map +0 -7
  112. package/dist/shared/signals.js.map +0 -7
  113. package/src/cli/cli.ts +0 -462
  114. package/src/cli/daemon.ts +0 -306
  115. package/src/cli/framebuffer.ts +0 -115
  116. package/src/cli/generated/viewer-bundle.ts +0 -5
  117. package/src/cli/ipc.ts +0 -78
  118. package/src/cli/native/window.swift +0 -151
  119. package/src/cli/paths.ts +0 -28
  120. package/src/cli/png.ts +0 -106
  121. package/src/cli/session.ts +0 -263
  122. package/src/cli/shell.ts +0 -184
  123. package/src/cli/transport.ts +0 -86
  124. package/src/cli/viewer-client/assets.d.ts +0 -4
  125. package/src/cli/viewer-client/main.ts +0 -323
  126. package/src/cli/viewer-page.ts +0 -138
  127. package/src/cli/viewer.ts +0 -296
  128. package/src/cli/window.ts +0 -140
  129. package/src/cli/wsserver.ts +0 -144
  130. package/src/protocols/index.ts +0 -72
  131. package/src/protocols/rdp/buffer.ts +0 -204
  132. package/src/protocols/rdp/caps.ts +0 -304
  133. package/src/protocols/rdp/cert.ts +0 -190
  134. package/src/protocols/rdp/client.ts +0 -1371
  135. package/src/protocols/rdp/cliprdr.ts +0 -344
  136. package/src/protocols/rdp/credssp.ts +0 -319
  137. package/src/protocols/rdp/crypto.ts +0 -416
  138. package/src/protocols/rdp/display.ts +0 -750
  139. package/src/protocols/rdp/gcc.ts +0 -162
  140. package/src/protocols/rdp/keymap.ts +0 -68
  141. package/src/protocols/rdp/mcs.ts +0 -283
  142. package/src/protocols/rdp/ntlm.ts +0 -380
  143. package/src/protocols/rdp/pdu.ts +0 -497
  144. package/src/protocols/rdp/rail.ts +0 -480
  145. package/src/protocols/rdp/rle.ts +0 -343
  146. package/src/protocols/rdp/sec.ts +0 -184
  147. package/src/protocols/rdp/session.ts +0 -393
  148. package/src/protocols/rdp/tls.ts +0 -452
  149. package/src/protocols/rdp/vchannel.ts +0 -89
  150. package/src/protocols/rdp/x224.ts +0 -139
  151. package/src/protocols/ssh/kex.ts +0 -227
  152. package/src/protocols/ssh/messages.ts +0 -54
  153. package/src/protocols/ssh/session.ts +0 -879
  154. package/src/protocols/ssh/terminal.ts +0 -242
  155. package/src/protocols/ssh/transport.ts +0 -745
  156. package/src/protocols/ssh/wire.ts +0 -220
  157. package/src/protocols/types.ts +0 -146
  158. package/src/protocols/vnc/input.ts +0 -13
  159. package/src/protocols/vnc/novnc.d.ts +0 -75
  160. package/src/protocols/vnc/session.ts +0 -367
  161. package/src/shared/connection.ts +0 -206
  162. package/src/shared/device.ts +0 -28
  163. package/src/shared/hosts.ts +0 -240
  164. package/src/shared/icons.ts +0 -49
  165. package/src/shared/protocol.ts +0 -55
  166. package/src/shared/signals.ts +0 -67
package/src/cli/daemon.ts DELETED
@@ -1,306 +0,0 @@
1
- /**
2
- * A session that outlives the command that started it.
3
- *
4
- * An RDP handshake costs a second or two and everything after it is stateful:
5
- * a click, then a screenshot of what the click did, are two halves of one
6
- * observation. A tool that reconnects between them can never see the result of
7
- * its own action, so the session lives here and commands attach to it.
8
- *
9
- * The viewer attaches the same way, and so does the terminal. None of them own
10
- * the session; it outlives all of them and ends when it is told to.
11
- */
12
-
13
- import net from 'node:net';
14
- import { unlinkSync, writeFileSync, existsSync } from 'node:fs';
15
- import { RdpSession, type SessionOptions } from './session';
16
- import { bareUsername, Shell } from './shell';
17
- import { downscale, encodePng } from './png';
18
- import { startViewer, type Viewer } from './viewer';
19
- import { readMessages, writeMessage, type Request } from './ipc';
20
- import { ensureRunDir, metaPath, socketPath } from './paths';
21
-
22
- export interface DaemonOptions extends SessionOptions {
23
- name: string;
24
- view: boolean;
25
- launchBrowser: boolean;
26
- viewPort: number;
27
- /** Account for the terminal, when one is wanted. Blank leaves SSH unopened. */
28
- sshUsername: string;
29
- /** Open in the default browser rather than a window of its own. */
30
- tab: boolean;
31
- /** Start the window already filling the display. */
32
- fullscreen: boolean;
33
- sshPort: number;
34
- idleMs: number;
35
- }
36
-
37
- export async function runDaemon(options: DaemonOptions): Promise<void> {
38
- ensureRunDir();
39
-
40
- const session = new RdpSession(options);
41
- const size = await session.connect();
42
-
43
- let shell: Shell | null = null;
44
- let shellStarting: Promise<Shell> | null = null;
45
-
46
- /**
47
- * The terminal is opened on first use rather than at connect time. Most
48
- * sessions never ask for one, and a second sign-in that nobody wanted is a
49
- * second chance to lock an account out.
50
- *
51
- * It signs in as the desktop's own account and password by default. Needing a
52
- * separate flag to get a terminal on the machine you are already looking at
53
- * is a question with an obvious answer, so it is not asked.
54
- */
55
- function openShell(): Promise<Shell> {
56
- if (shell) return Promise.resolve(shell);
57
- if (shellStarting) return shellStarting;
58
-
59
- shellStarting = (async () => {
60
- const started = new Shell({
61
- host: options.host,
62
- port: options.sshPort,
63
- username: options.sshUsername || bareUsername(options.username),
64
- password: options.password,
65
- });
66
- await started.connect();
67
- shell = started;
68
- return started;
69
- })();
70
-
71
- shellStarting.catch(() => { shellStarting = null; });
72
- return shellStarting;
73
- }
74
-
75
- const startedAt = new Date().toISOString();
76
-
77
- /**
78
- * What `ai-remote list` reads. Rewritten whenever the viewer comes or goes,
79
- * so the listing never advertises a window that is not there.
80
- */
81
- function writeMeta(): void {
82
- writeFileSync(metaPath(options.name), JSON.stringify({
83
- name: options.name,
84
- host: options.host,
85
- port: options.port,
86
- pid: process.pid,
87
- viewer: viewer?.url ?? null,
88
- width: session.framebuffer.width || size.width,
89
- height: session.framebuffer.height || size.height,
90
- startedAt,
91
- }, null, 2));
92
- }
93
-
94
- /**
95
- * The viewer is not part of the session; it attaches to one.
96
- *
97
- * So it can be opened and closed while the session keeps running, and a
98
- * session with no viewer is not a lesser session -- the framebuffer is the
99
- * authority either way.
100
- */
101
- // Annotated rather than inferred: every assignment happens inside a closure,
102
- // which control-flow analysis cannot follow, so it would otherwise narrow to
103
- // `null` and then to `never` at the first use.
104
- let viewer: Viewer | null = null as Viewer | null;
105
-
106
- async function openViewer(launch: boolean): Promise<Viewer> {
107
- if (viewer) {
108
- if (launch) viewer.launch();
109
- return viewer;
110
- }
111
- viewer = await startViewer(session, options.viewPort, {
112
- launch,
113
- openShell,
114
- tab: options.tab,
115
- fullscreen: options.fullscreen,
116
- });
117
- writeMeta();
118
- return viewer;
119
- }
120
-
121
- function closeViewer(): boolean {
122
- if (!viewer) return false;
123
- viewer.close();
124
- viewer = null;
125
- writeMeta();
126
- return true;
127
- }
128
-
129
- if (options.view) await openViewer(options.launchBrowser);
130
-
131
- const path = socketPath(options.name);
132
- if (existsSync(path)) unlinkSync(path);
133
-
134
- let lastUsed = Date.now();
135
- const touch = () => { lastUsed = Date.now(); };
136
-
137
- const server = net.createServer((socket) => {
138
- readMessages(socket, async (message: Request) => {
139
- touch();
140
- try {
141
- const result = await handle(message);
142
- writeMessage(socket, { id: message.id, ok: true, result });
143
- } catch (error) {
144
- writeMessage(socket, {
145
- id: message.id,
146
- ok: false,
147
- error: error instanceof Error ? error.message : String(error),
148
- });
149
- }
150
- });
151
- });
152
-
153
- async function handle(message: Request): Promise<unknown> {
154
- const args = (message.args ?? {}) as Record<string, any>;
155
-
156
- switch (message.op) {
157
- case 'view-open': {
158
- const opened = await openViewer(args.launch !== false);
159
- return { viewer: opened.url, window: opened.window?.kind ?? null };
160
- }
161
-
162
- case 'view-close':
163
- return { closed: closeViewer() };
164
-
165
- case 'status':
166
- return {
167
- host: options.host,
168
- port: options.port,
169
- name: options.name,
170
- width: session.framebuffer.width,
171
- height: session.framebuffer.height,
172
- connected: session.connected,
173
- viewer: viewer?.url ?? null,
174
- window: viewer?.window?.kind ?? null,
175
- viewers: viewer?.viewers ?? 0,
176
- shell: shell ? 'open' : shellStarting ? 'opening' : 'closed',
177
- pid: process.pid,
178
- };
179
-
180
- case 'shot': {
181
- await session.settle({ quietMs: 250, timeoutMs: 3000 });
182
- const shot = session.framebuffer.snapshot();
183
- const scaled = args.maxEdge ? downscale(shot.rgba, shot.width, shot.height, args.maxEdge) : shot;
184
- const png = encodePng(scaled.rgba, scaled.width, scaled.height);
185
- if (args.file) {
186
- writeFileSync(args.file, png);
187
- return { file: args.file, width: scaled.width, height: scaled.height };
188
- }
189
- return { base64: Buffer.from(png).toString('base64'), width: scaled.width, height: scaled.height };
190
- }
191
-
192
- case 'script':
193
- return { steps: await runScript(session, String(args.script ?? '')) };
194
-
195
- case 'exec': {
196
- const live = await openShell();
197
- const result = await live.runCommand(String(args.command ?? ''));
198
- return result;
199
- }
200
-
201
- case 'shell-open':
202
- await openShell();
203
- return { shell: 'open' };
204
-
205
- case 'refresh':
206
- session.requestFullRepaint();
207
- return {};
208
-
209
- case 'close':
210
- setTimeout(() => shutdown('asked to close'), 50);
211
- return { closing: true };
212
-
213
- default:
214
- throw new Error(`Unknown operation "${message.op}".`);
215
- }
216
- }
217
-
218
- await new Promise<void>((resolve) => server.listen(path, resolve));
219
-
220
- writeMeta();
221
-
222
- // A session whose host went away is not worth keeping a socket open for.
223
- session.client.addEventListener('close', () => shutdown('the host closed the connection'));
224
-
225
- const idleTimer = options.idleMs > 0
226
- ? setInterval(() => {
227
- if (Date.now() - lastUsed > options.idleMs) shutdown('idle');
228
- }, 30_000)
229
- : null;
230
-
231
- let shuttingDown = false;
232
- function shutdown(why: string): void {
233
- if (shuttingDown) return;
234
- shuttingDown = true;
235
- console.log(`[session] closing: ${why}`);
236
-
237
- if (idleTimer) clearInterval(idleTimer);
238
- viewer?.close();
239
- shell?.disconnect();
240
- session.disconnect();
241
- server.close();
242
- for (const file of [path, metaPath(options.name)]) {
243
- try { unlinkSync(file); } catch { /* already gone */ }
244
- }
245
- setTimeout(() => process.exit(0), 100);
246
- }
247
-
248
- for (const signal of ['SIGINT', 'SIGTERM'] as const) {
249
- process.on(signal, () => shutdown(signal));
250
- }
251
-
252
- console.log(`[session] ${options.name} ready at ${size.width}x${size.height}`);
253
- if (viewer) console.log(`[session] viewer ${viewer.url}`);
254
- }
255
-
256
- /**
257
- * Run several steps against one live session.
258
- *
259
- * Shared by the `do` command and the daemon, because they mean the same thing:
260
- * a sequence that keeps its own state between steps.
261
- */
262
- export async function runScript(session: RdpSession, script: string): Promise<string[]> {
263
- const done: string[] = [];
264
-
265
- for (const raw of script.split(';')) {
266
- const step = raw.trim();
267
- if (!step) continue;
268
-
269
- const [verb, ...rest] = step.split(/\s+/);
270
- const argument = rest.join(' ');
271
- const numbers = () => argument.split(',').map(Number);
272
-
273
- switch (verb) {
274
- case 'move': { const [x, y] = numbers(); session.movePointer(x, y); break; }
275
- case 'click': {
276
- const [x, y] = numbers();
277
- const named = argument.split(',')[2]?.trim();
278
- session.click(x, y, { right: 2, middle: 1 }[named ?? ''] ?? 0);
279
- break;
280
- }
281
- case 'dblclick': { const [x, y] = numbers(); session.click(x, y, 0); session.click(x, y, 0); break; }
282
- case 'scroll': { const [x, y, dx, dy] = numbers(); session.scroll(x, y, dx || 0, dy || 0); break; }
283
- case 'type': session.typeText(argument); break;
284
- case 'key': {
285
- const codes = argument.split('+').map((code) => code.trim()).filter(Boolean);
286
- for (const code of codes) session.sendKeyCode(code, true);
287
- for (const code of [...codes].reverse()) session.sendKeyCode(code, false);
288
- break;
289
- }
290
- case 'cad': session.sendCtrlAltDel(); break;
291
- case 'wait': await new Promise((resolve) => setTimeout(resolve, Number(argument) || 500)); break;
292
- case 'shot': {
293
- await session.settle({ quietMs: 350, timeoutMs: 4000 });
294
- const shot = session.framebuffer.snapshot();
295
- writeFileSync(argument || 'screen.png', encodePng(shot.rgba, shot.width, shot.height));
296
- break;
297
- }
298
- default:
299
- throw new Error(`Unknown step "${verb}" in the script.`);
300
- }
301
-
302
- done.push(step);
303
- }
304
-
305
- return done;
306
- }
@@ -1,115 +0,0 @@
1
- /**
2
- * The desktop as pixels, held in memory.
3
- *
4
- * In a browser the canvas is both the surface the protocol paints into and the
5
- * thing a person looks at. Headless, those come apart: this holds the pixels so
6
- * a screenshot can be taken at any moment, and the viewer -- when there is one
7
- * -- is a separate attachment fed the same rectangles.
8
- *
9
- * Which is what makes the viewer optional. The framebuffer is the authority,
10
- * so a session with nobody watching is not a degraded session.
11
- */
12
-
13
- import { decodeBitmapRect } from '../protocols/rdp/rle';
14
-
15
- export interface PaintedRect {
16
- left: number;
17
- top: number;
18
- width: number;
19
- height: number;
20
- rgba: Uint8Array;
21
- }
22
-
23
- /** Palettes only exist for 8bpp updates; anything else ignores this. */
24
- const NO_PALETTE = new Uint32Array(256);
25
-
26
- export class Framebuffer {
27
- width = 0;
28
- height = 0;
29
- /** RGBA, row-major, `width * height * 4` bytes. */
30
- pixels = new Uint8Array(0);
31
- /**
32
- * Bumped on anything that changes what a viewer should be showing, including
33
- * a resize.
34
- */
35
- generation = 0;
36
- /**
37
- * Rectangles painted. Kept apart from `generation` because "the desktop has
38
- * been sized" and "the desktop has sent pixels" are different questions, and
39
- * a screenshot taken on the strength of the first one is a black image.
40
- */
41
- paints = 0;
42
-
43
- palette: Uint32Array | null = null;
44
-
45
- resize(width: number, height: number): void {
46
- if (width === this.width && height === this.height) return;
47
- this.width = width;
48
- this.height = height;
49
- this.pixels = new Uint8Array(width * height * 4);
50
- // Opaque black, so an unpainted desktop reads as a screen rather than as
51
- // transparency that a PNG viewer will render as a checkerboard.
52
- for (let i = 3; i < this.pixels.length; i += 4) this.pixels[i] = 255;
53
- this.generation++;
54
- }
55
-
56
- /**
57
- * Decode and blit one update. Returns what was painted, so a viewer can be
58
- * sent the same rectangles without decoding them a second time.
59
- */
60
- draw(rects: readonly any[], palette: Uint32Array | null): PaintedRect[] {
61
- if (palette) this.palette = palette;
62
- const painted: PaintedRect[] = [];
63
-
64
- for (const rect of rects) {
65
- if (!rect.width || !rect.height) continue;
66
- let rgba: Uint8Array;
67
- try {
68
- // decodeBitmapRect hands back a Uint8ClampedArray, which is the right
69
- // thing for a canvas and the wrong thing for everything else here; the
70
- // view costs nothing and shares the same bytes.
71
- const decoded = decodeBitmapRect(rect, this.palette ?? NO_PALETTE);
72
- rgba = new Uint8Array(decoded.buffer, decoded.byteOffset, decoded.byteLength);
73
- } catch (error) {
74
- // One malformed rectangle must not end a session; the next full update
75
- // repaints the area anyway. Same rule the canvas renderer follows.
76
- console.warn('[rdp] dropped an undecodable rectangle', {
77
- geometry: `${rect.width}x${rect.height}+${rect.left}+${rect.top}`,
78
- error: error instanceof Error ? error.message : String(error),
79
- });
80
- continue;
81
- }
82
-
83
- this.blit(rect.left, rect.top, rect.width, rect.height, rgba);
84
- painted.push({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, rgba });
85
- }
86
-
87
- if (painted.length) {
88
- this.generation++;
89
- this.paints += painted.length;
90
- }
91
- return painted;
92
- }
93
-
94
- /** Copy one RGBA rectangle in, clipped to the framebuffer. */
95
- blit(left: number, top: number, width: number, height: number, rgba: Uint8Array): void {
96
- for (let row = 0; row < height; row++) {
97
- const y = top + row;
98
- if (y < 0 || y >= this.height) continue;
99
-
100
- const from = row * width * 4;
101
- const columns = Math.min(width, this.width - left);
102
- if (columns <= 0) continue;
103
-
104
- this.pixels.set(
105
- rgba.subarray(from, from + columns * 4),
106
- (y * this.width + left) * 4
107
- );
108
- }
109
- }
110
-
111
- /** A copy, so a caller holding a screenshot is not looking at live memory. */
112
- snapshot(): { rgba: Uint8Array; width: number; height: number } {
113
- return { rgba: this.pixels.slice(), width: this.width, height: this.height };
114
- }
115
- }