Skip to content

Commit bddb34c

Browse files
committed
Rework the simulator: real REPL, multi-file programs, vendored editor
Replace the separate REPL input box with an xterm.js terminal driven straight by replProcessCharWithAsyncify, so MicroPython's own line editor owns the cursor: arrow-key history, Ctrl-A/E editing and tab completion are the real REPL's rather than a JS reimplementation. Keystrokes are serialised through a promise queue because Asyncify allows only one operation in flight, and Ctrl-C bypasses that queue via mp_sched_keyboard_interrupt so a runaway loop can actually be stopped -- with a Stop button next to RUN. Run the editor through the raw REPL instead of echoing an exec(compile(...)) one-liner, filtering the OK/\x04 framing back out, and map <stdin> and ./module.py traceback frames onto Monaco error markers. Programs are now a flat set of .py files sharing a working directory, with main.py as the entry point; a tab strip manages them and every buffer is written to the runtime FS before a run. Saved programs live in localStorage and carry their display size. Sharing and the LZ-String dependency are gone. Vendor a trimmed Monaco 0.52.2 so a cold offline install still gets an editor, and precache it in the service worker. cdnjs is no longer used at all. Also: fix the vertical splitter, which had no matching CSS and so was zero pixels wide; wire up the horizontal one, which had markup and styles but no handler; move both to pointer events for touch; style .sim-brand-title and .sim-brand-badge, which were never defined; push display size changes into the running VM; and replace prompt() for custom resolutions with a dialog.
1 parent 576e7c1 commit bddb34c

11 files changed

Lines changed: 2445 additions & 310 deletions

File tree

simulator/index.html

Lines changed: 130 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
<link rel="preconnect" href="https://fonts.googleapis.com">
1414
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1515
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap">
16+
<link rel="stylesheet" href="/vendor/xterm/xterm.css">
1617
<link rel="stylesheet" href="simulator.css">
17-
<!-- LZ-String for compressed URL hash sharing -->
18-
<script src="https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js"></script>
19-
<!-- Monaco Editor AMD Loader -->
20-
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.js"></script>
18+
<!-- Monaco Editor AMD Loader, vendored so a cold offline install still has
19+
an editor. Google Fonts above is now the only cross-origin load, and it
20+
falls back to the local stacks when it fails. -->
21+
<script src="/vendor/monaco/vs/loader.js"></script>
2122
</head>
2223
<body>
2324
<!-- Top Global App Bar -->
@@ -52,47 +53,76 @@
5253
<!-- Resolution & Shape Selector -->
5354
<div class="sim-select-wrap">
5455
<select id="resolution-select" aria-label="Select Screen Resolution">
55-
<option value="320x240" selected>320 × 240 (QVGA Landscape)</option>
56-
<option value="240x320">240 × 320 (QVGA Portrait)</option>
57-
<option value="240x240-square">240 × 240 (Square Watch)</option>
58-
<option value="240x240-round">240 × 240 (Round Watch)</option>
59-
<option value="480x320">480 × 320 (HVGA Landscape)</option>
60-
<option value="320x480">320 × 480 (HVGA Portrait)</option>
61-
<option value="800x480">800 × 480 (WVGA Landscape)</option>
56+
<option value="320x240" selected>320 × 240</option>
57+
<option value="240x320">240 × 320</option>
58+
<!-- Both 240 × 240 entries would read identically without the
59+
one word that actually distinguishes them. -->
60+
<option value="240x240-square">240 × 240</option>
61+
<option value="240x240-round">240 × 240 round</option>
62+
<option value="480x320">480 × 320</option>
63+
<option value="320x480">320 × 480</option>
64+
<option value="800x480">800 × 480</option>
6265
<option value="custom">Custom...</option>
6366
</select>
6467
</div>
6568

6669
</div>
6770

6871
<div class="sim-header-right">
69-
<button type="button" class="sim-btn" id="enable-audio">Enable Audio</button>
70-
<button type="button" class="sim-btn" id="enable-microphone">Enable Microphone</button>
71-
<button type="button" class="sim-btn" id="refresh-application" title="Clear the simulator cache and reload the latest version">
72-
<span>Refresh app</span>
73-
</button>
7472
<!-- Run Editor Button -->
7573
<button type="button" class="sim-btn sim-btn-primary" id="run-btn" title="Run editor in the REPL globals (Ctrl+Enter)">
7674
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m8 5 11 7-11 7z"/></svg>
7775
<span>RUN</span>
7876
<kbd>Ctrl+↵</kbd>
7977
</button>
8078

79+
<button type="button" class="sim-btn" id="stop-btn" title="Interrupt the running program (Ctrl+C)" disabled>
80+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
81+
<span>Stop</span>
82+
<kbd>Ctrl+C</kbd>
83+
</button>
84+
8185
<button type="button" class="sim-btn" id="reset-vm" title="Reset the MicroPython virtual machine">
8286
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
8387
<span>Reset VM</span>
8488
</button>
8589

86-
<!-- Share URL Button -->
87-
<button type="button" class="sim-btn" id="share-btn" title="Share Interactive Code Link">
88-
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
89-
<span>Share</span>
90+
<!-- Saved Programs -->
91+
<button type="button" class="sim-btn" id="save-btn" title="Save this program in the browser or download it (Ctrl+S)">
92+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
93+
<span>Save</span>
94+
<kbd>Ctrl+S</kbd>
9095
</button>
9196

9297
<!-- Theme Toggle -->
9398
<button type="button" class="sim-btn sim-icon-btn" id="theme-toggle" aria-label="Toggle Color Theme" title="Toggle Light/Dark Theme">
9499
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
95100
</button>
101+
102+
<!-- Overflow menu: everything that is not part of the edit/run loop -->
103+
<div class="sim-menu-wrap">
104+
<button type="button" class="sim-btn sim-icon-btn" id="more-menu-btn"
105+
aria-label="More actions" title="More actions"
106+
aria-haspopup="menu" aria-expanded="false" aria-controls="more-menu">
107+
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><circle cx="12" cy="5" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="12" cy="19" r="1.8"/></svg>
108+
</button>
109+
<div class="sim-menu" id="more-menu" role="menu" hidden>
110+
<button type="button" class="sim-menu-item" role="menuitem" id="enable-audio">
111+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/></svg>
112+
<span>Enable Audio</span>
113+
</button>
114+
<button type="button" class="sim-menu-item" role="menuitem" id="enable-microphone">
115+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="11" rx="3"/><path d="M5 10a7 7 0 0 0 14 0M12 17v5"/></svg>
116+
<span>Enable Microphone</span>
117+
</button>
118+
<hr class="sim-menu-sep">
119+
<button type="button" class="sim-menu-item" role="menuitem" id="refresh-application"
120+
title="Clear the simulator cache and reload the latest version">
121+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>
122+
<span>Refresh app</span>
123+
</button>
124+
</div>
125+
</div>
96126
</div>
97127
</header>
98128

@@ -107,10 +137,12 @@
107137
<!-- Left Pane: Monaco Code Editor -->
108138
<section class="sim-editor-pane" id="editor-pane" aria-label="Code Editor">
109139
<div class="sim-pane-header">
110-
<div class="sim-tab">
111-
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
112-
<span>main.py</span>
113-
</div>
140+
<!-- simulator.js renders one tab per file; main.py is the entry
141+
point and cannot be renamed or removed. -->
142+
<div class="sim-tabs" id="file-tabs" role="tablist" aria-label="Program files"></div>
143+
<button type="button" class="sim-tab-add" id="add-file-btn" title="New file" aria-label="New file">
144+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
145+
</button>
114146
<div class="sim-status-indicator">
115147
<span class="sim-status-dot is-ready" id="status-dot"></span>
116148
<span id="status-text">Ready</span>
@@ -126,6 +158,13 @@
126158
<section class="sim-preview-pane" id="preview-pane" aria-label="Device Preview and Console">
127159
<!-- Top Sub-Pane: Hardware Canvas Bezel Stage -->
128160
<div class="sim-device-stage" id="device-stage">
161+
<!-- Shown when the display size changes after a run: the panel the
162+
program already created keeps the size it booted with. -->
163+
<div class="sim-stage-notice" id="stage-notice" role="status" hidden>
164+
<span>Display size changed — reset the VM to apply it.</span>
165+
<button type="button" class="sim-btn sim-btn-primary" id="stage-notice-reset">Reset VM</button>
166+
<button type="button" class="sim-stage-notice-dismiss" id="stage-notice-dismiss" aria-label="Dismiss">×</button>
167+
</div>
129168
<div class="sim-device-bezel" id="device-bezel">
130169
<canvas id="display_canvas" width="320" height="240" tabindex="0"></canvas>
131170
<canvas id="aux_canvas" width="1" height="1" tabindex="0"></canvas>
@@ -146,15 +185,79 @@
146185
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
147186
</button>
148187
</div>
149-
<div class="sim-console-body sim-terminal-body" id="console-log">
150-
<!-- simulator.js mounts the direct MicroPython REPL here. -->
188+
<div class="sim-terminal-body" id="console-log">
189+
<!-- simulator.js mounts the xterm-backed MicroPython REPL here. -->
151190
</div>
152191
</section>
153192
</section>
154193
</main>
155194

195+
<!-- Custom resolution entry. A <dialog> rather than prompt(), which is
196+
blocked outright in some standalone-PWA contexts. -->
197+
<dialog class="sim-dialog" id="custom-res-dialog" aria-label="Custom display size">
198+
<form id="custom-res-form" method="dialog">
199+
<h2 class="sim-dialog-title">Custom display size</h2>
200+
<div class="sim-dialog-fields">
201+
<label>Width
202+
<input type="number" id="custom-res-w" min="16" max="2048" step="1" required>
203+
</label>
204+
<label>Height
205+
<input type="number" id="custom-res-h" min="16" max="2048" step="1" required>
206+
</label>
207+
</div>
208+
<p class="sim-dialog-hint">Reset the VM after applying so the running program picks up the new size.</p>
209+
<div class="sim-dialog-actions">
210+
<button type="button" class="sim-btn" id="custom-res-cancel">Cancel</button>
211+
<button type="submit" class="sim-btn sim-btn-primary">Apply</button>
212+
</div>
213+
</form>
214+
</dialog>
215+
216+
<!-- New file / rename. Flat .py names only, so `import helper` resolves
217+
against the working directory the runtime already has on sys.path. -->
218+
<dialog class="sim-dialog" id="file-name-dialog" aria-label="File name">
219+
<form id="file-name-form" method="dialog">
220+
<h2 class="sim-dialog-title" id="file-name-title">New file</h2>
221+
<div class="sim-dialog-fields">
222+
<label>File name
223+
<input type="text" id="file-name-input" required autocomplete="off"
224+
spellcheck="false" placeholder="helper.py">
225+
</label>
226+
</div>
227+
<p class="sim-dialog-hint" id="file-name-error" hidden></p>
228+
<div class="sim-dialog-actions">
229+
<button type="button" class="sim-btn" id="file-name-cancel">Cancel</button>
230+
<button type="submit" class="sim-btn sim-btn-primary" id="file-name-ok">Create</button>
231+
</div>
232+
</form>
233+
</dialog>
234+
235+
<!-- Saved programs. Everything lives in this browser; the download link is
236+
the way a program leaves it. There is no upload counterpart -- paste
237+
code straight into the editor instead. -->
238+
<dialog class="sim-dialog sim-dialog-wide" id="save-dialog" aria-label="Saved programs">
239+
<h2 class="sim-dialog-title">Saved programs</h2>
240+
<form id="save-form" method="dialog">
241+
<div class="sim-save-row">
242+
<label class="sim-save-name">Name
243+
<input type="text" id="save-name" maxlength="60" required
244+
autocomplete="off" spellcheck="false" placeholder="my-program">
245+
</label>
246+
<button type="submit" class="sim-btn sim-btn-primary">Save</button>
247+
</div>
248+
</form>
249+
250+
<ul class="sim-save-list" id="save-list"></ul>
251+
<p class="sim-save-empty" id="save-empty">Nothing saved yet — saved programs are kept in this browser.</p>
252+
253+
<div class="sim-dialog-actions">
254+
<a class="sim-btn" id="download-py" download="main.py" href="#">Download .py</a>
255+
<button type="button" class="sim-btn" id="save-close">Close</button>
256+
</div>
257+
</dialog>
258+
156259
<!-- Toast Notification -->
157-
<div class="sim-toast" id="sim-toast">Link copied to clipboard!</div>
260+
<div class="sim-toast" id="sim-toast">Saved</div>
158261

159262
<!-- Simulator Application Scripts -->
160263
<script src="./templates.js"></script>

0 commit comments

Comments
 (0)