Expand browser editor runtime and LED simulation workflows.

Add Docker deployment support, richer Selenium/LED pattern tests, in-browser diagnostics, responsive UI improvements, and 16x16 panel simulation tooling to speed iteration and hardware-style prototyping.

Made-with: Cursor
This commit is contained in:
2026-05-01 20:24:05 +12:00
parent f204109a84
commit e4c811f51d
30 changed files with 1478 additions and 60 deletions

1
src/static/.reload-token Normal file
View File

@@ -0,0 +1 @@
1777623664358

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Python Editor</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="/static/styles.css?v=6">
<link rel="stylesheet" href="/static/styles.css?v=10">
</head>
<body>
<div class="container">
@@ -29,6 +29,7 @@
<div class="file-info">
<span id="save-status" class="save-status"></span>
<span class="runtime-hint" title="Python runs locally in your browser via Pyodide; completions use Jedi in the same runtime.">Browser · Pyodide</span>
<span id="lsp-status" class="runtime-hint" title="Jedi in-browser diagnostics">LSP: n/a</span>
</div>
<div class="mode-toggle">
<a id="home-btn" class="mode-btn active" href="/">Home</a>
@@ -36,9 +37,14 @@
<div class="editor-actions">
<button id="run-btn" disabled>Run Python</button>
<button id="stop-btn" disabled>Stop</button>
<select id="run-file-select" title="Script to run">
<option value="">Run active file</option>
</select>
<label for="run-main-checkbox" class="run-main-toggle">
<input type="checkbox" id="run-main-checkbox" />
Run `main.py`
</label>
<label for="panel-16x16-checkbox" class="run-main-toggle">
<input type="checkbox" id="panel-16x16-checkbox" />
16x16 panel
</label>
</div>
</div>
@@ -49,6 +55,19 @@
<div id="completion-dropdown" class="completion-dropdown"></div>
</div>
<section id="led-sim-panel" class="led-sim-panel hidden" aria-label="NeoPixel Simulator">
<div class="led-sim-header">
<h3>NeoPixel Simulator</h3>
<div class="led-sim-actions">
<button id="led-run-btn" type="button">Run</button>
<button id="led-stop-btn" type="button">Stop</button>
<button id="led-close-btn" type="button" aria-label="Close simulator">Close</button>
</div>
</div>
<div id="led-meta" class="led-meta">Waiting for neopixel.write()...</div>
<div id="led-grid" class="led-grid"></div>
</section>
<div class="console-container">
<div class="console-header">Console Output</div>
<pre id="console-output" class="console-output"></pre>
@@ -67,6 +86,6 @@
</div>
</div>
<script type="module" src="/static/script.js?v=10"></script>
<script type="module" src="/static/script.js?v=22"></script>
</body>
</html>

View File

@@ -77,6 +77,38 @@ json.dumps(out)
return;
}
if (type === 'diagnostics') {
const rel = String(payload.path || 'scratch.py').replace(/^\/+/, '');
const vpath = `/workspace/${rel}`;
p.globals.set('__diag_code', String(payload.content ?? ''));
p.globals.set('__diag_path', vpath);
p.globals.set('__diag_extra_json', JSON.stringify(payload.extra_files || {}));
const raw = p.runPython(`
import json, os
import jedi
extra = json.loads(__diag_extra_json)
os.makedirs("/workspace", exist_ok=True)
for rel_path, body in extra.items():
rel_path = str(rel_path).lstrip("/")
full = os.path.join("/workspace", rel_path)
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, "w", encoding="utf-8") as fh:
fh.write(str(body))
os.makedirs(os.path.dirname(__diag_path), exist_ok=True)
with open(__diag_path, "w", encoding="utf-8") as fh:
fh.write(__diag_code)
proj = jedi.Project("/workspace")
s = jedi.Script(code=__diag_code, path=__diag_path, project=proj)
errs = s.get_syntax_errors()
out = [{"line": e.line, "column": e.column, "message": str(e.get_message())} for e in errs]
json.dumps(out)
`);
const diagnostics = JSON.parse(String(raw));
self.postMessage({ id, type: 'diagnostics', ok: true, diagnostics });
return;
}
if (type === 'run') {
const files = payload.files && typeof payload.files === 'object' ? payload.files : {};
const mainRel = String(payload.mainPath || '').replace(/^\/+/, '');
@@ -97,7 +129,7 @@ for rel, body in files.items():
with open(full, "w", encoding="utf-8") as fh:
fh.write(str(body))
for entry in ("/workspace/lib", "/workspace"):
for entry in ("/workspace/code", "/workspace/lib", "/workspace"):
if entry not in sys.path:
sys.path.insert(0, entry)

View File

@@ -1,4 +1,6 @@
import { EditorView, basicSetup } from "/static/codemirror.bundle.mjs";
import { EditorView, basicSetup } from "https://esm.sh/codemirror";
import { Compartment } from "https://esm.sh/@codemirror/state";
import { python } from "https://esm.sh/@codemirror/lang-python";
class TextEditor {
constructor() {
@@ -7,6 +9,7 @@ class TextEditor {
this.pyWorkerMsgId = 0;
this.pyWorkerHandlers = new Map();
this.pyodideInited = false;
this.workerWarmupPromise = null;
this.pyRunGeneration = 0;
this.editor = null;
this.currentFilePath = null;
@@ -26,11 +29,19 @@ class TextEditor {
this.completionIndex = 0;
this.completionOpen = false;
this.completionRequestId = 0;
this.diagnosticsRequestId = 0;
this.diagnosticsTimer = null;
this.draggedItemPath = null;
this.draggedItemIsDirectory = false;
this.dragHoverExpandTimer = null;
this.dragHoverTargetPath = null;
this.savedSession = null;
this.languageCompartment = new Compartment();
this.autoCompleteTimer = null;
this.ledSimWindow = null;
this.ledPanelDismissed = false;
this.lastLedFrame = null;
this.ledPanelWindow = null;
this.init();
}
@@ -47,10 +58,44 @@ class TextEditor {
this.loadSessionState();
this.setupEditor();
this.setupEventListeners();
this.setupDevAutoReload();
this.updateRunButtonState();
this.setLspStatus('LSP: n/a', 'Open a Python file for diagnostics');
this.prewarmPyWorker();
this.loadInitialDirectoryState().then(() => this.restoreSessionTabs());
}
setupDevAutoReload() {
const isLocalhost =
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
if (!isLocalhost) return;
let lastToken = null;
const poll = async () => {
try {
const resp = await fetch(`/static/.reload-token?ts=${Date.now()}`, { cache: 'no-store' });
if (!resp.ok) {
return;
}
const token = (await resp.text()).trim();
if (lastToken === null) {
lastToken = token;
return;
}
if (token && token !== lastToken) {
window.location.reload();
return;
}
} catch (_err) {
// Dev hook file may not exist yet.
}
};
setInterval(poll, 1000);
poll();
}
apiFetch(url, init = {}) {
const next = { ...init };
const headers = new Headers(init.headers || {});
@@ -77,7 +122,7 @@ class TextEditor {
ensurePyWorker() {
if (!this.pyWorker) {
const worker = new Worker('/static/pyodide-worker.js');
const worker = new Worker('/static/pyodide-worker.js?v=3');
this.pyWorker = worker;
worker.onmessage = (event) => this.handlePyWorkerMessage(event);
}
@@ -126,6 +171,19 @@ class TextEditor {
this.pyodideInited = true;
}
prewarmPyWorker() {
if (this.pyodideInited || this.workerWarmupPromise) {
return;
}
this.workerWarmupPromise = this.ensurePyodideReady()
.catch(() => {
// Ignore warm-up failures; next foreground run will retry.
})
.finally(() => {
this.workerWarmupPromise = null;
});
}
loadSessionState() {
try {
const raw = localStorage.getItem(this.sessionStorageKey);
@@ -139,11 +197,13 @@ class TextEditor {
saveSessionState() {
try {
const runFileSelect = document.getElementById('run-file-select');
const runMainCheckbox = document.getElementById('run-main-checkbox');
const panelModeCheckbox = document.getElementById('panel-16x16-checkbox');
const session = {
openTabPaths: this.openTabs.map((tab) => tab.path),
activeTabPath: this.activeTabPath,
selectedRunFile: runFileSelect ? runFileSelect.value : '',
runMainChecked: Boolean(runMainCheckbox && runMainCheckbox.checked),
panel16x16Checked: Boolean(panelModeCheckbox && panelModeCheckbox.checked),
expandedDirs: Array.from(this.expandedDirs || []),
selectedPath: this.selectedPath || '',
selectedIsDirectory: Boolean(this.selectedIsDirectory)
@@ -170,9 +230,13 @@ class TextEditor {
if (session.activeTabPath && this.findTab(session.activeTabPath)) {
this.switchToTab(session.activeTabPath);
}
const runFileSelect = document.getElementById('run-file-select');
if (runFileSelect && typeof session.selectedRunFile === 'string') {
runFileSelect.value = session.selectedRunFile;
const runMainCheckbox = document.getElementById('run-main-checkbox');
if (runMainCheckbox && typeof session.runMainChecked === 'boolean') {
runMainCheckbox.checked = session.runMainChecked;
}
const panelModeCheckbox = document.getElementById('panel-16x16-checkbox');
if (panelModeCheckbox && typeof session.panel16x16Checked === 'boolean') {
panelModeCheckbox.checked = session.panel16x16Checked;
}
this.saveSessionState();
}
@@ -256,7 +320,7 @@ class TextEditor {
setupEditor() {
this.editor = new EditorView({
doc: '',
extensions: [basicSetup],
extensions: [basicSetup, this.languageCompartment.of([])],
parent: document.getElementById('editor')
});
@@ -323,10 +387,88 @@ class TextEditor {
this.hideCompletionDropdown();
this.updateActiveTabContent();
this.markAsModified();
this.scheduleAutoCompletion();
this.scheduleDiagnostics();
}
})(this.editor.dispatch);
}
setLspStatus(text, title = '') {
const status = document.getElementById('lsp-status');
if (!status) return;
status.textContent = text;
status.title = title || text;
}
scheduleDiagnostics() {
if (this.diagnosticsTimer) {
clearTimeout(this.diagnosticsTimer);
}
this.diagnosticsTimer = setTimeout(() => {
this.runDiagnostics();
}, 220);
}
async runDiagnostics() {
if (!this.currentFilePath || !this.currentFilePath.toLowerCase().endsWith('.py')) {
this.setLspStatus('LSP: n/a', 'Open a Python file for diagnostics');
return;
}
const requestId = ++this.diagnosticsRequestId;
this.setLspStatus('LSP: checking...', 'Running Jedi syntax diagnostics');
try {
await this.ensurePyodideReady();
const extraFiles = {};
for (const tab of this.openTabs) {
if (tab.path && tab.path.toLowerCase().endsWith('.py')) {
extraFiles[tab.path] = tab.content;
}
}
const data = await this.callPyWorker('diagnostics', {
path: this.currentFilePath,
content: this.editor.state.doc.toString(),
extra_files: extraFiles
});
if (requestId !== this.diagnosticsRequestId) return;
const diagnostics = Array.isArray(data.diagnostics) ? data.diagnostics : [];
if (diagnostics.length === 0) {
this.setLspStatus('LSP: OK', 'No syntax errors');
} else {
const first = diagnostics[0];
const msg = `${first.message || 'Syntax error'} (line ${first.line ?? '?'})`;
this.setLspStatus(`LSP: ${diagnostics.length} issue(s)`, msg);
}
} catch (_error) {
this.setLspStatus('LSP: unavailable', 'Diagnostics failed');
}
}
scheduleAutoCompletion() {
if (!this.currentFilePath || !this.currentFilePath.toLowerCase().endsWith('.py')) {
return;
}
const cursor = this.editor.state.selection.main.head;
const doc = this.editor.state.doc.toString();
const prev = cursor > 0 ? doc[cursor - 1] : '';
if (!(prev === '.' || /[A-Za-z0-9_]/.test(prev))) {
return;
}
if (this.autoCompleteTimer) {
clearTimeout(this.autoCompleteTimer);
}
this.autoCompleteTimer = setTimeout(() => {
this.showCompletionDropdown();
}, 140);
}
setLanguageForPath(path) {
const ext = (path || '').toLowerCase();
const language = ext.endsWith('.py') ? python() : [];
this.editor.dispatch({
effects: this.languageCompartment.reconfigure(language),
});
}
getCursorLineAndColumn() {
const cursor = this.editor.state.selection.main.head;
const lineInfo = this.editor.state.doc.lineAt(cursor);
@@ -566,6 +708,27 @@ class TextEditor {
this.stopPython();
});
const ledRunBtn = document.getElementById('led-run-btn');
if (ledRunBtn) {
ledRunBtn.addEventListener('click', () => {
this.runPython();
});
}
const ledStopBtn = document.getElementById('led-stop-btn');
if (ledStopBtn) {
ledStopBtn.addEventListener('click', () => {
this.stopPython();
});
}
const ledCloseBtn = document.getElementById('led-close-btn');
if (ledCloseBtn) {
ledCloseBtn.addEventListener('click', () => {
this.ledPanelDismissed = true;
const panel = document.getElementById('led-sim-panel');
if (panel) panel.classList.add('hidden');
});
}
document.getElementById('create-file-btn').addEventListener('click', () => {
this.createNewFile();
});
@@ -590,15 +753,26 @@ class TextEditor {
window.location.href = '/';
});
document.getElementById('run-file-select').addEventListener('change', (event) => {
const selectedPath = event.target.value;
if (!selectedPath) return;
const tab = this.findTab(selectedPath);
if (tab) {
this.switchToTab(selectedPath);
}
this.saveSessionState();
});
const runMainCheckbox = document.getElementById('run-main-checkbox');
if (runMainCheckbox) {
runMainCheckbox.addEventListener('change', () => {
this.saveSessionState();
this.updateRunButtonState();
});
}
const panelModeCheckbox = document.getElementById('panel-16x16-checkbox');
if (panelModeCheckbox) {
panelModeCheckbox.addEventListener('change', () => {
this.saveSessionState();
if (!panelModeCheckbox.checked && this.ledPanelWindow && !this.ledPanelWindow.closed) {
this.ledPanelWindow.close();
this.ledPanelWindow = null;
}
if (this.lastLedFrame) {
this.renderLedSimulation(this.lastLedFrame);
}
});
}
}
@@ -644,29 +818,9 @@ class TextEditor {
this.closeTab(closeButton.dataset.path);
});
});
this.renderRunFileSelect();
this.saveSessionState();
}
renderRunFileSelect() {
const select = document.getElementById('run-file-select');
if (!select) return;
const pythonTabs = this.openTabs
.map((tab) => tab.path)
.filter((path) => typeof path === 'string' && path.toLowerCase().endsWith('.py'));
const currentValue = select.value;
select.innerHTML = '<option value="">Run active file</option>';
pythonTabs.forEach((path) => {
const option = document.createElement('option');
option.value = path;
option.textContent = path.startsWith('code/') ? path.slice('code/'.length) : path;
if (path === currentValue || path === this.currentFilePath) {
option.selected = true;
}
select.appendChild(option);
});
}
findTab(path) {
return this.openTabs.find((tab) => tab.path === path);
}
@@ -705,10 +859,12 @@ class TextEditor {
}
});
this.ignoreNextChange = false;
this.setLanguageForPath(path);
const currentFileEl = document.getElementById('current-file');
if (currentFileEl) currentFileEl.textContent = path;
this.setEditorReadOnly(this.isReadOnlyPath(path));
this.updateRunButtonState();
this.scheduleDiagnostics();
if (tab.isModified) {
this.markAsModified();
} else {
@@ -737,6 +893,7 @@ class TextEditor {
this.activeTabPath = null;
this.currentFilePath = null;
this.setEditorReadOnly(false);
this.setLanguageForPath('');
this.ignoreNextChange = true;
this.editor.dispatch({
changes: {
@@ -749,6 +906,7 @@ class TextEditor {
const currentFileEl = document.getElementById('current-file');
if (currentFileEl) currentFileEl.textContent = 'No file selected';
this.updateRunButtonState();
this.setLspStatus('LSP: n/a', 'Open a Python file for diagnostics');
this.markAsSaved();
}
}
@@ -1021,12 +1179,14 @@ class TextEditor {
}
});
this.ignoreNextChange = false;
this.setLanguageForPath(filePath);
this.markAsSaved();
this.setEditorReadOnly(this.isReadOnlyPath(filePath));
const currentFileEl = document.getElementById('current-file');
if (currentFileEl) currentFileEl.textContent = filePath;
this.updateRunButtonState();
this.scheduleDiagnostics();
this.renderTabs();
this.saveSessionState();
} catch (error) {
@@ -1107,6 +1267,7 @@ class TextEditor {
const currentFileEl = document.getElementById('current-file');
if (currentFileEl) currentFileEl.textContent = 'No file selected';
this.updateRunButtonState();
this.setLspStatus('LSP: n/a', 'Open a Python file for diagnostics');
}
this.directoryCache.clear();
@@ -1129,6 +1290,7 @@ class TextEditor {
} else {
this.activeTabPath = null;
this.currentFilePath = null;
this.setLanguageForPath('');
this.ignoreNextChange = true;
this.editor.dispatch({
changes: {
@@ -1150,9 +1312,13 @@ class TextEditor {
updateRunButtonState() {
const runButton = document.getElementById('run-btn');
const stopButton = document.getElementById('stop-btn');
const runMainCheckbox = document.getElementById('run-main-checkbox');
const runMainSelected = Boolean(runMainCheckbox && runMainCheckbox.checked);
const hasPythonFile = Boolean(this.currentFilePath && this.currentFilePath.toLowerCase().endsWith('.py'));
runButton.disabled = !hasPythonFile || this.isPythonRunning;
const canRun = runMainSelected || hasPythonFile;
runButton.disabled = !canRun;
stopButton.disabled = !this.isPythonRunning;
this.updateLedWindowControls();
}
clearConsole() {
@@ -1165,9 +1331,147 @@ class TextEditor {
}
}
maybePrepareLedWindow(files) {
const importsNeoPixel = Object.values(files || {}).some((content) =>
typeof content === 'string' &&
/\bimport\s+neopixel\b|\bfrom\s+neopixel\s+import\b/.test(content)
);
if (!importsNeoPixel) return;
this.ledPanelDismissed = false;
this.ensureLedWindow();
}
ensureLedWindow() {
const panel = document.getElementById('led-sim-panel');
if (!panel) return null;
if (!this.ledPanelDismissed) {
panel.classList.remove('hidden');
}
this.updateLedWindowControls();
return panel;
}
ensureLedPanelWindow() {
if (this.ledPanelWindow && !this.ledPanelWindow.closed) {
return this.ledPanelWindow;
}
const win = window.open('', 'neopixel-16x16', 'width=760,height=760');
if (!win) return null;
win.document.title = 'NeoPixel 16x16 Panel';
win.document.body.innerHTML = `
<style>
body { margin: 0; background: #111827; color: #e5e7eb; font-family: system-ui, sans-serif; padding: 12px; }
.meta { margin-bottom: 10px; color: #9ca3af; font-size: 13px; }
.grid { display: grid; grid-template-columns: repeat(16, 24px); grid-auto-rows: 24px; gap: 6px; }
.led { width: 24px; height: 24px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.2); box-shadow: inset 0 0 8px rgba(0,0,0,0.45); }
</style>
<div id="meta" class="meta">Waiting for frame...</div>
<div id="grid" class="grid"></div>
`;
this.ledPanelWindow = win;
return win;
}
closeLedPanelWindow() {
if (this.ledPanelWindow && !this.ledPanelWindow.closed) {
this.ledPanelWindow.close();
}
this.ledPanelWindow = null;
}
updateLedWindowControls() {
const runBtn = document.getElementById('led-run-btn');
const stopBtn = document.getElementById('led-stop-btn');
if (!runBtn || !stopBtn) return;
const runMainCheckbox = document.getElementById('run-main-checkbox');
const runMainSelected = Boolean(runMainCheckbox && runMainCheckbox.checked);
const hasPythonFile = Boolean(this.currentFilePath && this.currentFilePath.toLowerCase().endsWith('.py'));
runBtn.disabled = !(runMainSelected || hasPythonFile);
stopBtn.disabled = !this.isPythonRunning;
}
renderLedSimulation(frame) {
this.lastLedFrame = frame;
this.ledPanelDismissed = false;
if (!frame) return;
const panelModeCheckbox = document.getElementById('panel-16x16-checkbox');
const panelMode = Boolean(panelModeCheckbox && panelModeCheckbox.checked);
const pixels = Array.isArray(frame.pixels) ? frame.pixels : [];
if (panelMode) {
const panelWindow = this.ensureLedPanelWindow();
if (!panelWindow || panelWindow.closed) return;
const meta = panelWindow.document.getElementById('meta');
const grid = panelWindow.document.getElementById('grid');
if (!meta || !grid) return;
meta.textContent = `pin=${frame.pin ?? '?'} | leds=${pixels.length} | bpp=${frame.bpp ?? 3} | mode=16x16`;
grid.innerHTML = '';
const panelSize = 16 * 16;
for (let panelIndex = 0; panelIndex < panelSize; panelIndex += 1) {
const row = Math.floor(panelIndex / 16);
const col = panelIndex % 16;
const ledIndex = row % 2 === 0 ? row * 16 + (15 - col) : row * 16 + col;
const px = pixels[ledIndex] || [0, 0, 0];
const r = Number(px?.[0] ?? 0);
const g = Number(px?.[1] ?? 0);
const b = Number(px?.[2] ?? 0);
const div = panelWindow.document.createElement('div');
div.className = 'led';
div.title = `panel(${row},${col}) -> #${ledIndex} (${r}, ${g}, ${b})`;
div.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
div.style.boxShadow = `0 0 10px rgba(${r}, ${g}, ${b}, 0.55), inset 0 0 8px rgba(0,0,0,0.45)`;
grid.appendChild(div);
}
} else {
const panel = this.ensureLedWindow();
if (!panel) return;
const meta = document.getElementById('led-meta');
const grid = document.getElementById('led-grid');
if (!meta || !grid) return;
meta.textContent = `pin=${frame.pin ?? '?'} | leds=${pixels.length} | bpp=${frame.bpp ?? 3}`;
grid.innerHTML = '';
grid.classList.remove('panel-mode');
pixels.forEach((px, i) => {
const r = Number(px?.[0] ?? 0);
const g = Number(px?.[1] ?? 0);
const b = Number(px?.[2] ?? 0);
const div = document.createElement('div');
div.className = 'led';
div.title = `#${i} (${r}, ${g}, ${b})`;
div.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
div.style.boxShadow = `0 0 10px rgba(${r}, ${g}, ${b}, 0.55), inset 0 0 8px rgba(0,0,0,0.45)`;
grid.appendChild(div);
});
}
}
processSimulationLines(lines) {
for (const line of lines || []) {
if (typeof line !== 'string') continue;
const marker = '[neopixel-json]';
const idx = line.indexOf(marker);
if (idx === -1) continue;
const jsonPart = line.slice(idx + marker.length).trim();
if (!jsonPart) continue;
try {
const payload = JSON.parse(jsonPart);
if (payload && payload.type === 'neopixel') {
this.renderLedSimulation(payload);
}
} catch (_err) {
// Ignore malformed simulation payloads.
}
}
}
appendConsoleOutput(lines) {
if (!Array.isArray(lines) || lines.length === 0) return;
this.consolePendingText += lines.join('');
this.processSimulationLines(lines);
const visibleLines = lines.filter((line) => {
if (typeof line !== 'string') return false;
return !line.includes('[neopixel-json]');
});
if (visibleLines.length === 0) return;
this.consolePendingText += visibleLines.join('');
if (this.consoleFlushTimer) return;
this.consoleFlushTimer = setTimeout(() => {
const consoleOutput = document.getElementById('console-output');
@@ -1185,8 +1489,8 @@ class TextEditor {
}
async runPython() {
const runFileSelect = document.getElementById('run-file-select');
const selectedRunFile = runFileSelect && runFileSelect.value ? runFileSelect.value : this.currentFilePath;
const runMainCheckbox = document.getElementById('run-main-checkbox');
const selectedRunFile = runMainCheckbox && runMainCheckbox.checked ? 'code/main.py' : this.currentFilePath;
if (selectedRunFile && selectedRunFile !== this.currentFilePath && this.findTab(selectedRunFile)) {
this.switchToTab(selectedRunFile);
}
@@ -1214,6 +1518,7 @@ class TextEditor {
files[tab.path] = tab.content;
}
}
this.maybePrepareLedWindow(files);
this.clearConsole();
const args = [];
this.appendConsoleOutput([`$ pyodide ${selectedRunFile}\n`]);
@@ -1228,6 +1533,7 @@ class TextEditor {
});
if (generation === this.pyRunGeneration) {
this.appendConsoleOutput(['\n[Finished]\n']);
this.closeLedPanelWindow();
}
} catch (error) {
this.appendConsoleOutput([`\n${error.message}\n`]);
@@ -1244,8 +1550,10 @@ class TextEditor {
this.pyRunGeneration += 1;
this.disposePyWorker();
this.isPythonRunning = false;
this.closeLedPanelWindow();
this.appendConsoleOutput(['\n[Execution stopped — Pyodide worker was reset]\n']);
this.updateRunButtonState();
this.prewarmPyWorker();
}
async deleteSelected() {

View File

@@ -7,13 +7,14 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
height: 100vh;
height: 100dvh;
overflow: hidden;
}
.container {
display: flex;
height: 100vh;
height: 100dvh;
overflow: hidden;
}
/* Sidebar */
@@ -23,6 +24,7 @@ body {
color: white;
display: flex;
flex-direction: column;
min-height: 0;
}
.sidebar-header {
@@ -117,6 +119,9 @@ body {
display: flex;
flex-direction: column;
background-color: white;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.editor-header {
@@ -191,15 +196,6 @@ body {
gap: 0.5rem;
}
#run-file-select {
min-width: 220px;
padding: 0.5rem 0.65rem;
border: 1px solid #cbd5e0;
border-radius: 6px;
font-size: 0.85rem;
background: white;
}
.editor-actions button {
padding: 0.5rem 1rem;
border: 1px solid #e2e8f0;
@@ -225,10 +221,28 @@ body {
background-color: #edf2f7;
}
.run-main-toggle {
display: inline-flex;
align-items: center;
gap: 0.45rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 0.4rem 0.6rem;
font-size: 0.85rem;
color: #374151;
background: white;
white-space: nowrap;
}
.run-main-toggle input[type="checkbox"] {
margin: 0;
}
.editor-container {
flex: 1;
position: relative;
min-height: 0;
overflow: hidden;
}
.hidden {
@@ -350,6 +364,89 @@ body {
background: #0f172a;
}
.led-sim-panel {
border-top: 1px solid #e2e8f0;
background: #111827;
color: #e5e7eb;
padding: 0.75rem;
}
.led-sim-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
}
.led-sim-header h3 {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
}
.led-sim-actions {
display: flex;
align-items: center;
gap: 0.4rem;
}
.led-sim-actions button {
padding: 0.3rem 0.65rem;
border: 1px solid #374151;
background: #1f2937;
color: #e5e7eb;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
}
.led-sim-actions button:hover:not(:disabled) {
background: #243244;
}
.led-sim-actions button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.led-meta {
margin-top: 0.5rem;
color: #9ca3af;
font-size: 0.82rem;
}
.led-grid {
margin-top: 0.6rem;
display: flex;
flex-wrap: wrap;
overflow-x: hidden;
overflow-y: auto;
gap: 8px;
width: 100%;
max-height: 132px;
padding: 0.1rem 0 0.2rem;
}
.led-grid.panel-mode {
display: grid;
grid-template-columns: repeat(16, minmax(0, 24px));
grid-auto-rows: 24px;
justify-content: start;
align-content: start;
gap: 6px;
max-height: 470px;
overflow: auto;
}
.led {
width: 24px;
height: 24px;
min-width: 24px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.45);
}
.console-header {
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
@@ -445,14 +542,106 @@ body {
/* Responsive design */
@media (max-width: 768px) {
body {
height: 100dvh;
overflow: hidden;
}
.container {
flex-direction: column;
height: 100dvh;
overflow: hidden;
}
.sidebar {
width: 250px;
width: 100%;
max-height: 30vh;
min-height: 170px;
}
.sidebar-header {
padding: 0.7rem 0.8rem;
}
.file-tree {
min-height: 100px;
}
.main-content {
min-height: 0;
}
.editor-header {
padding: 0.65rem;
flex-wrap: wrap;
gap: 0.55rem;
align-items: stretch;
}
.file-info {
width: 100%;
justify-content: space-between;
}
.mode-toggle {
order: 3;
}
.editor-actions {
width: 100%;
order: 4;
flex-wrap: wrap;
}
.editor-actions button {
flex: 1 1 100px;
}
.run-main-toggle {
width: 100%;
justify-content: flex-start;
flex: 1 1 100%;
}
.tabs {
padding: 0.3rem 0.35rem;
}
.tab-title {
max-width: 150px;
}
.editor-container {
min-height: 46vh;
}
.cm-editor {
font-size: 13px;
}
.modal-content {
width: 90%;
margin: 20% auto;
}
.led-grid {
max-height: 120px;
}
.led-sim-header {
flex-wrap: wrap;
align-items: center;
}
.led-sim-actions {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
}
.console-container {
height: 180px;
}
}
/* Scrollbar styling */