hyperbook 0.99.0 → 0.100.0

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.
@@ -23,6 +23,7 @@
23
23
  var PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js";
24
24
  var PYTAMARO_URI_BEGIN = "@@@PYTAMARO_DATA_URI_BEGIN@@@";
25
25
  var PYTAMARO_URI_END = "@@@PYTAMARO_DATA_URI_END@@@";
26
+ var scriptLooksLikeTurtle = (script) => /\bfrom\s+turtle\s+import\b|\bimport\s+turtle\b/.test(String(script || ""));
26
27
 
27
28
  // assets/directive-pyide/src/output.js
28
29
  var getOutput = (id) => {
@@ -184,11 +185,77 @@
184
185
  pytamaroStdoutCarry.delete(id);
185
186
  };
186
187
 
188
+ // assets/directive-pyide/src/stdin.js
189
+ var pending = /* @__PURE__ */ new Map();
190
+ var parts = (id) => {
191
+ const elem = document.getElementById(id);
192
+ if (!elem) return null;
193
+ const form = elem.getElementsByClassName("stdin")[0];
194
+ const field = elem.getElementsByClassName("stdin-field")[0];
195
+ if (!form || !field) return null;
196
+ const label = elem.getElementsByClassName("stdin-prompt")[0] || null;
197
+ return { form, field, label };
198
+ };
199
+ var hideStdinField = (id) => {
200
+ const found = parts(id);
201
+ if (!found) return;
202
+ found.form.classList.add("hidden");
203
+ found.field.value = "";
204
+ if (found.label) found.label.textContent = "";
205
+ };
206
+ var cancelStdin = (id, value = "") => {
207
+ const entry = pending.get(id);
208
+ if (!entry) return;
209
+ pending.delete(id);
210
+ entry.cleanup();
211
+ hideStdinField(id);
212
+ entry.resolve(value);
213
+ };
214
+ var askStdinAsync = (id, promptText = "") => {
215
+ const found = parts(id);
216
+ if (!found) return Promise.resolve(askStdinSync(id, promptText));
217
+ cancelStdin(id);
218
+ const { form, field, label } = found;
219
+ if (label) label.textContent = String(promptText ?? "").trimEnd();
220
+ return new Promise((resolve) => {
221
+ const onSubmit = (event) => {
222
+ event.preventDefault();
223
+ const value = field.value;
224
+ pending.delete(id);
225
+ cleanup();
226
+ hideStdinField(id);
227
+ resolve(value);
228
+ };
229
+ const cleanup = () => form.removeEventListener("submit", onSubmit);
230
+ pending.set(id, { resolve, cleanup });
231
+ form.addEventListener("submit", onSubmit);
232
+ form.classList.remove("hidden");
233
+ field.value = "";
234
+ field.focus();
235
+ form.scrollIntoView?.({ block: "nearest" });
236
+ });
237
+ };
238
+ var askStdinSync = (id, promptText = "") => {
239
+ const text = promptText || hyperbook.i18n.get("pyide-input-prompt");
240
+ const value = window.prompt(text);
241
+ return value === null ? "" : value;
242
+ };
243
+
187
244
  // assets/directive-pyide/src/turtle-ffi.js
188
245
  var createTurtleJsFFI = (id) => {
189
246
  const DEFAULT_LINE_WIDTH = 1;
190
247
  const DEFAULT_FONT_SIZE = 8;
191
248
  const DEFAULT_SHAPE = "classic";
249
+ const DEFAULT_SPEED = 3;
250
+ const DEFAULT_DELAY_MS = 80;
251
+ const DEFAULT_UNDO_BUFFER = 1e3;
252
+ const SPEED_NAMES = {
253
+ fastest: 0,
254
+ fast: 10,
255
+ normal: 6,
256
+ slow: 3,
257
+ slowest: 1
258
+ };
192
259
  const TURTLE_SHAPES = {
193
260
  classic: [
194
261
  [0, 0],
@@ -212,8 +279,9 @@
212
279
  [10, 10],
213
280
  [-10, 10]
214
281
  ],
215
- circle: null,
282
+ circle: "arc",
216
283
  // rendered as arc, not polygon
284
+ blank: [],
217
285
  turtle: [
218
286
  [16, 0],
219
287
  [14, 2],
@@ -251,16 +319,25 @@
251
319
  let backgroundColor = "#ffffff";
252
320
  let backgroundImage = null;
253
321
  let colorMode = 1;
254
- let delayMs = 80;
255
- let turtleSpeed = 3;
322
+ let delayMs = DEFAULT_DELAY_MS;
323
+ let turtleSpeed = DEFAULT_SPEED;
256
324
  let screenWidth = 640;
257
325
  let screenHeight = 480;
326
+ let screenMode = "standard";
327
+ let screenTitle = "";
328
+ let tracerN = 1;
329
+ let tracerCounter = 0;
258
330
  let operationQueue = [];
259
331
  let queueGeneration = 0;
260
332
  let queueRunning = false;
333
+ let stampCounter = 0;
334
+ let undoBufferSize = DEFAULT_UNDO_BUFFER;
335
+ let eventCleanups = [];
336
+ let pendingTimers = [];
261
337
  const textMeasureCanvas = document.createElement("canvas");
262
338
  const textMeasureContext = textMeasureCanvas.getContext("2d");
263
339
  const allPens = [];
340
+ const getCanvas = (elementId) => document.getElementById(elementId)?.getElementsByClassName("canvas")[0] || null;
264
341
  const normalizeAngle = (angle) => {
265
342
  const value = Number(angle) || 0;
266
343
  return (value % 360 + 360) % 360;
@@ -358,9 +435,9 @@
358
435
  const normalizeFontStyle = (value) => {
359
436
  const style = toPlainString(value, "normal").trim().toLowerCase();
360
437
  if (!style || style === "normal") return "";
361
- const parts = style.split(/\s+/).filter(Boolean);
362
- const hasItalic = parts.includes("italic");
363
- const hasBold = parts.includes("bold");
438
+ const parts2 = style.split(/\s+/).filter(Boolean);
439
+ const hasItalic = parts2.includes("italic");
440
+ const hasBold = parts2.includes("bold");
364
441
  if (hasItalic && hasBold) return "italic bold";
365
442
  if (hasBold) return "bold";
366
443
  if (hasItalic) return "italic";
@@ -407,28 +484,33 @@
407
484
  return toColorString(value.toJs({ pyproxies: [] }));
408
485
  }
409
486
  if (Array.isArray(value) || value && typeof value === "object" && "length" in value) {
410
- const parts = Array.from(value).slice(0, 3).map((part) => Number(part));
411
- if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) {
487
+ const parts2 = Array.from(value).slice(0, 3).map((part) => Number(part));
488
+ if (parts2.length !== 3 || parts2.some((part) => !Number.isFinite(part))) {
412
489
  throw new Error(`bad color sequence: ${String(value)}`);
413
490
  }
414
491
  if (colorMode === 1) {
415
- if (parts.some((part) => part < 0 || part > 1)) {
492
+ if (parts2.some((part) => part < 0 || part > 1)) {
416
493
  throw new Error(`bad color sequence: ${String(value)}`);
417
494
  }
418
- const [r, g, b] = parts.map((part) => Math.round(part * 255));
495
+ const [r, g, b] = parts2.map((part) => Math.round(part * 255));
419
496
  return `rgb(${r}, ${g}, ${b})`;
420
497
  }
421
498
  if (colorMode === 255) {
422
- if (parts.some((part) => part < 0 || part > 255)) {
499
+ if (parts2.some((part) => part < 0 || part > 255)) {
423
500
  throw new Error(`bad color sequence: ${String(value)}`);
424
501
  }
425
- const [r, g, b] = parts.map((part) => Math.round(part));
502
+ const [r, g, b] = parts2.map((part) => Math.round(part));
426
503
  return `rgb(${r}, ${g}, ${b})`;
427
504
  }
428
505
  throw new Error(`bad color sequence: ${String(value)}`);
429
506
  }
430
507
  return "#000000";
431
508
  };
509
+ const colorFromArgs = (args) => {
510
+ if (args.length === 0) return null;
511
+ if (args.length === 1) return toColorString(args[0]);
512
+ return toColorString(args.map((part) => toPlainNumber(part, Number.NaN)));
513
+ };
432
514
  const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
433
515
  const clearQueue = () => {
434
516
  queueGeneration += 1;
@@ -436,7 +518,7 @@
436
518
  queueRunning = false;
437
519
  };
438
520
  const enqueueOperation = (fn) => {
439
- if (delayMs <= 0) {
521
+ if (delayMs <= 0 || tracerN === 0) {
440
522
  fn();
441
523
  return;
442
524
  }
@@ -504,7 +586,36 @@
504
586
  }
505
587
  context.restore();
506
588
  };
589
+ const drawCursor = (cursor) => {
590
+ const points = TURTLE_SHAPES[cursor.shapeName] ?? TURTLE_SHAPES[DEFAULT_SHAPE];
591
+ if (Array.isArray(points) && points.length === 0) return;
592
+ context.save();
593
+ context.translate(toCanvasX(cursor.x), toCanvasY(cursor.y));
594
+ context.rotate(-toRadians(cursor.heading + (cursor.tilt || 0)));
595
+ context.scale(cursor.stretchLen || 1, cursor.stretchWid || 1);
596
+ context.fillStyle = cursor.fillColor;
597
+ context.strokeStyle = cursor.penColor;
598
+ context.lineWidth = cursor.outline || 1;
599
+ if (cursor.shapeName === "circle" || !Array.isArray(points)) {
600
+ context.beginPath();
601
+ context.arc(0, 0, 10, 0, 2 * Math.PI);
602
+ } else {
603
+ context.beginPath();
604
+ context.moveTo(points[0][0], points[0][1]);
605
+ for (let i = 1; i < points.length; i++) {
606
+ context.lineTo(points[i][0], points[i][1]);
607
+ }
608
+ context.closePath();
609
+ }
610
+ context.fill();
611
+ context.stroke();
612
+ context.restore();
613
+ };
507
614
  const drawPathSegment = (path) => {
615
+ if (path?.stamp) {
616
+ drawCursor(path.stamp);
617
+ return;
618
+ }
508
619
  if (!path?.points?.length) return;
509
620
  if (path.fill) {
510
621
  context.beginPath();
@@ -565,32 +676,21 @@
565
676
  };
566
677
  const drawTurtleShape = (pen) => {
567
678
  if (!pen.renderedTurtleVisible) return;
568
- context.save();
569
- context.translate(toCanvasX(pen.renderedX), toCanvasY(pen.renderedY));
570
- context.rotate(-toRadians(pen.renderedHeading));
571
- context.fillStyle = pen.shapeColor;
572
- context.strokeStyle = pen.shapeColor;
573
- context.lineWidth = 1;
574
- const shapeName = pen.shapeName || DEFAULT_SHAPE;
575
- if (shapeName === "circle") {
576
- context.beginPath();
577
- context.arc(0, 0, 10, 0, 2 * Math.PI);
578
- context.fill();
579
- context.stroke();
580
- } else {
581
- const points = TURTLE_SHAPES[shapeName] || TURTLE_SHAPES[DEFAULT_SHAPE];
582
- context.beginPath();
583
- context.moveTo(points[0][0], points[0][1]);
584
- for (let i = 1; i < points.length; i++) {
585
- context.lineTo(points[i][0], points[i][1]);
586
- }
587
- context.closePath();
588
- context.fill();
589
- context.stroke();
590
- }
591
- context.restore();
679
+ drawCursor({
680
+ x: pen.renderedX,
681
+ y: pen.renderedY,
682
+ heading: pen.renderedHeading,
683
+ shapeName: pen.shapeName || DEFAULT_SHAPE,
684
+ // CPython fills the cursor with fillcolor and outlines it with pencolor.
685
+ fillColor: pen.shapeFillColor,
686
+ penColor: pen.shapeColor,
687
+ stretchLen: pen.stretchLen,
688
+ stretchWid: pen.stretchWid,
689
+ outline: pen.outlineWidth,
690
+ tilt: pen.tilt
691
+ });
592
692
  };
593
- const draw = () => {
693
+ const render = () => {
594
694
  if (!active) return;
595
695
  if (!setupCanvasResolution()) return;
596
696
  drawBackground();
@@ -601,6 +701,12 @@
601
701
  drawTurtleShape(pen);
602
702
  }
603
703
  };
704
+ const draw = () => {
705
+ if (tracerN === 0) return;
706
+ tracerCounter += 1;
707
+ if (tracerN > 1 && tracerCounter % tracerN !== 0) return;
708
+ render();
709
+ };
604
710
  const createTurtlePen = () => {
605
711
  let x = 0;
606
712
  let y = 0;
@@ -609,12 +715,15 @@
609
715
  let turtleVisible = true;
610
716
  let strokeColor = "#000000";
611
717
  let fillColor = "#000000";
718
+ let penWidth = DEFAULT_LINE_WIDTH;
612
719
  let fontSize = DEFAULT_FONT_SIZE;
613
720
  let currentFontFamily = "Arial";
614
721
  let currentFontStyle = "normal";
615
722
  let filling = false;
616
723
  let fillPath = null;
617
724
  let currentPath = null;
725
+ let degreesPerUnit = 1;
726
+ const undoStack = [];
618
727
  const pen = {
619
728
  paths: [],
620
729
  renderedX: 0,
@@ -622,12 +731,29 @@
622
731
  renderedHeading: 0,
623
732
  renderedTurtleVisible: true,
624
733
  shapeColor: "#000000",
625
- shapeName: DEFAULT_SHAPE
734
+ shapeFillColor: "#000000",
735
+ shapeName: DEFAULT_SHAPE,
736
+ stretchLen: 1,
737
+ stretchWid: 1,
738
+ outlineWidth: 1,
739
+ tilt: 0
740
+ };
741
+ const toUserAngle = (internal) => normalizeAngle(screenMode === "logo" ? 90 - internal : internal) / degreesPerUnit;
742
+ const toInternalAngle = (user) => {
743
+ const degrees = Number(user || 0) * degreesPerUnit;
744
+ return normalizeAngle(screenMode === "logo" ? 90 - degrees : degrees);
745
+ };
746
+ const pushUndo = (entry) => {
747
+ if (undoBufferSize === 0) return;
748
+ undoStack.push(entry);
749
+ if (undoBufferSize > 0 && undoStack.length > undoBufferSize) {
750
+ undoStack.shift();
751
+ }
626
752
  };
627
753
  const makePath = (overrides = {}) => ({
628
754
  down: penDown,
629
755
  stroke: strokeColor,
630
- lineWidth: DEFAULT_LINE_WIDTH,
756
+ lineWidth: penWidth,
631
757
  fontsize: fontSize,
632
758
  fontfamily: currentFontFamily,
633
759
  fontstyle: currentFontStyle,
@@ -642,10 +768,7 @@
642
768
  return currentPath;
643
769
  };
644
770
  const commitStyleToNewPath = () => {
645
- currentPath = makePath({
646
- down: penDown,
647
- lineWidth: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH
648
- });
771
+ currentPath = makePath();
649
772
  pen.paths.push(currentPath);
650
773
  return currentPath;
651
774
  };
@@ -659,144 +782,206 @@
659
782
  pendown: penDown,
660
783
  pencolor: strokeColor,
661
784
  fillcolor: fillColor,
662
- pensize: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
785
+ pensize: penWidth,
663
786
  speed: turtleSpeed,
664
- shown: turtleVisible
787
+ shown: turtleVisible,
788
+ stretchfactor: [pen.stretchWid, pen.stretchLen],
789
+ outline: pen.outlineWidth,
790
+ tilt: pen.tilt
665
791
  });
666
792
  beginCurrentPath();
667
- const forward = (distance) => {
668
- if (!ensureContext()) return;
793
+ const moveTo = (nextX, nextY) => {
794
+ const prevX = x;
795
+ const prevY = y;
669
796
  const path = ensurePath();
670
797
  const fill = filling ? fillPath : null;
671
- const length = Number(distance) || 0;
672
- const nextX = x + length * Math.cos(toRadians(heading));
673
- const nextY = y + length * Math.sin(toRadians(heading));
674
798
  x = nextX;
675
799
  y = nextY;
676
800
  const point = { x, y, move: !penDown };
801
+ pushUndo(() => {
802
+ x = prevX;
803
+ y = prevY;
804
+ if (path.points[path.points.length - 1] === point) path.points.pop();
805
+ if (fill) fill.points.pop();
806
+ pen.renderedX = prevX;
807
+ pen.renderedY = prevY;
808
+ });
677
809
  enqueueOperation(() => {
678
810
  pen.renderedX = point.x;
679
811
  pen.renderedY = point.y;
680
812
  path.points.push(point);
681
813
  if (fill) {
682
- fill.points.push({ x, y });
814
+ fill.points.push({ x: point.x, y: point.y });
683
815
  }
684
816
  draw();
685
817
  });
686
818
  };
687
- const backward = (distance) => forward(-Number(distance || 0));
688
- const right = (angle) => {
689
- heading = normalizeAngle(heading - Number(angle || 0));
690
- const nextHeading = heading;
691
- enqueueOperation(() => {
692
- pen.renderedHeading = nextHeading;
693
- draw();
819
+ const turnTo = (nextHeading) => {
820
+ const prevHeading = heading;
821
+ heading = normalizeAngle(nextHeading);
822
+ const target = heading;
823
+ pushUndo(() => {
824
+ heading = prevHeading;
825
+ pen.renderedHeading = prevHeading;
694
826
  });
695
- };
696
- const left = (angle) => {
697
- heading = normalizeAngle(heading + Number(angle || 0));
698
- const nextHeading = heading;
699
827
  enqueueOperation(() => {
700
- pen.renderedHeading = nextHeading;
828
+ pen.renderedHeading = target;
701
829
  draw();
702
830
  });
703
831
  };
832
+ const forward = (distance2) => {
833
+ if (!ensureContext()) return;
834
+ const length = toPlainNumber(distance2, 0) || 0;
835
+ moveTo(
836
+ x + length * Math.cos(toRadians(heading)),
837
+ y + length * Math.sin(toRadians(heading))
838
+ );
839
+ };
840
+ const backward = (distance2) => forward(-toPlainNumber(distance2, 0));
841
+ const right = (angle) => turnTo(heading - toPlainNumber(angle, 0) * degreesPerUnit);
842
+ const left = (angle) => turnTo(heading + toPlainNumber(angle, 0) * degreesPerUnit);
704
843
  const penup = () => {
844
+ const previous = penDown;
705
845
  penDown = false;
706
846
  commitStyleToNewPath();
847
+ pushUndo(() => {
848
+ penDown = previous;
849
+ commitStyleToNewPath();
850
+ });
707
851
  draw();
708
852
  };
709
853
  const pendownFn = () => {
854
+ const previous = penDown;
710
855
  penDown = true;
711
856
  commitStyleToNewPath();
857
+ pushUndo(() => {
858
+ penDown = previous;
859
+ commitStyleToNewPath();
860
+ });
712
861
  draw();
713
862
  };
863
+ const toPoint = (a, b) => {
864
+ if (b !== void 0 && b !== null) {
865
+ return [toPlainNumber(a, 0), toPlainNumber(b, 0)];
866
+ }
867
+ const source = toSequence(a);
868
+ if (source && source.length >= 2) {
869
+ return [toPlainNumber(source[0], 0), toPlainNumber(source[1], 0)];
870
+ }
871
+ const obj = a && typeof a === "object" ? a : null;
872
+ if (obj && ("x" in obj || "y" in obj)) {
873
+ return [toPlainNumber(obj.x, 0), toPlainNumber(obj.y, 0)];
874
+ }
875
+ return [toPlainNumber(a, 0), 0];
876
+ };
714
877
  const goto_ = (a, b) => {
715
878
  if (!ensureContext()) return;
716
- let nextX = Number(a);
717
- let nextY = Number(b);
718
- if (b === void 0 && (Array.isArray(a) || a && typeof a === "object")) {
719
- const source = a && typeof a.toJs === "function" ? a.toJs({ pyproxies: [] }) : a;
720
- nextX = Number(source?.[0] ?? source?.x ?? 0);
721
- nextY = Number(source?.[1] ?? source?.y ?? 0);
722
- }
723
- x = Number.isFinite(nextX) ? nextX : 0;
724
- y = Number.isFinite(nextY) ? nextY : 0;
725
- const path = ensurePath();
726
- const fill = filling ? fillPath : null;
727
- const point = { x, y, move: !penDown };
728
- enqueueOperation(() => {
729
- pen.renderedX = point.x;
730
- pen.renderedY = point.y;
731
- path.points.push(point);
732
- if (fill) {
733
- fill.points.push({ x, y });
734
- }
735
- draw();
736
- });
879
+ const [nextX, nextY] = toPoint(a, b);
880
+ moveTo(nextX, nextY);
737
881
  };
738
- const setx = (value) => goto_(Number(value), y);
739
- const sety = (value) => goto_(x, Number(value));
882
+ const setx = (value) => goto_(toPlainNumber(value, 0), y);
883
+ const sety = (value) => goto_(x, toPlainNumber(value, 0));
740
884
  const position = () => [x, y];
741
885
  const xcor = () => x;
742
886
  const ycor = () => y;
743
- const heading_ = () => heading;
744
- const setheading = (angle) => {
745
- heading = normalizeAngle(angle);
746
- const nextHeading = heading;
747
- enqueueOperation(() => {
748
- pen.renderedHeading = nextHeading;
749
- draw();
750
- });
751
- };
887
+ const heading_ = () => toUserAngle(heading);
888
+ const setheading = (angle) => turnTo(toInternalAngle(angle));
752
889
  const home = () => {
753
890
  goto_(0, 0);
754
891
  setheading(0);
755
892
  };
756
893
  const towards = (tx, ty) => {
757
- const dx = Number(tx) - x;
758
- const dy = Number(ty) - y;
894
+ const [px, py] = toPoint(tx, ty);
895
+ const dx = px - x;
896
+ const dy = py - y;
759
897
  if (!Number.isFinite(dx) || !Number.isFinite(dy)) return 0;
760
- return normalizeAngle(Math.atan2(dy, dx) * 180 / Math.PI);
898
+ return toUserAngle(Math.atan2(dy, dx) * 180 / Math.PI);
899
+ };
900
+ const distance = (tx, ty) => {
901
+ const [px, py] = toPoint(tx, ty);
902
+ return Math.hypot(px - x, py - y);
903
+ };
904
+ const degreesFn = (fullcircle = 360) => {
905
+ const circle2 = toPlainNumber(fullcircle, 360) || 360;
906
+ degreesPerUnit = 360 / circle2;
761
907
  };
762
- const speed = (value = 0) => {
763
- const numeric = Number(value);
764
- turtleSpeed = Number.isFinite(numeric) ? numeric : 0;
908
+ const radians = () => degreesFn(2 * Math.PI);
909
+ const speed = (value = void 0) => {
910
+ if (value === void 0 || value === null) return turtleSpeed;
911
+ const name = typeof value === "string" ? value.trim().toLowerCase() : null;
912
+ let numeric = name !== null && name in SPEED_NAMES ? SPEED_NAMES[name] : toPlainNumber(value, Number.NaN);
913
+ if (!Number.isFinite(numeric)) numeric = DEFAULT_SPEED;
914
+ if (numeric < 0.5 || numeric > 10.5) numeric = 0;
915
+ turtleSpeed = Math.round(numeric);
765
916
  delayMs = turtleSpeed <= 0 ? 0 : Math.max(0, Math.round(300 / turtleSpeed));
766
- return delayMs;
917
+ return turtleSpeed;
767
918
  };
768
- const color = (...args) => {
769
- const stroke = toColorString(args[0]);
770
- const fill = args.length > 1 ? toColorString(args[1]) : stroke;
771
- strokeColor = stroke;
772
- fillColor = fill;
773
- pen.shapeColor = stroke;
774
- commitStyleToNewPath();
919
+ const setStroke = (value) => {
920
+ strokeColor = value;
921
+ pen.shapeColor = value;
922
+ };
923
+ const setFill = (value) => {
924
+ fillColor = value;
925
+ pen.shapeFillColor = value;
775
926
  if (filling && fillPath) {
776
- fillPath.fillstyle = fillColor;
927
+ fillPath.fillstyle = value;
928
+ }
929
+ };
930
+ const color = (...args) => {
931
+ if (args.length === 0) return [strokeColor, fillColor];
932
+ const previous = [strokeColor, fillColor];
933
+ if (args.length === 2) {
934
+ setStroke(toColorString(args[0]));
935
+ setFill(toColorString(args[1]));
936
+ } else {
937
+ const value = colorFromArgs(args);
938
+ setStroke(value);
939
+ setFill(value);
777
940
  }
941
+ commitStyleToNewPath();
942
+ pushUndo(() => {
943
+ setStroke(previous[0]);
944
+ setFill(previous[1]);
945
+ commitStyleToNewPath();
946
+ });
778
947
  draw();
779
948
  };
780
- const pencolor = (value) => {
781
- strokeColor = toColorString(value);
782
- pen.shapeColor = strokeColor;
949
+ const pencolor = (...args) => {
950
+ if (args.length === 0) return strokeColor;
951
+ const previous = strokeColor;
952
+ setStroke(colorFromArgs(args));
783
953
  commitStyleToNewPath();
954
+ pushUndo(() => {
955
+ setStroke(previous);
956
+ commitStyleToNewPath();
957
+ });
784
958
  draw();
785
959
  };
786
- const fillcolor = (value) => {
787
- fillColor = toColorString(value);
788
- if (filling && fillPath) {
789
- fillPath.fillstyle = fillColor;
790
- }
960
+ const fillcolor = (...args) => {
961
+ if (args.length === 0) return fillColor;
962
+ const previous = fillColor;
963
+ setFill(colorFromArgs(args));
791
964
  commitStyleToNewPath();
965
+ pushUndo(() => {
966
+ setFill(previous);
967
+ commitStyleToNewPath();
968
+ });
792
969
  draw();
793
970
  };
794
- const pensize = (value) => {
795
- ensurePath().lineWidth = Math.max(1, Number(value) || DEFAULT_LINE_WIDTH);
971
+ const pensize = (value = void 0) => {
972
+ if (value === void 0 || value === null) return penWidth;
973
+ const previous = penWidth;
974
+ penWidth = Math.max(1, toPlainNumber(value, DEFAULT_LINE_WIDTH));
796
975
  commitStyleToNewPath();
976
+ pushUndo(() => {
977
+ penWidth = previous;
978
+ commitStyleToNewPath();
979
+ });
797
980
  draw();
981
+ return penWidth;
798
982
  };
799
- const width = (value) => pensize(value);
983
+ const width = (value = void 0) => pensize(value);
984
+ const isfilling = () => filling;
800
985
  const begin_fill = () => {
801
986
  if (filling) return;
802
987
  filling = true;
@@ -804,48 +989,75 @@
804
989
  down: false,
805
990
  fill: true,
806
991
  stroke: "transparent",
807
- lineWidth: 1,
808
992
  fillstyle: fillColor
809
993
  });
810
994
  pen.paths.push(fillPath);
995
+ commitStyleToNewPath();
996
+ const started = fillPath;
997
+ pushUndo(() => {
998
+ filling = false;
999
+ fillPath = null;
1000
+ const index = pen.paths.indexOf(started);
1001
+ if (index >= 0) pen.paths.splice(index, 1);
1002
+ });
811
1003
  draw();
812
1004
  };
813
1005
  const end_fill = () => {
1006
+ if (!filling) return;
1007
+ const finished = fillPath;
814
1008
  filling = false;
815
1009
  fillPath = null;
816
1010
  commitStyleToNewPath();
1011
+ pushUndo(() => {
1012
+ filling = true;
1013
+ fillPath = finished;
1014
+ });
817
1015
  draw();
818
1016
  };
819
- const dot = (size = 5, colorValue = null) => {
1017
+ const dot = (size = void 0, ...colorArgs) => {
1018
+ const requested = toPlainNumber(size, Number.NaN);
1019
+ const diameter = Number.isFinite(requested) ? Math.max(1, requested) : Math.max(penWidth + 4, penWidth * 2);
1020
+ const dotColor = colorArgs.length > 0 ? colorFromArgs(colorArgs) : strokeColor;
820
1021
  const segment = makePath({
821
1022
  down: false,
822
1023
  fill: false,
823
- stroke: colorValue ? toColorString(colorValue) : strokeColor,
824
- fillstyle: colorValue ? toColorString(colorValue) : strokeColor,
825
- points: [
826
- {
827
- x,
828
- y,
829
- dotRadius: Math.max(0.5, Number(size) || 5) / 2,
830
- color: colorValue ? toColorString(colorValue) : strokeColor
831
- }
832
- ]
1024
+ stroke: dotColor,
1025
+ fillstyle: dotColor,
1026
+ points: [{ x, y, dotRadius: diameter / 2, color: dotColor }]
1027
+ });
1028
+ pushUndo(() => {
1029
+ const index = pen.paths.indexOf(segment);
1030
+ if (index >= 0) pen.paths.splice(index, 1);
833
1031
  });
834
1032
  enqueueOperation(() => {
835
1033
  pen.paths.push(segment);
836
1034
  draw();
837
1035
  });
838
1036
  };
839
- const circle = (radius, steps = 120) => {
840
- const numericRadius = Number(radius) || 0;
841
- const stepCount = Math.max(8, Number(steps) | 0);
842
- const circumference = 2 * Math.PI * Math.abs(numericRadius);
843
- const stepLength = circumference / stepCount;
844
- const stepTurn = 360 / stepCount * (numericRadius >= 0 ? 1 : -1);
1037
+ const circle = (radius, extent = void 0, steps = void 0) => {
1038
+ const numericRadius = toPlainNumber(radius, 0) || 0;
1039
+ const fullCircle = 360 / degreesPerUnit;
1040
+ const extentUnits = toPlainNumber(extent, fullCircle);
1041
+ const extentDegrees = extentUnits * degreesPerUnit;
1042
+ const requestedSteps = toPlainNumber(steps, Number.NaN);
1043
+ const stepCount = Number.isFinite(requestedSteps) ? Math.max(1, Math.trunc(requestedSteps)) : (
1044
+ // CPython scales the step count with the arc's size.
1045
+ 1 + Math.trunc(
1046
+ Math.min(11 + Math.abs(numericRadius) / 6, 59) * (Math.abs(extentUnits) / fullCircle)
1047
+ )
1048
+ );
1049
+ let stepTurn = extentDegrees / stepCount;
1050
+ let stepLength = 2 * numericRadius * Math.sin(toRadians(stepTurn / 2));
1051
+ if (numericRadius < 0) {
1052
+ stepLength = -stepLength;
1053
+ stepTurn = -stepTurn;
1054
+ }
1055
+ turnTo(heading + stepTurn / 2);
845
1056
  for (let index = 0; index < stepCount; index += 1) {
846
1057
  forward(stepLength);
847
- left(stepTurn);
1058
+ turnTo(heading + stepTurn);
848
1059
  }
1060
+ turnTo(heading - stepTurn / 2);
849
1061
  };
850
1062
  const write = (text, move = false, align = "left", font = null) => {
851
1063
  let writeText = text;
@@ -916,6 +1128,10 @@
916
1128
  }
917
1129
  commitStyleToNewPath();
918
1130
  }
1131
+ pushUndo(() => {
1132
+ const index = pen.paths.indexOf(segment);
1133
+ if (index >= 0) pen.paths.splice(index, 1);
1134
+ });
919
1135
  enqueueOperation(() => {
920
1136
  pen.paths.push(segment);
921
1137
  draw();
@@ -949,13 +1165,97 @@
949
1165
  if (name === void 0 || name === null) {
950
1166
  return pen.shapeName;
951
1167
  }
952
- const nameStr = toPlainString(name, DEFAULT_SHAPE).toLowerCase().trim();
953
- if (nameStr in TURTLE_SHAPES) {
954
- pen.shapeName = nameStr;
1168
+ const nameStr = toPlainString(name, "").toLowerCase().trim();
1169
+ if (!(nameStr in TURTLE_SHAPES)) {
1170
+ throw new Error(
1171
+ `There is no shape named ${nameStr} (available: ${Object.keys(
1172
+ TURTLE_SHAPES
1173
+ ).join(", ")})`
1174
+ );
955
1175
  }
1176
+ pen.shapeName = nameStr;
956
1177
  draw();
957
1178
  return pen.shapeName;
958
1179
  };
1180
+ const shapesize = (stretchWid = void 0, stretchLen = void 0, outline = void 0) => {
1181
+ if (stretchWid === void 0 && stretchLen === void 0 && outline === void 0) {
1182
+ return [pen.stretchWid, pen.stretchLen, pen.outlineWidth];
1183
+ }
1184
+ const wid = toPlainNumber(stretchWid, Number.NaN);
1185
+ const len = toPlainNumber(stretchLen, Number.NaN);
1186
+ if (Number.isFinite(wid)) {
1187
+ pen.stretchWid = wid;
1188
+ pen.stretchLen = Number.isFinite(len) ? len : wid;
1189
+ } else if (Number.isFinite(len)) {
1190
+ pen.stretchLen = len;
1191
+ }
1192
+ const outlineWidth = toPlainNumber(outline, Number.NaN);
1193
+ if (Number.isFinite(outlineWidth)) pen.outlineWidth = outlineWidth;
1194
+ draw();
1195
+ return [pen.stretchWid, pen.stretchLen, pen.outlineWidth];
1196
+ };
1197
+ const settiltangle = (angle) => {
1198
+ pen.tilt = toPlainNumber(angle, 0) * degreesPerUnit;
1199
+ draw();
1200
+ };
1201
+ const tilt = (angle) => settiltangle(pen.tilt / degreesPerUnit + toPlainNumber(angle, 0));
1202
+ const tiltangle = (angle = void 0) => {
1203
+ if (angle === void 0 || angle === null) {
1204
+ return normalizeAngle(pen.tilt) / degreesPerUnit;
1205
+ }
1206
+ settiltangle(angle);
1207
+ return normalizeAngle(pen.tilt) / degreesPerUnit;
1208
+ };
1209
+ const cursorSnapshot = () => ({
1210
+ x,
1211
+ y,
1212
+ heading,
1213
+ shapeName: pen.shapeName,
1214
+ fillColor,
1215
+ penColor: strokeColor,
1216
+ stretchLen: pen.stretchLen,
1217
+ stretchWid: pen.stretchWid,
1218
+ outline: pen.outlineWidth,
1219
+ tilt: pen.tilt
1220
+ });
1221
+ const stamp = () => {
1222
+ stampCounter += 1;
1223
+ const segment = { stampId: stampCounter, stamp: cursorSnapshot() };
1224
+ pushUndo(() => {
1225
+ const index = pen.paths.indexOf(segment);
1226
+ if (index >= 0) pen.paths.splice(index, 1);
1227
+ });
1228
+ enqueueOperation(() => {
1229
+ pen.paths.push(segment);
1230
+ draw();
1231
+ });
1232
+ return stampCounter;
1233
+ };
1234
+ const clearstamp = (stampId) => {
1235
+ const wanted = toPlainNumber(stampId, Number.NaN);
1236
+ const index = pen.paths.findIndex((path) => path.stampId === wanted);
1237
+ if (index >= 0) pen.paths.splice(index, 1);
1238
+ draw();
1239
+ };
1240
+ const clearstamps = (n = void 0) => {
1241
+ const count = toPlainNumber(n, Number.NaN);
1242
+ let stamps = pen.paths.filter((path) => path.stampId !== void 0);
1243
+ if (Number.isFinite(count) && count !== 0) {
1244
+ stamps = count > 0 ? stamps.slice(0, count) : stamps.slice(count);
1245
+ }
1246
+ for (const entry of stamps) {
1247
+ const index = pen.paths.indexOf(entry);
1248
+ if (index >= 0) pen.paths.splice(index, 1);
1249
+ }
1250
+ draw();
1251
+ };
1252
+ const undo = () => {
1253
+ const entry = undoStack.pop();
1254
+ if (!entry) return;
1255
+ entry();
1256
+ draw();
1257
+ };
1258
+ const undobufferentries = () => undoStack.length;
959
1259
  const penFn = (options = void 0) => {
960
1260
  if (options === void 0) {
961
1261
  return getPenState();
@@ -966,16 +1266,16 @@
966
1266
  penDown = !!source.pendown;
967
1267
  }
968
1268
  if ("pencolor" in source) {
969
- strokeColor = toColorString(source.pencolor);
1269
+ setStroke(toColorString(source.pencolor));
970
1270
  }
971
1271
  if ("fillcolor" in source) {
972
- fillColor = toColorString(source.fillcolor);
1272
+ setFill(toColorString(source.fillcolor));
973
1273
  }
974
1274
  if ("pensize" in source) {
975
- ensurePath().lineWidth = Math.max(
976
- 1,
977
- Number(source.pensize) || DEFAULT_LINE_WIDTH
978
- );
1275
+ penWidth = Math.max(1, toPlainNumber(source.pensize, penWidth));
1276
+ }
1277
+ if ("speed" in source) {
1278
+ speed(source.speed);
979
1279
  }
980
1280
  if ("shown" in source) {
981
1281
  turtleVisible = !!source.shown;
@@ -989,6 +1289,8 @@
989
1289
  pen.paths = [];
990
1290
  currentPath = null;
991
1291
  fillPath = null;
1292
+ filling = false;
1293
+ undoStack.length = 0;
992
1294
  beginCurrentPath();
993
1295
  draw();
994
1296
  };
@@ -996,22 +1298,30 @@
996
1298
  pen.paths = [];
997
1299
  currentPath = null;
998
1300
  fillPath = null;
1301
+ undoStack.length = 0;
999
1302
  x = 0;
1000
1303
  y = 0;
1001
- heading = 0;
1304
+ heading = screenMode === "logo" ? 90 : 0;
1002
1305
  pen.renderedX = 0;
1003
1306
  pen.renderedY = 0;
1004
- pen.renderedHeading = 0;
1307
+ pen.renderedHeading = heading;
1005
1308
  penDown = true;
1006
1309
  turtleVisible = true;
1007
1310
  pen.renderedTurtleVisible = true;
1008
1311
  strokeColor = "#000000";
1009
1312
  fillColor = "#000000";
1313
+ penWidth = DEFAULT_LINE_WIDTH;
1010
1314
  pen.shapeColor = "#000000";
1315
+ pen.shapeFillColor = "#000000";
1011
1316
  pen.shapeName = DEFAULT_SHAPE;
1317
+ pen.stretchLen = 1;
1318
+ pen.stretchWid = 1;
1319
+ pen.outlineWidth = 1;
1320
+ pen.tilt = 0;
1012
1321
  fontSize = DEFAULT_FONT_SIZE;
1013
1322
  currentFontFamily = "Arial";
1014
1323
  currentFontStyle = "normal";
1324
+ degreesPerUnit = 1;
1015
1325
  filling = false;
1016
1326
  beginCurrentPath();
1017
1327
  };
@@ -1041,6 +1351,9 @@
1041
1351
  ycor,
1042
1352
  heading: heading_,
1043
1353
  towards,
1354
+ distance,
1355
+ degrees: degreesFn,
1356
+ radians,
1044
1357
  pendown: pendownFn,
1045
1358
  pd: pendownFn,
1046
1359
  down: pendownFn,
@@ -1057,6 +1370,7 @@
1057
1370
  fillcolor,
1058
1371
  begin_fill,
1059
1372
  end_fill,
1373
+ filling: isfilling,
1060
1374
  speed,
1061
1375
  showturtle,
1062
1376
  st: showturtle,
@@ -1064,14 +1378,24 @@
1064
1378
  ht: hideturtle,
1065
1379
  isvisible,
1066
1380
  shape,
1381
+ shapesize,
1382
+ turtlesize: shapesize,
1383
+ tilt,
1384
+ tiltangle,
1385
+ settiltangle,
1386
+ stamp,
1387
+ clearstamp,
1388
+ clearstamps,
1389
+ undo,
1390
+ undobufferentries,
1067
1391
  clear,
1068
1392
  reset: resetPen
1069
1393
  };
1070
1394
  return { pen, api: api2, resetPen };
1071
1395
  };
1072
- const colormode = (mode = void 0) => {
1073
- if (mode === void 0) return colorMode;
1074
- const numeric = Number(mode);
1396
+ const colormode = (mode2 = void 0) => {
1397
+ if (mode2 === void 0) return colorMode;
1398
+ const numeric = Number(mode2);
1075
1399
  if (numeric !== 1 && numeric !== 255) {
1076
1400
  throw new Error("colormode must be 1.0 or 255");
1077
1401
  }
@@ -1142,7 +1466,138 @@
1142
1466
  }
1143
1467
  }
1144
1468
  };
1469
+ const mode = (value = void 0) => {
1470
+ if (value === void 0 || value === null) return screenMode;
1471
+ const next = toPlainString(value, "").toLowerCase().trim();
1472
+ if (next !== "standard" && next !== "logo") {
1473
+ throw new Error(`No mode named ${next} (available: standard, logo)`);
1474
+ }
1475
+ screenMode = next;
1476
+ for (const penObj of allPenObjects) penObj.resetPen();
1477
+ draw();
1478
+ return screenMode;
1479
+ };
1480
+ const tracer = (n = void 0, delay2 = void 0) => {
1481
+ if (n === void 0 || n === null) return tracerN;
1482
+ tracerN = Math.max(0, Math.trunc(toPlainNumber(n, 1)));
1483
+ tracerCounter = 0;
1484
+ const nextDelay = toPlainNumber(delay2, Number.NaN);
1485
+ if (Number.isFinite(nextDelay)) delayMs = Math.max(0, nextDelay);
1486
+ if (tracerN !== 0) render();
1487
+ return tracerN;
1488
+ };
1489
+ const update = () => {
1490
+ tracerCounter = 0;
1491
+ render();
1492
+ };
1493
+ const delay = (ms = void 0) => {
1494
+ if (ms === void 0 || ms === null) return delayMs;
1495
+ delayMs = Math.max(0, toPlainNumber(ms, delayMs));
1496
+ return delayMs;
1497
+ };
1498
+ const setundobuffer = (size) => {
1499
+ const next = toPlainNumber(size, DEFAULT_UNDO_BUFFER);
1500
+ undoBufferSize = Number.isFinite(next) ? Math.max(0, next) : 0;
1501
+ };
1502
+ const window_width = () => cssWidth || screenWidth;
1503
+ const window_height = () => cssHeight || screenHeight;
1504
+ const title = (value = void 0) => {
1505
+ if (value === void 0) return screenTitle;
1506
+ screenTitle = toPlainString(value, "");
1507
+ if (canvas) canvas.setAttribute("aria-label", screenTitle);
1508
+ return screenTitle;
1509
+ };
1510
+ const register_shape = (name, shapePoints = void 0) => {
1511
+ const shapeName = toPlainString(name, "").toLowerCase().trim();
1512
+ if (!shapeName) throw new Error("register_shape needs a name");
1513
+ const source = toSequence(shapePoints);
1514
+ if (!source) {
1515
+ throw new Error(`register_shape: no polygon given for ${shapeName}`);
1516
+ }
1517
+ TURTLE_SHAPES[shapeName] = source.map((point) => {
1518
+ const pair = toSequence(point) || [0, 0];
1519
+ const sx = toPlainNumber(pair[0], 0);
1520
+ const sy = toPlainNumber(pair[1], 0);
1521
+ return [sy, -sx];
1522
+ });
1523
+ return shapeName;
1524
+ };
1525
+ const inputLabel = (promptTitle, promptText) => [toPlainString(promptTitle, ""), toPlainString(promptText, "")].filter(Boolean).join(" ").trim();
1526
+ const numinput = (promptTitle = "", promptText = "") => {
1527
+ const answer = askStdinSync(id, inputLabel(promptTitle, promptText));
1528
+ if (!answer) return null;
1529
+ const numeric = Number(answer);
1530
+ return Number.isFinite(numeric) ? numeric : null;
1531
+ };
1532
+ const textinput = (promptTitle = "", promptText = "") => askStdinSync(id, inputLabel(promptTitle, promptText));
1533
+ const addListener = (target, type, handler) => {
1534
+ if (!target) return;
1535
+ target.addEventListener(type, handler);
1536
+ eventCleanups.push(() => target.removeEventListener(type, handler));
1537
+ };
1538
+ const clearListeners = () => {
1539
+ for (const cleanup of eventCleanups) {
1540
+ try {
1541
+ cleanup();
1542
+ } catch {
1543
+ }
1544
+ }
1545
+ eventCleanups = [];
1546
+ for (const timerId of pendingTimers) window.clearTimeout(timerId);
1547
+ pendingTimers = [];
1548
+ };
1549
+ const canvasPoint = (event) => {
1550
+ const rect = canvas?.getBoundingClientRect?.();
1551
+ if (!rect) return [0, 0];
1552
+ return [
1553
+ event.clientX - rect.left - cssWidth / 2,
1554
+ cssHeight / 2 - (event.clientY - rect.top)
1555
+ ];
1556
+ };
1557
+ const onscreenclick = (fn, btn = 1) => {
1558
+ if (!ensureContext()) return;
1559
+ if (!fn) return;
1560
+ const wanted = toPlainNumber(btn, 1) - 1;
1561
+ addListener(canvas, "mousedown", (event) => {
1562
+ if (event.button !== wanted) return;
1563
+ const [px, py] = canvasPoint(event);
1564
+ fn(px, py);
1565
+ });
1566
+ };
1567
+ const onkey = (fn, key = void 0) => {
1568
+ if (!ensureContext()) return;
1569
+ const wanted = toPlainString(key, "");
1570
+ addListener(canvas, "keyup", (event) => {
1571
+ if (wanted && event.key !== wanted && event.code !== wanted) return;
1572
+ if (fn) fn();
1573
+ });
1574
+ };
1575
+ const onkeypress = (fn, key = void 0) => {
1576
+ if (!ensureContext()) return;
1577
+ const wanted = toPlainString(key, "");
1578
+ addListener(canvas, "keydown", (event) => {
1579
+ if (wanted && event.key !== wanted && event.code !== wanted) return;
1580
+ if (fn) fn();
1581
+ });
1582
+ };
1583
+ const listen = () => {
1584
+ if (!ensureContext()) return;
1585
+ canvas.focus?.();
1586
+ };
1587
+ const ontimer = (fn, ms = 0) => {
1588
+ if (!fn) return;
1589
+ const timerId = window.setTimeout(
1590
+ () => {
1591
+ pendingTimers = pendingTimers.filter((entry) => entry !== timerId);
1592
+ fn();
1593
+ },
1594
+ Math.max(0, toPlainNumber(ms, 0))
1595
+ );
1596
+ pendingTimers.push(timerId);
1597
+ };
1598
+ const allPenObjects = [];
1145
1599
  const defaultPenObj = createTurtlePen();
1600
+ allPenObjects.push(defaultPenObj);
1146
1601
  allPens.push(defaultPenObj.pen);
1147
1602
  const bindCanvas = (nextCanvas) => {
1148
1603
  canvas = nextCanvas || getCanvas(id);
@@ -1155,6 +1610,7 @@
1155
1610
  const deactivate = () => {
1156
1611
  active = false;
1157
1612
  clearQueue();
1613
+ clearListeners();
1158
1614
  for (const pen of allPens) {
1159
1615
  pen.paths = [];
1160
1616
  pen.renderedTurtleVisible = false;
@@ -1162,39 +1618,114 @@
1162
1618
  };
1163
1619
  const resetState = () => {
1164
1620
  clearQueue();
1621
+ clearListeners();
1165
1622
  allPens.length = 0;
1166
1623
  allPens.push(defaultPenObj.pen);
1624
+ allPenObjects.length = 0;
1625
+ allPenObjects.push(defaultPenObj);
1167
1626
  defaultPenObj.resetPen();
1168
1627
  backgroundColor = "#ffffff";
1169
1628
  backgroundImage = null;
1170
1629
  colorMode = 1;
1171
1630
  screenWidth = 640;
1172
1631
  screenHeight = 480;
1632
+ screenMode = "standard";
1633
+ screenTitle = "";
1634
+ turtleSpeed = DEFAULT_SPEED;
1635
+ delayMs = DEFAULT_DELAY_MS;
1636
+ tracerN = 1;
1637
+ tracerCounter = 0;
1638
+ stampCounter = 0;
1639
+ undoBufferSize = DEFAULT_UNDO_BUFFER;
1173
1640
  active = true;
1641
+ render();
1642
+ };
1643
+ const clearscreen = () => {
1644
+ for (const penObj of allPenObjects) penObj.resetPen();
1645
+ backgroundColor = "#ffffff";
1646
+ backgroundImage = null;
1174
1647
  draw();
1175
1648
  };
1649
+ const resetscreen = () => {
1650
+ for (const penObj of allPenObjects) penObj.resetPen();
1651
+ draw();
1652
+ };
1653
+ const setup = (width = void 0, height = void 0, startx = void 0, starty = void 0) => {
1654
+ const w = toPlainNumber(width, Number.NaN);
1655
+ const h = toPlainNumber(height, Number.NaN);
1656
+ return screensize(
1657
+ Number.isFinite(w) && w > 1 ? w : void 0,
1658
+ Number.isFinite(h) && h > 1 ? h : void 0
1659
+ );
1660
+ };
1176
1661
  const Turtle = function() {
1177
1662
  const penObj = createTurtlePen();
1663
+ allPenObjects.push(penObj);
1178
1664
  enqueueOperation(() => {
1179
1665
  allPens.push(penObj.pen);
1180
1666
  draw();
1181
1667
  });
1182
1668
  return penObj.api;
1183
1669
  };
1670
+ const screenApi = {
1671
+ colormode,
1672
+ screensize,
1673
+ setup,
1674
+ bgcolor,
1675
+ bgpic,
1676
+ mode,
1677
+ tracer,
1678
+ update,
1679
+ delay,
1680
+ title,
1681
+ register_shape,
1682
+ addshape: register_shape,
1683
+ window_width,
1684
+ window_height,
1685
+ numinput,
1686
+ textinput,
1687
+ listen,
1688
+ onkey,
1689
+ onkeyrelease: onkey,
1690
+ onkeypress,
1691
+ onscreenclick,
1692
+ onclick: onscreenclick,
1693
+ ontimer,
1694
+ clearscreen,
1695
+ clear: clearscreen,
1696
+ resetscreen,
1697
+ reset: resetscreen,
1698
+ // The canvas is always on screen, so there is no event loop to enter or leave.
1699
+ mainloop: () => {
1700
+ },
1701
+ done: () => {
1702
+ },
1703
+ exitonclick: () => {
1704
+ },
1705
+ bye: () => {
1706
+ }
1707
+ };
1184
1708
  const api = {
1185
1709
  __bindCanvas: bindCanvas,
1186
1710
  __deactivate: deactivate,
1187
- __redraw: draw,
1711
+ __redraw: render,
1188
1712
  __resetState: resetState,
1189
1713
  __setPyodide: (runtime) => {
1190
1714
  pyodide = runtime;
1191
1715
  },
1192
1716
  ...defaultPenObj.api,
1193
1717
  Turtle,
1194
- colormode,
1195
- screensize,
1196
- bgcolor,
1197
- bgpic
1718
+ Screen: () => screenApi,
1719
+ getscreen: () => screenApi,
1720
+ setundobuffer,
1721
+ // Screen helpers are also exposed at module level, matching `from turtle import *`.
1722
+ ...screenApi,
1723
+ // The turtle's own clear()/reset() win over the screen's at module level,
1724
+ // exactly as CPython's functional interface binds them.
1725
+ clear: defaultPenObj.api.clear,
1726
+ reset: defaultPenObj.api.reset,
1727
+ clearscreen,
1728
+ resetscreen
1198
1729
  };
1199
1730
  return api;
1200
1731
  };
@@ -1737,10 +2268,125 @@
1737
2268
  };
1738
2269
 
1739
2270
  // assets/directive-pyide/src/execution.js
1740
- var scriptLooksLikeTurtle = (script) => {
1741
- return /\bfrom\s+turtle\s+import\b|\bimport\s+turtle\b/.test(
1742
- String(script || "")
1743
- );
2271
+ var runSyncSupport = /* @__PURE__ */ new Map();
2272
+ var PROBE_RUN_SYNC = `
2273
+ def _hyperbook_probe():
2274
+ try:
2275
+ from pyodide.ffi import run_sync
2276
+ import js
2277
+ except Exception:
2278
+ return False
2279
+ try:
2280
+ run_sync(js.Promise.resolve(True))
2281
+ return True
2282
+ except Exception:
2283
+ return False
2284
+
2285
+ _hyperbook_probe()
2286
+ `;
2287
+ var INPUT_SHIM = `
2288
+ def input(prompt=""):
2289
+ import sys
2290
+ from pyodide.ffi import run_sync
2291
+
2292
+ text = "" if prompt is None else str(prompt)
2293
+ if text:
2294
+ sys.stdout.write(text)
2295
+ sys.stdout.flush()
2296
+ answer = run_sync(__hyperbook_ask_stdin(text))
2297
+ answer = "" if answer is None else str(answer)
2298
+ # A terminal echoes what was typed and the Enter that ended it; nothing
2299
+ # does that here, so the transcript has to include it explicitly.
2300
+ sys.stdout.write(answer + "\\n")
2301
+ sys.stdout.flush()
2302
+ return answer
2303
+ `;
2304
+ var TURTLE_INPUT_SHIM = `
2305
+ def _hyperbook_patch_turtle():
2306
+ import turtle
2307
+ from pyodide.ffi import run_sync
2308
+
2309
+ def _label(title, prompt):
2310
+ return " ".join(str(part) for part in (title, prompt) if part).strip()
2311
+
2312
+ def textinput(title="", prompt=""):
2313
+ return run_sync(__hyperbook_ask_stdin(_label(title, prompt)))
2314
+
2315
+ def numinput(title="", prompt="", default=None, minval=None, maxval=None):
2316
+ hint = ""
2317
+ if minval is not None and maxval is not None:
2318
+ hint = " [{0}, {1}]".format(minval, maxval)
2319
+ elif minval is not None:
2320
+ hint = " [>= {0}]".format(minval)
2321
+ elif maxval is not None:
2322
+ hint = " [<= {0}]".format(maxval)
2323
+ while True:
2324
+ answer = run_sync(__hyperbook_ask_stdin(_label(title, prompt) + hint))
2325
+ # An empty answer stands in for tkinter's Cancel button.
2326
+ if answer is None or str(answer).strip() == "":
2327
+ return default
2328
+ try:
2329
+ value = float(str(answer).strip().replace(",", "."))
2330
+ except ValueError:
2331
+ continue
2332
+ if minval is not None and value < minval:
2333
+ continue
2334
+ if maxval is not None and value > maxval:
2335
+ continue
2336
+ return value
2337
+
2338
+ # Screen() hands back one shared object, so patching it once is enough.
2339
+ try:
2340
+ screen = turtle.Screen()
2341
+ screen.numinput = numinput
2342
+ screen.textinput = textinput
2343
+ except Exception:
2344
+ pass
2345
+
2346
+ try:
2347
+ turtle.numinput = numinput
2348
+ turtle.textinput = textinput
2349
+ except Exception:
2350
+ pass
2351
+ if getattr(turtle, "numinput", None) is not numinput:
2352
+ # The JS module rejected the assignment; shadow it with a real Python
2353
+ # module so \`from turtle import *\` still picks up the patched names.
2354
+ import sys
2355
+ import types
2356
+
2357
+ wrapper = types.ModuleType("turtle")
2358
+ for name in dir(turtle):
2359
+ try:
2360
+ setattr(wrapper, name, getattr(turtle, name))
2361
+ except Exception:
2362
+ pass
2363
+ wrapper.numinput = numinput
2364
+ wrapper.textinput = textinput
2365
+ sys.modules["turtle"] = wrapper
2366
+ sys.modules["jturtle"] = wrapper
2367
+
2368
+ _hyperbook_patch_turtle()
2369
+ del _hyperbook_patch_turtle
2370
+ `;
2371
+ var supportsRunSync = async (id, pyodide) => {
2372
+ if (runSyncSupport.has(id)) return runSyncSupport.get(id);
2373
+ let supported = false;
2374
+ const dict = pyodide.globals.get("dict");
2375
+ const probeGlobals = dict();
2376
+ try {
2377
+ supported = !!await pyodide.runPythonAsync(PROBE_RUN_SYNC, {
2378
+ globals: probeGlobals,
2379
+ locals: probeGlobals,
2380
+ filename: "<hyperbook-probe>"
2381
+ });
2382
+ } catch {
2383
+ supported = false;
2384
+ } finally {
2385
+ probeGlobals.destroy();
2386
+ dict.destroy();
2387
+ }
2388
+ runSyncSupport.set(id, supported);
2389
+ return supported;
1744
2390
  };
1745
2391
  var resetCanvas = (canvas) => {
1746
2392
  if (!canvas) return;
@@ -1912,13 +2558,15 @@
1912
2558
  }
1913
2559
  });
1914
2560
  pyodide.setStdin({
2561
+ // Reached by sys.stdin reads, and by input() when Python cannot be
2562
+ // suspended. Both block the main thread; there is no way around that
2563
+ // without stack switching.
1915
2564
  stdin: () => {
1916
- const promptText = lastStdinPrompt || hyperbook.i18n.get("pyide-input-prompt");
2565
+ const promptText = lastStdinPrompt;
1917
2566
  lastStdinPrompt = "";
1918
- const value = window.prompt(promptText);
1919
- if (value === null) {
1920
- return "";
1921
- }
2567
+ const value = askStdinSync(id, promptText);
2568
+ appendOutputLine(id, `${value}
2569
+ `);
1922
2570
  return value;
1923
2571
  }
1924
2572
  });
@@ -1931,12 +2579,31 @@
1931
2579
  });
1932
2580
  await ensureMicropipPackages(id, pyodide, packages);
1933
2581
  await pyodide.loadPackagesFromImports(executableScript);
2582
+ const canSuspend = await supportsRunSync(id, pyodide);
1934
2583
  const dict = pyodide.globals.get("dict");
1935
2584
  const globals = dict();
1936
2585
  try {
1937
2586
  for (const [key, value] of Object.entries(globalsContext)) {
1938
2587
  globals.set(key, value);
1939
2588
  }
2589
+ if (canSuspend) {
2590
+ globals.set(
2591
+ "__hyperbook_ask_stdin",
2592
+ (promptText) => askStdinAsync(id, promptText)
2593
+ );
2594
+ await pyodide.runPythonAsync(INPUT_SHIM, {
2595
+ globals,
2596
+ locals: globals,
2597
+ filename: "<hyperbook-stdin>"
2598
+ });
2599
+ if (scriptLooksLikeTurtle(executableScript)) {
2600
+ await pyodide.runPythonAsync(TURTLE_INPUT_SHIM, {
2601
+ globals,
2602
+ locals: globals,
2603
+ filename: "<hyperbook-stdin>"
2604
+ });
2605
+ }
2606
+ }
1940
2607
  const results = await pyodide.runPythonAsync(executableScript, {
1941
2608
  globals,
1942
2609
  locals: globals,
@@ -1944,6 +2611,7 @@
1944
2611
  });
1945
2612
  return { results };
1946
2613
  } finally {
2614
+ hideStdinField(id);
1947
2615
  globals.destroy();
1948
2616
  dict.destroy();
1949
2617
  if (canvas) {
@@ -1974,12 +2642,379 @@ if _pg:
1974
2642
  }
1975
2643
  };
1976
2644
 
2645
+ // assets/directive-pyide/src/turtle-completions.js
2646
+ var TURTLE_MOVE = [
2647
+ [
2648
+ "forward",
2649
+ "forward(distance)",
2650
+ "Move the turtle forward by distance, in the direction it is headed."
2651
+ ],
2652
+ ["fd", "fd(distance)", "Short form of forward()."],
2653
+ [
2654
+ "backward",
2655
+ "backward(distance)",
2656
+ "Move the turtle backward by distance, without changing its heading."
2657
+ ],
2658
+ ["bk", "bk(distance)", "Short form of backward()."],
2659
+ ["back", "back(distance)", "Short form of backward()."],
2660
+ [
2661
+ "right",
2662
+ "right(angle)",
2663
+ "Turn the turtle right (clockwise) by angle units."
2664
+ ],
2665
+ ["rt", "rt(angle)", "Short form of right()."],
2666
+ [
2667
+ "left",
2668
+ "left(angle)",
2669
+ "Turn the turtle left (counterclockwise) by angle units."
2670
+ ],
2671
+ ["lt", "lt(angle)", "Short form of left()."],
2672
+ [
2673
+ "goto",
2674
+ "goto(x, y=None)",
2675
+ "Move the turtle to an absolute position. Draws a line if the pen is down."
2676
+ ],
2677
+ ["setpos", "setpos(x, y=None)", "Same as goto()."],
2678
+ ["setposition", "setposition(x, y=None)", "Same as goto()."],
2679
+ [
2680
+ "setx",
2681
+ "setx(x)",
2682
+ "Set the turtle's first coordinate, leaving the second unchanged."
2683
+ ],
2684
+ [
2685
+ "sety",
2686
+ "sety(y)",
2687
+ "Set the turtle's second coordinate, leaving the first unchanged."
2688
+ ],
2689
+ [
2690
+ "setheading",
2691
+ "setheading(angle)",
2692
+ "Set the turtle's orientation to angle. 0 is east in standard mode, north in logo mode."
2693
+ ],
2694
+ ["seth", "seth(angle)", "Short form of setheading()."],
2695
+ [
2696
+ "home",
2697
+ "home()",
2698
+ "Move the turtle to the origin and set its heading back to 0."
2699
+ ],
2700
+ [
2701
+ "circle",
2702
+ "circle(radius, extent=None, steps=None)",
2703
+ "Draw a circle or, with extent, an arc. A positive radius curves to the left."
2704
+ ],
2705
+ [
2706
+ "dot",
2707
+ "dot(size=None, *color)",
2708
+ "Draw a filled dot at the current position."
2709
+ ],
2710
+ ["position", "position()", "Return the turtle's current position as (x, y)."],
2711
+ ["pos", "pos()", "Short form of position()."],
2712
+ ["xcor", "xcor()", "Return the turtle's x coordinate."],
2713
+ ["ycor", "ycor()", "Return the turtle's y coordinate."],
2714
+ ["heading", "heading()", "Return the turtle's current heading."],
2715
+ [
2716
+ "towards",
2717
+ "towards(x, y=None)",
2718
+ "Return the angle from the turtle to the given position."
2719
+ ],
2720
+ [
2721
+ "distance",
2722
+ "distance(x, y=None)",
2723
+ "Return the distance from the turtle to the given position."
2724
+ ],
2725
+ [
2726
+ "degrees",
2727
+ "degrees(fullcircle=360)",
2728
+ "Measure angles in degrees, or in fullcircle units per revolution."
2729
+ ],
2730
+ ["radians", "radians()", "Measure angles in radians."],
2731
+ [
2732
+ "speed",
2733
+ "speed(speed=None)",
2734
+ "Set the animation speed from 1 (slowest) to 10 (fastest), or 0 for no animation. Also accepts 'fastest', 'fast', 'normal', 'slow' and 'slowest'. Without an argument it returns the current speed."
2735
+ ],
2736
+ ["undo", "undo()", "Undo the most recent turtle action."],
2737
+ [
2738
+ "undobufferentries",
2739
+ "undobufferentries()",
2740
+ "Return the number of actions that can still be undone."
2741
+ ]
2742
+ ];
2743
+ var TURTLE_PEN = [
2744
+ [
2745
+ "pendown",
2746
+ "pendown()",
2747
+ "Put the pen down \u2014 the turtle draws when it moves."
2748
+ ],
2749
+ ["pd", "pd()", "Short form of pendown()."],
2750
+ ["down", "down()", "Short form of pendown()."],
2751
+ ["penup", "penup()", "Pull the pen up \u2014 the turtle moves without drawing."],
2752
+ ["pu", "pu()", "Short form of penup()."],
2753
+ ["up", "up()", "Short form of penup()."],
2754
+ [
2755
+ "pensize",
2756
+ "pensize(width=None)",
2757
+ "Set the line thickness. Without an argument it returns the current width."
2758
+ ],
2759
+ ["width", "width(width=None)", "Same as pensize()."],
2760
+ [
2761
+ "pen",
2762
+ "pen(pen=None, **pendict)",
2763
+ "Read or set the pen's state as a dictionary (pendown, pencolor, fillcolor, pensize, speed, shown)."
2764
+ ],
2765
+ [
2766
+ "color",
2767
+ "color(*args)",
2768
+ "Set pen and fill colour at once: color(c), color(pen, fill) or color(r, g, b). Without arguments it returns both."
2769
+ ],
2770
+ [
2771
+ "pencolor",
2772
+ "pencolor(*args)",
2773
+ "Set the line colour, as a name, an (r, g, b) tuple or three numbers. Without arguments it returns the current colour."
2774
+ ],
2775
+ [
2776
+ "fillcolor",
2777
+ "fillcolor(*args)",
2778
+ "Set the fill colour, as a name, an (r, g, b) tuple or three numbers. Without arguments it returns the current colour."
2779
+ ],
2780
+ [
2781
+ "begin_fill",
2782
+ "begin_fill()",
2783
+ "Start recording a shape to fill. Call end_fill() to fill it."
2784
+ ],
2785
+ [
2786
+ "end_fill",
2787
+ "end_fill()",
2788
+ "Fill the shape drawn since the last begin_fill()."
2789
+ ],
2790
+ ["filling", "filling()", "Return True while a fill is being recorded."],
2791
+ [
2792
+ "write",
2793
+ "write(arg, move=False, align='left', font=('Arial', 8, 'normal'))",
2794
+ "Write text at the turtle's position."
2795
+ ],
2796
+ [
2797
+ "setfontsize",
2798
+ "setfontsize(size)",
2799
+ "Set the default font size used by write()."
2800
+ ],
2801
+ [
2802
+ "clear",
2803
+ "clear()",
2804
+ "Erase this turtle's drawings, leaving its position and pen state alone."
2805
+ ],
2806
+ [
2807
+ "reset",
2808
+ "reset()",
2809
+ "Erase this turtle's drawings and restore its starting state."
2810
+ ]
2811
+ ];
2812
+ var TURTLE_CURSOR = [
2813
+ ["showturtle", "showturtle()", "Make the turtle visible."],
2814
+ ["st", "st()", "Short form of showturtle()."],
2815
+ [
2816
+ "hideturtle",
2817
+ "hideturtle()",
2818
+ "Make the turtle invisible. Drawing is noticeably faster while hidden."
2819
+ ],
2820
+ ["ht", "ht()", "Short form of hideturtle()."],
2821
+ ["isvisible", "isvisible()", "Return True if the turtle is visible."],
2822
+ [
2823
+ "shape",
2824
+ "shape(name=None)",
2825
+ "Set the turtle's shape: 'classic', 'arrow', 'turtle', 'circle', 'square', 'triangle' or 'blank'."
2826
+ ],
2827
+ [
2828
+ "shapesize",
2829
+ "shapesize(stretch_wid=None, stretch_len=None, outline=None)",
2830
+ "Scale the turtle cursor and set its outline width."
2831
+ ],
2832
+ [
2833
+ "turtlesize",
2834
+ "turtlesize(stretch_wid=None, stretch_len=None, outline=None)",
2835
+ "Same as shapesize()."
2836
+ ],
2837
+ [
2838
+ "tilt",
2839
+ "tilt(angle)",
2840
+ "Rotate the turtle's shape by angle, without changing its heading."
2841
+ ],
2842
+ [
2843
+ "tiltangle",
2844
+ "tiltangle(angle=None)",
2845
+ "Read or set the shape's rotation relative to the heading."
2846
+ ],
2847
+ [
2848
+ "settiltangle",
2849
+ "settiltangle(angle)",
2850
+ "Set the shape's rotation relative to the heading."
2851
+ ],
2852
+ [
2853
+ "stamp",
2854
+ "stamp()",
2855
+ "Stamp a copy of the turtle's shape onto the canvas and return its id."
2856
+ ],
2857
+ ["clearstamp", "clearstamp(stampid)", "Remove the stamp with the given id."],
2858
+ [
2859
+ "clearstamps",
2860
+ "clearstamps(n=None)",
2861
+ "Remove all stamps, the first n, or the last n if n is negative."
2862
+ ]
2863
+ ];
2864
+ var TURTLE_SCREEN = [
2865
+ [
2866
+ "Screen",
2867
+ "Screen()",
2868
+ "Return the screen object, which carries the window-level functions."
2869
+ ],
2870
+ ["getscreen", "getscreen()", "Return the screen the turtle draws on."],
2871
+ [
2872
+ "Turtle",
2873
+ "Turtle()",
2874
+ "Create an additional turtle drawing on the same canvas."
2875
+ ],
2876
+ ["bgcolor", "bgcolor(color)", "Set the background colour of the canvas."],
2877
+ [
2878
+ "bgpic",
2879
+ "bgpic(picname=None)",
2880
+ "Set a background image from a file in the IDE's file system."
2881
+ ],
2882
+ [
2883
+ "screensize",
2884
+ "screensize(canvwidth=None, canvheight=None, bg=None)",
2885
+ "Set the size of the drawing canvas in pixels."
2886
+ ],
2887
+ [
2888
+ "setup",
2889
+ "setup(width, height, startx=None, starty=None)",
2890
+ "Set the canvas size in pixels."
2891
+ ],
2892
+ [
2893
+ "colormode",
2894
+ "colormode(cmode=None)",
2895
+ "Use colour values from 0 to 1 (cmode=1.0) or 0 to 255 (cmode=255)."
2896
+ ],
2897
+ [
2898
+ "mode",
2899
+ "mode(mode=None)",
2900
+ "Switch between 'standard' (0 = east) and 'logo' (0 = north). Changing the mode resets every turtle."
2901
+ ],
2902
+ [
2903
+ "tracer",
2904
+ "tracer(n=None, delay=None)",
2905
+ "Draw only every n-th update. tracer(0) turns animation off until update() is called \u2014 the usual way to draw complex figures quickly."
2906
+ ],
2907
+ ["update", "update()", "Redraw the canvas. Needed after tracer(0)."],
2908
+ [
2909
+ "delay",
2910
+ "delay(delay=None)",
2911
+ "Read or set the delay between drawing steps, in milliseconds."
2912
+ ],
2913
+ ["title", "title(titlestring)", "Set the canvas title."],
2914
+ ["window_width", "window_width()", "Return the canvas width in pixels."],
2915
+ ["window_height", "window_height()", "Return the canvas height in pixels."],
2916
+ [
2917
+ "register_shape",
2918
+ "register_shape(name, shape)",
2919
+ "Register a polygon as a new turtle shape."
2920
+ ],
2921
+ ["addshape", "addshape(name, shape)", "Same as register_shape()."],
2922
+ ["clearscreen", "clearscreen()", "Erase everything and reset every turtle."],
2923
+ ["resetscreen", "resetscreen()", "Reset every turtle to its starting state."],
2924
+ [
2925
+ "setundobuffer",
2926
+ "setundobuffer(size)",
2927
+ "Set how many actions undo() can step back through."
2928
+ ],
2929
+ [
2930
+ "numinput",
2931
+ "numinput(title, prompt)",
2932
+ "Ask the user for a number and return it, or None if cancelled."
2933
+ ],
2934
+ [
2935
+ "textinput",
2936
+ "textinput(title, prompt)",
2937
+ "Ask the user for a string and return it, or None if cancelled."
2938
+ ],
2939
+ [
2940
+ "listen",
2941
+ "listen()",
2942
+ "Give the canvas keyboard focus, so key handlers fire."
2943
+ ],
2944
+ [
2945
+ "onkey",
2946
+ "onkey(fun, key)",
2947
+ "Call fun when key is released. Requires listen()."
2948
+ ],
2949
+ [
2950
+ "onkeypress",
2951
+ "onkeypress(fun, key=None)",
2952
+ "Call fun when key is pressed. Requires listen()."
2953
+ ],
2954
+ ["onkeyrelease", "onkeyrelease(fun, key)", "Same as onkey()."],
2955
+ [
2956
+ "onclick",
2957
+ "onclick(fun, btn=1)",
2958
+ "Call fun(x, y) when the canvas is clicked."
2959
+ ],
2960
+ ["onscreenclick", "onscreenclick(fun, btn=1)", "Same as onclick()."],
2961
+ ["ontimer", "ontimer(fun, t=0)", "Call fun after t milliseconds."],
2962
+ ["mainloop", "mainloop()", "No-op here \u2014 the canvas is always on screen."],
2963
+ ["done", "done()", "No-op here \u2014 the canvas is always on screen."],
2964
+ [
2965
+ "exitonclick",
2966
+ "exitonclick()",
2967
+ "No-op here \u2014 the canvas is always on screen."
2968
+ ],
2969
+ ["bye", "bye()", "No-op here \u2014 the canvas is always on screen."]
2970
+ ];
2971
+ var toOptions = (entries, boost = 0) => entries.map(([label, detail, info]) => ({
2972
+ label,
2973
+ type: "function",
2974
+ detail,
2975
+ info,
2976
+ boost
2977
+ }));
2978
+ var isSecondary = ([, detail, info]) => /^(Short form|Same as|No-op)/.test(info);
2979
+ var buildOptions = (groups) => {
2980
+ const entries = groups.flat();
2981
+ return [
2982
+ ...toOptions(
2983
+ entries.filter((entry) => !isSecondary(entry)),
2984
+ 1
2985
+ ),
2986
+ ...toOptions(entries.filter(isSecondary), -1)
2987
+ ];
2988
+ };
2989
+ var MODULE_OPTIONS = buildOptions([
2990
+ TURTLE_MOVE,
2991
+ TURTLE_PEN,
2992
+ TURTLE_CURSOR,
2993
+ TURTLE_SCREEN
2994
+ ]);
2995
+ var MEMBER_OPTIONS = MODULE_OPTIONS.filter(
2996
+ (option) => option.label !== "Turtle" && option.label !== "Screen"
2997
+ );
2998
+ var MEMBER_BASE_PATTERN = /(?:^|\n)\s*([A-Za-z_]\w*)\s*=\s*(?:turtle\s*\.\s*)?(?:Turtle|Screen|getscreen)\s*\(/g;
2999
+ var memberBases = (doc) => {
3000
+ const bases = ["turtle"];
3001
+ for (const match of doc.matchAll(MEMBER_BASE_PATTERN)) {
3002
+ if (!bases.includes(match[1])) bases.push(match[1]);
3003
+ }
3004
+ return bases;
3005
+ };
3006
+ var turtleCompletions = {
3007
+ enabled: scriptLooksLikeTurtle,
3008
+ globals: MODULE_OPTIONS,
3009
+ members: MEMBER_OPTIONS,
3010
+ memberBases
3011
+ };
3012
+
1977
3013
  // assets/directive-pyide/src/ui.js
1978
3014
  var updateFullscreenButtonState = (elem, button) => {
1979
3015
  if (!elem || !button) return;
1980
3016
  const isFullscreen = document.fullscreenElement === elem;
1981
3017
  const label = hyperbook.i18n.get("ide-fullscreen-enter");
1982
- button.textContent = "\u26F6";
1983
3018
  button.title = label;
1984
3019
  button.setAttribute("aria-label", label);
1985
3020
  button.classList.toggle("active", isFullscreen);
@@ -2108,6 +3143,7 @@ if _pg:
2108
3143
  if (!state.running && !hasRuntime || state.stopRequested) return;
2109
3144
  state.stopRequested = true;
2110
3145
  state.stopping = true;
3146
+ cancelStdin(id);
2111
3147
  const interruptBuffer = interruptBuffers.get(id);
2112
3148
  if (interruptBuffer) {
2113
3149
  interruptBuffer[0] = 2;
@@ -2296,6 +3332,7 @@ if _pg:
2296
3332
  const cm = editorDiv ? HyperbookCM.create(editorDiv, {
2297
3333
  lang: editorDiv.dataset.lang || "python",
2298
3334
  value: initialSource,
3335
+ completions: [turtleCompletions],
2299
3336
  onChange: (code) => {
2300
3337
  void persistPyideState({ script: code });
2301
3338
  }