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:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user