Skip to content

Commit 8f96adf

Browse files
committed
Move Save into the editor as the program's name
The editor pane was showing the runtime's status while the global toolbar held Save, which is the one control up there with no runtime meaning. Swap them, so each sits with its subject. Save becomes the program's identity rather than a verb: the editor pane names the program it holds, marks unsaved edits with a dot, and opens the save dialog when clicked. Dirty state is derived by comparing against the stored project instead of tracked with a flag, so editing something back to its saved state clears the marker. The runtime status indicator moves beside the REPL header, where the VM's own output already is.
1 parent bddb34c commit 8f96adf

3 files changed

Lines changed: 108 additions & 19 deletions

File tree

simulator/index.html

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,6 @@
8787
<span>Reset VM</span>
8888
</button>
8989

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>
95-
</button>
96-
9790
<!-- Theme Toggle -->
9891
<button type="button" class="sim-btn sim-icon-btn" id="theme-toggle" aria-label="Toggle Color Theme" title="Toggle Light/Dark Theme">
9992
<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>
@@ -143,10 +136,13 @@
143136
<button type="button" class="sim-tab-add" id="add-file-btn" title="New file" aria-label="New file">
144137
<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>
145138
</button>
146-
<div class="sim-status-indicator">
147-
<span class="sim-status-dot is-ready" id="status-dot"></span>
148-
<span id="status-text">Ready</span>
149-
</div>
139+
<!-- The program this pane is holding. Names it, flags unsaved
140+
edits, and opens the save dialog. -->
141+
<button type="button" class="sim-program" id="program-btn" title="Save this program (Ctrl+S)">
142+
<svg viewBox="0 0 24 24" width="13" height="13" 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>
143+
<span id="program-name">Unsaved</span>
144+
<span class="sim-program-dirty" id="program-dirty" role="img" aria-label="Unsaved changes" hidden></span>
145+
</button>
150146
</div>
151147
<div class="sim-editor-container" id="editor-container"></div>
152148
</section>
@@ -181,6 +177,11 @@
181177
<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="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
182178
<span>REPL</span>
183179
</div>
180+
<!-- VM state belongs next to the VM's own output. -->
181+
<div class="sim-status-indicator">
182+
<span class="sim-status-dot is-ready" id="status-dot"></span>
183+
<span id="status-text">Ready</span>
184+
</div>
184185
<button type="button" class="sim-btn sim-icon-btn" id="clear-console-btn" title="Clear runtime output">
185186
<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>
186187
</button>

simulator/simulator.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,57 @@ html, body {
396396
color: var(--text-main);
397397
}
398398

399+
/* The program name, at the right of the tab strip. Distinct from the file tabs
400+
next to it: it names the whole program, not one of its files. */
401+
.sim-program {
402+
display: inline-flex;
403+
align-items: center;
404+
gap: 6px;
405+
flex: none;
406+
max-width: 40%;
407+
padding: 4px 9px;
408+
background: none;
409+
border: 1px solid transparent;
410+
border-radius: 7px;
411+
color: var(--text-muted);
412+
font-family: inherit;
413+
font-size: 0.78rem;
414+
font-weight: 600;
415+
cursor: pointer;
416+
}
417+
418+
.sim-program:hover {
419+
background: var(--bg-control);
420+
border-color: var(--border-color);
421+
color: var(--text-main);
422+
}
423+
424+
.sim-program svg {
425+
flex: none;
426+
opacity: 0.75;
427+
}
428+
429+
.sim-program #program-name {
430+
overflow: hidden;
431+
text-overflow: ellipsis;
432+
white-space: nowrap;
433+
}
434+
435+
/* A drawn dot rather than a "•" glyph: a bullet sized to look right needs a
436+
collapsed line-height, which leaves the element with a zero-height box. */
437+
.sim-program-dirty {
438+
display: inline-block;
439+
flex: none;
440+
width: 7px;
441+
height: 7px;
442+
border-radius: 50%;
443+
background: var(--accent);
444+
}
445+
446+
.sim-program-dirty[hidden] {
447+
display: none;
448+
}
449+
399450
.sim-editor-container {
400451
flex: 1;
401452
width: 100%;
@@ -565,7 +616,11 @@ html, body {
565616
align-items: center;
566617
gap: 6px;
567618
font-size: 0.76rem;
619+
font-weight: 500;
568620
color: var(--text-muted);
621+
/* Sits beside the REPL label rather than being spread across the header by
622+
the pane header's space-between. */
623+
margin: 0 auto 0 14px;
569624
}
570625

571626
.sim-status-dot {

simulator/simulator.js

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/**
22
* simulator.js — Interactive PyDevices Python Simulator Engine
33
*
4-
* Manages Monaco Editor, direct MicroPython runtime execution,
5-
* synthetic board_config binding, LZ-string sharing, and split layout.
4+
* Manages the Monaco editor and its multi-file program buffers, the xterm
5+
* REPL over the MicroPython WebAssembly runtime, locally saved programs,
6+
* and the resizable split layout.
67
*/
78

89
(function () {
@@ -22,7 +23,7 @@
2223
const elRunBtn = document.getElementById("run-btn");
2324
const elResetVmBtn = document.getElementById("reset-vm");
2425
const elStopBtn = document.getElementById("stop-btn");
25-
const elSaveBtn = document.getElementById("save-btn");
26+
const elProgramBtn = document.getElementById("program-btn");
2627
const elClearConsoleBtn = document.getElementById("clear-console-btn");
2728
const elDeviceBezel = document.getElementById("device-bezel");
2829
const elCanvas = document.getElementById("display_canvas");
@@ -122,10 +123,6 @@
122123
const fileModels = new Map();
123124
let activeFile = ENTRY_FILE;
124125

125-
function fileNames() {
126-
return [...fileModels.keys()];
127-
}
128-
129126
function fileSource(name) {
130127
const model = fileModels.get(name);
131128
return model ? model.getValue() : "";
@@ -254,6 +251,39 @@
254251
localStorage.setItem(DRAFT_STORAGE_KEY,
255252
JSON.stringify({ files: allFiles(), active: activeFile }));
256253
} catch (e) {}
254+
refreshProgramLabel();
255+
}
256+
257+
// Key order would otherwise make a rename look like an edit.
258+
function programSnapshot(files, width, height, shape) {
259+
const sorted = {};
260+
for (const name of Object.keys(files).sort()) sorted[name] = files[name];
261+
return JSON.stringify({ files: sorted, width, height, shape });
262+
}
263+
264+
// Compared against the stored project rather than tracked with a flag, so
265+
// editing something back to its saved state clears the marker honestly.
266+
function isProgramDirty() {
267+
const name = currentProjectName();
268+
if (!name) return true;
269+
const project = readProjects()[name];
270+
if (!project) return true;
271+
const { width, height, shape } = currentResolution;
272+
return programSnapshot(allFiles(), width, height, shape) !==
273+
programSnapshot(project.files || {}, project.width, project.height, project.shape);
274+
}
275+
276+
function refreshProgramLabel() {
277+
const label = document.getElementById("program-name");
278+
const dirty = document.getElementById("program-dirty");
279+
const button = document.getElementById("program-btn");
280+
if (!label || !dirty || !button) return;
281+
const name = currentProjectName();
282+
label.textContent = name || "Unsaved";
283+
dirty.hidden = !isProgramDirty();
284+
button.title = name
285+
? `${name} — save this program (Ctrl+S)`
286+
: "Save this program (Ctrl+S)";
257287
}
258288

259289
function readDraft() {
@@ -1113,6 +1143,7 @@ os.chdir("/")
11131143
localStorage.setItem(LAST_PROJECT_KEY, trimmed);
11141144
} catch (e) {}
11151145
renderProjectList();
1146+
refreshProgramLabel();
11161147
showToast(`✓ Saved “${trimmed}”`);
11171148
return true;
11181149
}
@@ -1128,6 +1159,7 @@ os.chdir("/")
11281159
try {
11291160
localStorage.setItem(LAST_PROJECT_KEY, name);
11301161
} catch (e) {}
1162+
refreshProgramLabel();
11311163
document.getElementById("save-dialog")?.close();
11321164
showToast(`Loaded “${name}”`);
11331165
}
@@ -1143,6 +1175,7 @@ os.chdir("/")
11431175
} catch (e) {}
11441176
}
11451177
renderProjectList();
1178+
refreshProgramLabel();
11461179
}
11471180

11481181
// Downloads the file that is currently open, under its own name. One file at a
@@ -1363,7 +1396,7 @@ os.chdir("/")
13631396
event.preventDefault();
13641397
requestInterrupt();
13651398
});
1366-
if (elSaveBtn) elSaveBtn.addEventListener("click", openSaveDialog);
1399+
if (elProgramBtn) elProgramBtn.addEventListener("click", openSaveDialog);
13671400
initSaveDialog();
13681401
initFileNameDialog();
13691402
initMoreMenu();

0 commit comments

Comments
 (0)