wasm-idle 0.3.2 → 0.4.1

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 (46) hide show
  1. package/README.md +22 -2
  2. package/dist/clang/app.d.ts +2 -1
  3. package/dist/clang/app.js +166 -137
  4. package/dist/clang/index.d.ts +6 -0
  5. package/dist/clang/index.js +137 -21
  6. package/dist/debug/expression.js +135 -19
  7. package/dist/debug/index.d.ts +1 -1
  8. package/dist/debug/index.js +1 -1
  9. package/dist/debug/language/index.d.ts +1 -0
  10. package/dist/debug/language/index.js +1 -0
  11. package/dist/debug/language/python.d.ts +2 -0
  12. package/dist/debug/language/python.js +14 -0
  13. package/dist/index.d.ts +3 -1
  14. package/dist/index.js +1 -1
  15. package/dist/playground/assetBridge.d.ts +20 -0
  16. package/dist/playground/assetBridge.js +192 -0
  17. package/dist/playground/assets.d.ts +32 -0
  18. package/dist/playground/assets.js +97 -0
  19. package/dist/playground/clang.d.ts +11 -3
  20. package/dist/playground/clang.js +57 -20
  21. package/dist/playground/index.d.ts +2 -1
  22. package/dist/playground/index.js +10 -2
  23. package/dist/playground/java.d.ts +33 -0
  24. package/dist/playground/java.js +161 -0
  25. package/dist/playground/javaStdin.d.ts +8 -0
  26. package/dist/playground/javaStdin.js +323 -0
  27. package/dist/playground/options.d.ts +19 -0
  28. package/dist/playground/options.js +15 -1
  29. package/dist/playground/python.d.ts +21 -6
  30. package/dist/playground/python.js +135 -25
  31. package/dist/playground/sandbox.d.ts +5 -2
  32. package/dist/playground/stdinBuffer.d.ts +7 -0
  33. package/dist/playground/stdinBuffer.js +100 -0
  34. package/dist/playground/teavmConfig.d.ts +3 -0
  35. package/dist/playground/teavmConfig.js +14 -0
  36. package/dist/playground/worker/assets.d.ts +12 -0
  37. package/dist/playground/worker/assets.js +257 -0
  38. package/dist/playground/worker/clang.js +13 -23
  39. package/dist/playground/worker/java.d.ts +1 -0
  40. package/dist/playground/worker/java.js +228 -0
  41. package/dist/playground/worker/python.js +264 -58
  42. package/dist/terminal/Terminal.svelte +98 -18
  43. package/dist/terminal/Terminal.svelte.d.ts +4 -1
  44. package/dist/terminal/theme.js +2 -2
  45. package/dist/terminal/types.d.ts +4 -2
  46. package/package.json +11 -11
@@ -1,3 +1,5 @@
1
+ import { flushQueuedStdin, readBufferedStdin, waitForBufferedStdin } from '../stdinBuffer';
2
+ import { configureWorkerRuntimeAssets, handleWorkerAssetMessage } from './assets';
1
3
  self.document = {
2
4
  querySelector() {
3
5
  return null;
@@ -6,8 +8,7 @@ self.document = {
6
8
  return [];
7
9
  }
8
10
  };
9
- let stdinBufferPyodide, interruptBufferPyodide, pyodide, path = '', packageBaseUrl = '';
10
- let jungolRobotReady = false;
11
+ let stdinBufferPyodide, debugBufferPyodide, watchBufferPyodide, watchResultBufferPyodide, interruptBufferPyodide, pyodide, baseUrl = '', packageBaseUrl = '';
11
12
  const imageHook = `
12
13
  if not globals().get("__wasm_idle_img_inited__", False):
13
14
  globals()["__wasm_idle_img_inited__"] = True
@@ -156,33 +157,12 @@ async function loadPyodide(path) {
156
157
  if (pyodide)
157
158
  return;
158
159
  const { loadPyodide } = await import('pyodide');
159
- pyodide = await loadPyodide({ indexURL: path + '/pyodide' });
160
- packageBaseUrl = path + '/pyodide';
160
+ pyodide = await loadPyodide({ indexURL: path });
161
+ packageBaseUrl = path;
161
162
  const setCdnUrl = pyodide?.setCdnUrl;
162
163
  if (typeof setCdnUrl === 'function')
163
164
  setCdnUrl(packageBaseUrl);
164
165
  }
165
- async function ensureRobotJungol() {
166
- if (jungolRobotReady || !pyodide)
167
- return;
168
- try {
169
- const res = await fetch(path + '/jungol-robot/jungol_robot.zip');
170
- if (!res.ok)
171
- throw new Error('jungol-robot zip not found');
172
- const data = await res.arrayBuffer();
173
- const siteRaw = pyodide.runPython('import site; site.getsitepackages()[0]');
174
- const sitePath = typeof siteRaw === 'string' ? siteRaw : siteRaw.toString();
175
- const unpackArchive = pyodide?.unpackArchive;
176
- if (typeof unpackArchive !== 'function')
177
- throw new Error('pyodide.unpackArchive unavailable');
178
- unpackArchive(data, 'zip', sitePath);
179
- pyodide.runPython('import importlib; importlib.invalidate_caches()');
180
- jungolRobotReady = true;
181
- }
182
- catch (e) {
183
- console.warn('jungol-robot preload failed', e);
184
- }
185
- }
186
166
  async function loadPackages(code) {
187
167
  if (!code)
188
168
  return;
@@ -202,20 +182,22 @@ async function loadPackages(code) {
202
182
  }
203
183
  }
204
184
  self.onmessage = async (event) => {
205
- const { code, buffer, load, interrupt, path: _p, prepare } = event.data;
185
+ if (handleWorkerAssetMessage(event.data))
186
+ return;
187
+ const { code, buffer, debugBuffer, watchBuffer, watchResultBuffer, load, interrupt, assets, prepare, debug = false, breakpoints = [], pauseOnEntry = false } = event.data;
206
188
  if (load) {
207
- path = _p;
189
+ const runtimeAssets = assets;
190
+ baseUrl = runtimeAssets?.baseUrl || baseUrl;
191
+ configureWorkerRuntimeAssets(runtimeAssets || null);
208
192
  postMessage({ output: 'Loading Pyodide...' });
209
- await loadPyodide(path);
210
- await ensureRobotJungol();
193
+ await loadPyodide(baseUrl);
211
194
  postMessage({ output: ' Done.\n\r' });
212
195
  postMessage({ load: true });
213
196
  }
214
197
  else if (prepare) {
215
198
  postMessage({ output: 'Loading packages...' });
216
199
  try {
217
- await loadPyodide(path);
218
- await ensureRobotJungol();
200
+ await loadPyodide(baseUrl);
219
201
  await loadPackages(code);
220
202
  postMessage({ output: ' Done.\n\r' });
221
203
  self.postMessage({ results: true });
@@ -225,11 +207,13 @@ self.onmessage = async (event) => {
225
207
  }
226
208
  }
227
209
  else if (code) {
228
- await loadPyodide(path);
229
- await ensureRobotJungol();
210
+ await loadPyodide(baseUrl);
230
211
  await loadPackages(code);
231
212
  const ts = Date.now();
232
213
  stdinBufferPyodide = new Int32Array(buffer);
214
+ debugBufferPyodide = new Int32Array(debugBuffer);
215
+ watchBufferPyodide = new Int32Array(watchBuffer);
216
+ watchResultBufferPyodide = new Int32Array(watchResultBuffer);
233
217
  interruptBufferPyodide = new Uint8Array(interrupt);
234
218
  pyodide.setInterruptBuffer(interruptBufferPyodide);
235
219
  const toPythonStr = (obj) => {
@@ -246,23 +230,7 @@ self.onmessage = async (event) => {
246
230
  self.prompt = self['__pyodide__input_' + ts] = (output) => {
247
231
  if (output)
248
232
  postMessage({ output });
249
- while (true) {
250
- postMessage({ buffer: true });
251
- const res = Atomics.wait(stdinBufferPyodide, 0, 0, 100);
252
- if (res === 'not-equal') {
253
- try {
254
- const cpy = new Int32Array(stdinBufferPyodide.byteLength);
255
- cpy.set(stdinBufferPyodide);
256
- stdinBufferPyodide.fill(0);
257
- const dec = new TextDecoder();
258
- const strInfo = dec.decode(cpy).replace(/\x00/g, ''), padding = parseInt(strInfo.slice(-1));
259
- return strInfo.slice(0, -padding);
260
- }
261
- catch (e) {
262
- postMessage({ log: { e } });
263
- }
264
- }
265
- }
233
+ return waitForBufferedStdin(stdinBufferPyodide, () => postMessage({ buffer: true }));
266
234
  };
267
235
  self['__pyodide__output_' + ts] = (...data) => {
268
236
  let sep = ' ', end = '\r\n', output = '', clear = [];
@@ -284,24 +252,262 @@ self.onmessage = async (event) => {
284
252
  output += end;
285
253
  postMessage({ output });
286
254
  };
255
+ const debugPauseName = `__wasm_idle_python_debug_pause_${ts}`;
256
+ const debugWaitName = `__wasm_idle_python_debug_wait_${ts}`;
257
+ const debugReadWatchName = `__wasm_idle_python_debug_watch_read_${ts}`;
258
+ const debugWriteWatchName = `__wasm_idle_python_debug_watch_write_${ts}`;
259
+ self[debugPauseName] = (line, reason, localsJson, callStackJson) => {
260
+ let locals = [];
261
+ let callStack = [];
262
+ try {
263
+ locals = JSON.parse(localsJson);
264
+ }
265
+ catch {
266
+ locals = [];
267
+ }
268
+ try {
269
+ callStack = JSON.parse(callStackJson);
270
+ }
271
+ catch {
272
+ callStack = [];
273
+ }
274
+ postMessage({
275
+ debugEvent: {
276
+ type: 'pause',
277
+ line: Number(line),
278
+ reason,
279
+ locals,
280
+ callStack
281
+ }
282
+ });
283
+ };
284
+ self[debugWaitName] = () => {
285
+ const sequence = Atomics.load(debugBufferPyodide, 0);
286
+ while (true) {
287
+ if (interruptBufferPyodide?.[0] === 2)
288
+ return -1;
289
+ Atomics.wait(debugBufferPyodide, 0, sequence, 100);
290
+ if (interruptBufferPyodide?.[0] === 2)
291
+ return -1;
292
+ const command = Atomics.exchange(debugBufferPyodide, 1, 0);
293
+ if (command)
294
+ return command;
295
+ }
296
+ };
297
+ self[debugReadWatchName] = () => readBufferedStdin(watchBufferPyodide) || '';
298
+ self[debugWriteWatchName] = (value) => {
299
+ flushQueuedStdin([value], watchResultBufferPyodide);
300
+ };
301
+ const userFilename = '__wasm_idle_user__.py';
302
+ const normalizedBreakpoints = JSON.stringify([...(Array.isArray(breakpoints) ? breakpoints : [])]
303
+ .map((value) => Number(value))
304
+ .filter((value) => Number.isInteger(value) && value > 0));
287
305
  try {
288
- await pyodide.runPythonAsync(`import asyncio
306
+ await pyodide.runPythonAsync(`import ast
307
+ import builtins
308
+ import inspect
309
+ import json
310
+ import sys
289
311
  from js import __pyodide__input_${ts}, __pyodide__output_${ts}
312
+ ${debug ? `from js import ${debugPauseName}, ${debugWaitName}` : ''}
313
+ ${debug ? `from js import ${debugReadWatchName}, ${debugWriteWatchName}` : ''}
290
314
 
291
- input = __pyodide__input_${ts}
292
- print = __pyodide__output_${ts}
293
-
294
- __builtins__.input = __pyodide__input_${ts}
295
- __builtins__.print = __pyodide__output_${ts}
315
+ __wasm_idle_input = __pyodide__input_${ts}
316
+ def __wasm_idle_input_wrapper(prompt = ""):
317
+ value = __wasm_idle_input(prompt)
318
+ if value is None:
319
+ raise EOFError
320
+ return value
321
+ __wasm_idle_output = __pyodide__output_${ts}
322
+ builtins.input = __wasm_idle_input_wrapper
323
+ builtins.print = __wasm_idle_output
296
324
 
297
325
  ${imageHook}
298
326
 
299
- ${code}`);
327
+ ${debug
328
+ ? `
329
+ __wasm_idle_debug_breakpoints = set(${normalizedBreakpoints})
330
+ __wasm_idle_debug_pause_on_entry = ${pauseOnEntry ? 'True' : 'False'}
331
+ __wasm_idle_debug_step_mode = None
332
+ __wasm_idle_debug_resume_skip = None
333
+ __wasm_idle_debug_next_depth = None
334
+ __wasm_idle_debug_next_line = None
335
+ __wasm_idle_debug_step_out_depth = None
336
+
337
+ def __wasm_idle_debug_depth(frame):
338
+ depth = 0
339
+ current = frame
340
+ while current is not None:
341
+ if current.f_code.co_filename == "${userFilename}":
342
+ depth += 1
343
+ current = current.f_back
344
+ return depth
345
+
346
+ def __wasm_idle_debug_preview(value, depth = 0):
347
+ if depth >= 2:
348
+ return "..."
349
+ if value is None:
350
+ return "None"
351
+ if isinstance(value, bool):
352
+ return "True" if value else "False"
353
+ if isinstance(value, (int, float)):
354
+ return repr(value)
355
+ if isinstance(value, (bytes, bytearray)):
356
+ text = repr(bytes(value))
357
+ return text if len(text) <= 80 else text[:77] + "..."
358
+ if isinstance(value, str):
359
+ text = repr(value)
360
+ return text if len(text) <= 80 else text[:77] + "..."
361
+ if isinstance(value, list):
362
+ items = [__wasm_idle_debug_preview(item, depth + 1) for item in value[:8]]
363
+ if len(value) > 8:
364
+ items.append("...")
365
+ return "[" + ", ".join(items) + "]"
366
+ if isinstance(value, tuple):
367
+ items = [__wasm_idle_debug_preview(item, depth + 1) for item in value[:8]]
368
+ if len(value) > 8:
369
+ items.append("...")
370
+ if len(value) == 1 and items:
371
+ return "(" + items[0] + ",)"
372
+ return "(" + ", ".join(items) + ")"
373
+ if isinstance(value, dict):
374
+ items = []
375
+ for index, (key, item) in enumerate(value.items()):
376
+ if index >= 6:
377
+ items.append("...")
378
+ break
379
+ items.append(__wasm_idle_debug_preview(key, depth + 1) + ": " + __wasm_idle_debug_preview(item, depth + 1))
380
+ return "{" + ", ".join(items) + "}"
381
+ if isinstance(value, set):
382
+ items = [__wasm_idle_debug_preview(item, depth + 1) for item in sorted(list(value), key = repr)[:6]]
383
+ if len(value) > 6:
384
+ items.append("...")
385
+ return "{" + ", ".join(items) + "}"
386
+ try:
387
+ text = repr(value)
388
+ return text if len(text) <= 80 else text[:77] + "..."
389
+ except Exception:
390
+ return "?"
391
+
392
+ def __wasm_idle_debug_locals(frame):
393
+ locals_preview = []
394
+ for name, value in frame.f_locals.items():
395
+ if name == "__builtins__" or name.startswith("__wasm_idle_") or name.startswith("."):
396
+ continue
397
+ locals_preview.append({"name": name, "value": __wasm_idle_debug_preview(value)})
398
+ locals_preview.sort(key = lambda item: item["name"])
399
+ return locals_preview
400
+
401
+ def __wasm_idle_debug_stack(frame):
402
+ stack = []
403
+ current = frame
404
+ while current is not None:
405
+ if current.f_code.co_filename == "${userFilename}":
406
+ stack.append({"functionName": current.f_code.co_name, "line": current.f_lineno})
407
+ current = current.f_back
408
+ return stack
409
+
410
+ def __wasm_idle_debug_trace(frame, event, arg):
411
+ global __wasm_idle_debug_pause_on_entry
412
+ global __wasm_idle_debug_step_mode
413
+ global __wasm_idle_debug_resume_skip
414
+ global __wasm_idle_debug_next_depth
415
+ global __wasm_idle_debug_next_line
416
+ global __wasm_idle_debug_step_out_depth
417
+
418
+ if frame.f_code.co_filename != "${userFilename}":
419
+ return None
420
+ if event != "line":
421
+ return __wasm_idle_debug_trace
422
+
423
+ depth = __wasm_idle_debug_depth(frame)
424
+ line = frame.f_lineno
425
+ if __wasm_idle_debug_resume_skip == (depth, line):
426
+ return __wasm_idle_debug_trace
427
+ if __wasm_idle_debug_resume_skip is not None:
428
+ __wasm_idle_debug_resume_skip = None
429
+
430
+ reason = None
431
+ if __wasm_idle_debug_pause_on_entry:
432
+ reason = "entry"
433
+ elif line in __wasm_idle_debug_breakpoints:
434
+ reason = "breakpoint"
435
+ elif __wasm_idle_debug_step_mode == "step":
436
+ reason = "step"
437
+ elif __wasm_idle_debug_step_mode == "next" and __wasm_idle_debug_next_depth is not None and depth <= __wasm_idle_debug_next_depth and line != __wasm_idle_debug_next_line:
438
+ reason = "nextLine"
439
+ elif __wasm_idle_debug_step_mode == "out" and __wasm_idle_debug_step_out_depth is not None and depth <= __wasm_idle_debug_step_out_depth:
440
+ reason = "stepOut"
441
+
442
+ if reason is None:
443
+ return __wasm_idle_debug_trace
444
+
445
+ __wasm_idle_debug_pause_on_entry = False
446
+ __wasm_idle_debug_step_mode = None
447
+ __wasm_idle_debug_next_depth = None
448
+ __wasm_idle_debug_next_line = None
449
+ __wasm_idle_debug_step_out_depth = None
450
+
451
+ ${debugPauseName}(line, reason, json.dumps(__wasm_idle_debug_locals(frame)), json.dumps(__wasm_idle_debug_stack(frame)))
452
+ while True:
453
+ command = ${debugWaitName}()
454
+ if command < 0:
455
+ raise KeyboardInterrupt()
456
+ if command != 5:
457
+ break
458
+ try:
459
+ expression = ${debugReadWatchName}()
460
+ result = __wasm_idle_debug_preview(eval(expression, frame.f_globals, frame.f_locals))
461
+ except Exception as error:
462
+ result = "?" if error.__class__.__name__ == "NameError" else "error"
463
+ ${debugWriteWatchName}(result)
464
+ __wasm_idle_debug_resume_skip = (depth, line)
465
+ if command == 2:
466
+ __wasm_idle_debug_step_mode = "step"
467
+ elif command == 3:
468
+ __wasm_idle_debug_step_mode = "next"
469
+ __wasm_idle_debug_next_depth = depth
470
+ __wasm_idle_debug_next_line = line
471
+ elif command == 4:
472
+ __wasm_idle_debug_step_mode = "out"
473
+ __wasm_idle_debug_step_out_depth = max(0, depth - 1)
474
+ return __wasm_idle_debug_trace
475
+
476
+ sys.settrace(__wasm_idle_debug_trace)
477
+ `
478
+ : ''}
479
+
480
+ try:
481
+ __wasm_idle_globals = {"__name__": "__main__"}
482
+ __wasm_idle_result = eval(
483
+ compile(${JSON.stringify(code)}, "${userFilename}", "exec", flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT),
484
+ __wasm_idle_globals,
485
+ __wasm_idle_globals,
486
+ )
487
+ if inspect.isawaitable(__wasm_idle_result):
488
+ await __wasm_idle_result
489
+ finally:
490
+ sys.settrace(None)
491
+ ${debug
492
+ ? `
493
+ __wasm_idle_debug_step_mode = None
494
+ __wasm_idle_debug_resume_skip = None
495
+ __wasm_idle_debug_next_depth = None
496
+ __wasm_idle_debug_next_line = None
497
+ __wasm_idle_debug_step_out_depth = None
498
+ `
499
+ : ''}
500
+ `);
300
501
  self.postMessage({ results: true });
301
502
  }
302
503
  catch (e) {
303
504
  self.postMessage({ error: e.message || 'Unknown error' });
304
505
  }
506
+ finally {
507
+ delete self[debugPauseName];
508
+ delete self[debugWaitName];
509
+ delete self[debugReadWatchName];
510
+ delete self[debugWriteWatchName];
511
+ }
305
512
  }
306
513
  };
307
- export {};
@@ -1,5 +1,11 @@
1
1
  <script lang="ts">
2
- import type { DebugCommand, DebugSessionEvent, SandboxExecutionOptions } from '../playground/options';
2
+ import type {
3
+ CompilerDiagnostic,
4
+ DebugCommand,
5
+ DebugSessionEvent,
6
+ SandboxExecutionOptions
7
+ } from '../playground/options';
8
+ import type { PlaygroundRuntimeAssets } from '../playground/assets';
3
9
  import type { Sandbox } from '../playground/sandbox';
4
10
  import type { TerminalControl } from './types';
5
11
  import { Theme, registerAllPlugins } from './';
@@ -11,12 +17,14 @@
11
17
  interface Props {
12
18
  dark?: boolean;
13
19
  path?: string;
20
+ runtimeAssets?: string | PlaygroundRuntimeAssets;
14
21
  font?: string;
15
22
 
16
23
  onload?: () => void;
17
24
  onfinish?: () => void;
18
25
  onkey?: (e: KeyboardEvent) => void;
19
26
  ondebug?: (event: DebugSessionEvent) => void;
27
+ oncompilediagnostic?: (diagnostic: CompilerDiagnostic) => void;
20
28
  onimage?: (payload: { mime: string; b64: string; ts?: number }) => void;
21
29
  terminal?: TerminalControl;
22
30
  }
@@ -24,11 +32,13 @@
24
32
  let {
25
33
  dark = false,
26
34
  path = '',
35
+ runtimeAssets = undefined,
27
36
  font = "'D2 coding', monospace",
28
37
  onload,
29
38
  onfinish,
30
39
  onkey,
31
40
  ondebug,
41
+ oncompilediagnostic,
32
42
  onimage,
33
43
  terminal = $bindable()
34
44
  }: Props = $props();
@@ -43,7 +53,22 @@
43
53
  first = true,
44
54
  tc = 0,
45
55
  plugin = $state(),
46
- ll: string | null = null;
56
+ ll: string | null = null,
57
+ stopRequested = false;
58
+
59
+ function phaseProgress(
60
+ progress: { set?: (value: number) => void } | undefined,
61
+ start: number,
62
+ end: number
63
+ ) {
64
+ if (!progress) return undefined;
65
+ return {
66
+ set(value: number) {
67
+ const clamped = Number.isFinite(value) ? Math.min(Math.max(value, 0), 1) : 0;
68
+ progress.set?.(start + (end - start) * clamped);
69
+ }
70
+ };
71
+ }
47
72
 
48
73
  function wait() {
49
74
  return new Promise<void>((r) => {
@@ -69,6 +94,7 @@
69
94
  }
70
95
  sandbox.image = onimage;
71
96
  sandbox.ondebug = ondebug;
97
+ sandbox.oncompilerdiagnostic = oncompilediagnostic;
72
98
  sandbox.output = (output: string) =>
73
99
  _tc === tc && term && term.write(output.replaceAll('\n', '\r\n'));
74
100
  }
@@ -80,10 +106,12 @@
80
106
  return x;
81
107
  })
82
108
  .catch((msg) => {
109
+ if (stopRequested) return false;
83
110
  term?.write(`\r\n\x1B[1;3;31m${msg}\u001B[?25l`);
84
111
  return false;
85
112
  })
86
113
  .finally(() => {
114
+ stopRequested = false;
87
115
  onfinish?.();
88
116
  finish = true;
89
117
  if (term) term.options.cursorBlink = false;
@@ -94,18 +122,40 @@
94
122
  await wait();
95
123
  if (!term) return;
96
124
  term.options.cursorBlink = blink;
125
+ term.write('\u001B[?25h');
97
126
  term.focus();
98
127
 
99
128
  if (!first) term?.write(`\r\n\x1b[0m`);
100
129
  first = false;
101
130
  }
102
131
 
132
+ function appendInputText(text: string) {
133
+ if (!text) return;
134
+ input += text;
135
+ term?.write(text);
136
+ }
137
+
138
+ function submitCurrentInput() {
139
+ term?.write('\r\n');
140
+ sandbox.write?.(input + '\n');
141
+ input = '';
142
+ }
143
+
144
+ function applyPastedText(text: string) {
145
+ const lines = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n');
146
+ for (let i = 0; i < lines.length; i++) {
147
+ appendInputText(lines[i]);
148
+ if (i < lines.length - 1) submitCurrentInput();
149
+ }
150
+ }
151
+
103
152
  const terminalControl: TerminalControl = {
104
153
  async clear() {
105
154
  await wait();
106
155
  term?.reset();
107
156
  term?.write(`\u001B[?25l\x1b[0m\x1b[?25h`);
108
157
  if (term) term.options.cursorBlink = false;
158
+ input = '';
109
159
  first = true;
110
160
  await new Promise((r) => setTimeout(r, 100));
111
161
  },
@@ -117,11 +167,18 @@
117
167
  args: string[] = [],
118
168
  options: SandboxExecutionOptions = {}
119
169
  ) {
170
+ const loadProgress = phaseProgress(prog, 0, 0.85);
171
+ const prepareProgress = phaseProgress(prog, 0.85, 0.99);
120
172
  await Promise.all([
121
- initSandbox(language).then(() => sandbox.load(path, code, log, args, options)),
173
+ initSandbox(language).then(() =>
174
+ sandbox.load(runtimeAssets || path, code, log, args, options, loadProgress)
175
+ ),
122
176
  initTerm(false)
123
177
  ]);
124
- return !!(await runSandbox(sandbox.run(code, true, log, prog, args, options)));
178
+ prepareProgress?.set?.(0);
179
+ return !!(await runSandbox(
180
+ sandbox.run(code, true, log, prepareProgress, args, options)
181
+ ));
125
182
  },
126
183
  async run(
127
184
  language: string,
@@ -132,7 +189,9 @@
132
189
  options: SandboxExecutionOptions = {}
133
190
  ) {
134
191
  await Promise.all([
135
- initSandbox(language).then(() => sandbox.load(path, code, log, args, options)),
192
+ initSandbox(language).then(() =>
193
+ sandbox.load(runtimeAssets || path, code, log, args, options, prog)
194
+ ),
136
195
  initTerm()
137
196
  ]);
138
197
  return await runSandbox(sandbox.run(code, false, log, prog, args, options));
@@ -142,14 +201,25 @@
142
201
  term?.dispose();
143
202
  if (sandbox) await sandbox.clear();
144
203
  },
204
+ async stop() {
205
+ await wait();
206
+ stopRequested = true;
207
+ finish = true;
208
+ if (sandbox?.kill) sandbox.kill();
209
+ else sandbox?.terminate?.();
210
+ },
145
211
  async debugCommand(command: DebugCommand) {
146
212
  await wait();
147
213
  sandbox.debugCommand?.(command);
148
214
  },
215
+ async debugEvaluate(expression: string) {
216
+ await wait();
217
+ return (await sandbox.debugEvaluate?.(expression)) || '?';
218
+ },
149
219
  async write(input: string) {
150
220
  await wait();
151
- term?.write(input + '\r\n');
152
- sandbox.write?.(input.replaceAll('\r', '\n') + '\n');
221
+ applyPastedText(input);
222
+ if (!input.endsWith('\n') && !input.endsWith('\r')) submitCurrentInput();
153
223
  }
154
224
  };
155
225
 
@@ -186,9 +256,7 @@
186
256
  const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey;
187
257
  onkey?.(ev);
188
258
  if (ev.key === 'Enter') {
189
- term.write('\r\n');
190
- sandbox.write?.(input + '\n');
191
- input = '';
259
+ submitCurrentInput();
192
260
  } else if (ev.key === 'Backspace') {
193
261
  if (term.buffer.active.cursorX > 0) {
194
262
  term.write('\b \b');
@@ -200,17 +268,23 @@
200
268
  e.key <= String.fromCharCode(0x7e)) ||
201
269
  e.key >= '\u00a0'
202
270
  ) {
203
- input += e.key;
204
- term.write(e.key);
271
+ appendInputText(e.key);
272
+ }
273
+ } else if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'c') {
274
+ if (term.hasSelection()) {
275
+ const selectedText = term.getSelection();
276
+ if (selectedText) {
277
+ navigator.clipboard.writeText(selectedText).catch(() => {});
278
+ return;
279
+ }
205
280
  }
206
- } else if ((ev.ctrlKey || ev.metaKey) && ev.key === 'c') {
207
- sandbox.kill?.();
208
- } else if ((ev.ctrlKey || ev.metaKey) && ev.key === 'd') {
209
281
  sandbox.kill?.();
210
- } else if ((ev.ctrlKey || ev.metaKey) && ev.key === 'v') {
282
+ } else if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'd') {
283
+ if (input.length > 0) submitCurrentInput();
284
+ else sandbox.eof?.();
285
+ } else if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'v') {
211
286
  navigator.clipboard.readText().then((text) => {
212
- term?.write(text);
213
- sandbox.write?.(text);
287
+ applyPastedText(text);
214
288
  });
215
289
  }
216
290
  });
@@ -242,4 +316,10 @@
242
316
  width: 100%;
243
317
  height: 100%;
244
318
  }
319
+
320
+ :global(.xterm),
321
+ :global(.xterm .xterm-viewport),
322
+ :global(.xterm .composition-view) {
323
+ background-color: transparent;
324
+ }
245
325
  </style>
@@ -1,14 +1,17 @@
1
- import type { DebugSessionEvent } from '../playground/options';
1
+ import type { CompilerDiagnostic, DebugSessionEvent } from '../playground/options';
2
+ import type { PlaygroundRuntimeAssets } from '../playground/assets';
2
3
  import type { TerminalControl } from './types';
3
4
  import '@xterm/xterm/css/xterm.css';
4
5
  interface Props {
5
6
  dark?: boolean;
6
7
  path?: string;
8
+ runtimeAssets?: string | PlaygroundRuntimeAssets;
7
9
  font?: string;
8
10
  onload?: () => void;
9
11
  onfinish?: () => void;
10
12
  onkey?: (e: KeyboardEvent) => void;
11
13
  ondebug?: (event: DebugSessionEvent) => void;
14
+ oncompilediagnostic?: (diagnostic: CompilerDiagnostic) => void;
12
15
  onimage?: (payload: {
13
16
  mime: string;
14
17
  b64: string;
@@ -1,7 +1,7 @@
1
1
  const theme = {
2
2
  Tango_Dark: {
3
3
  foreground: '#eee',
4
- background: '#111827',
4
+ background: 'transparent',
5
5
  cursor: '#FFFFFF',
6
6
  cursorAccent: '#ffffff',
7
7
  selection: '#ffffff44',
@@ -24,7 +24,7 @@ const theme = {
24
24
  },
25
25
  Tango_Light: {
26
26
  foreground: '#111',
27
- background: '#FFFFFF',
27
+ background: 'rgba(0, 0, 0, 0)',
28
28
  cursor: '#000000',
29
29
  cursorAccent: '#000000',
30
30
  selection: '#00000044',
@@ -1,4 +1,4 @@
1
- import type { DebugCommand, DebugFrame, DebugSessionEvent, DebugVariable, SandboxExecutionOptions } from '../playground/options';
1
+ import type { CompilerDiagnostic, DebugCommand, DebugFrame, DebugSessionEvent, DebugVariable, SandboxExecutionOptions } from '../playground/options';
2
2
  export interface ProgressLike {
3
3
  set?: (value: number) => void;
4
4
  }
@@ -7,7 +7,9 @@ export interface TerminalControl {
7
7
  prepare: (language: string, code: string, log?: boolean, prog?: ProgressLike, args?: string[], options?: SandboxExecutionOptions) => Promise<boolean>;
8
8
  run: (language: string, code: string, log?: boolean, prog?: ProgressLike, args?: string[], options?: SandboxExecutionOptions) => Promise<boolean | string>;
9
9
  destroy: () => Promise<void>;
10
+ stop?: () => Promise<void>;
10
11
  debugCommand: (command: DebugCommand) => Promise<void>;
12
+ debugEvaluate?: (expression: string) => Promise<string>;
11
13
  write: (input: string) => Promise<void>;
12
14
  }
13
- export type { DebugCommand, DebugFrame, DebugSessionEvent, DebugVariable, SandboxExecutionOptions };
15
+ export type { CompilerDiagnostic, DebugCommand, DebugFrame, DebugSessionEvent, DebugVariable, SandboxExecutionOptions };