Skip to content

Commit 8600972

Browse files
committed
feat(simulator): transform console output into interactive Python REPL with code.InteractiveConsole, multiline support, and history
1 parent 20618cc commit 8600972

3 files changed

Lines changed: 163 additions & 4 deletions

File tree

simulator/index.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,22 @@
123123
<!-- Horizontal Drag Splitter -->
124124
<div class="sim-splitter-h" id="splitter-h" role="separator" aria-orientation="horizontal"></div>
125125

126-
<!-- Bottom Sub-Pane: Console / REPL Stream -->
127-
<section class="sim-console-pane" id="console-pane" aria-label="Console Output">
126+
<!-- Bottom Sub-Pane: Interactive REPL -->
127+
<section class="sim-console-pane" id="console-pane" aria-label="Interactive Python REPL">
128128
<div class="sim-pane-header">
129129
<div class="sim-tab">
130130
<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>
131-
<span>Console Output</span>
131+
<span>REPL</span>
132132
</div>
133-
<button type="button" class="sim-btn sim-icon-btn" id="clear-console-btn" title="Clear Console">
133+
<button type="button" class="sim-btn sim-icon-btn" id="clear-console-btn" title="Clear REPL Log">
134134
<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>
135135
</button>
136136
</div>
137137
<div class="sim-console-body" id="console-log"></div>
138+
<form class="sim-repl-form" id="repl-form" autocomplete="off">
139+
<span class="sim-repl-prompt" id="repl-prompt">&gt;&gt;&gt;</span>
140+
<input type="text" class="sim-repl-input" id="repl-input" placeholder="Type Python expression or command (e.g. dir(), lv.screen_active()...)" spellcheck="false" autocomplete="off" autocapitalize="off">
141+
</form>
138142
</section>
139143
</section>
140144
</main>

simulator/simulator.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,55 @@ html, body {
400400
.sim-console-body .log-warn { color: #fbbf24; }
401401
.sim-console-body .log-err { color: #f87171; }
402402
.sim-console-body .log-dim { color: #64748b; }
403+
.sim-console-body .log-prompt { color: var(--accent); font-weight: 600; }
404+
.sim-console-body .log-repl-out { color: #a5b4fc; }
405+
406+
/* Interactive REPL Input Form */
407+
.sim-repl-form {
408+
display: flex;
409+
align-items: center;
410+
gap: 8px;
411+
padding: 6px 12px;
412+
background: var(--bg-panel-subtle);
413+
border-top: 1px solid var(--border-color);
414+
font-family: var(--font-mono);
415+
font-size: 0.84rem;
416+
flex: none;
417+
}
418+
419+
[data-theme="light"] .sim-console-body {
420+
background: #f8fafc;
421+
color: #1e293b;
422+
}
423+
424+
[data-theme="light"] .sim-console-body .log-repl-out {
425+
color: #4338ca;
426+
}
427+
428+
.sim-repl-prompt {
429+
color: var(--accent);
430+
font-weight: 700;
431+
font-family: var(--font-mono);
432+
user-select: none;
433+
font-size: 0.85rem;
434+
}
435+
436+
.sim-repl-input {
437+
flex: 1;
438+
background: transparent;
439+
border: none;
440+
color: var(--text-main);
441+
font-family: var(--font-mono);
442+
font-size: 0.84rem;
443+
outline: none;
444+
padding: 4px 0;
445+
}
446+
447+
.sim-repl-input::placeholder {
448+
color: var(--text-subtle);
449+
font-style: italic;
450+
font-size: 0.78rem;
451+
}
403452

404453
/* Status indicator pill in status bar */
405454
.sim-status-indicator {

simulator/simulator.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@
3131
const elDeviceBezel = document.getElementById("device-bezel");
3232
const elCanvas = document.getElementById("display_canvas");
3333
const elToast = document.getElementById("sim-toast");
34+
const elReplForm = document.getElementById("repl-form");
35+
const elReplInput = document.getElementById("repl-input");
36+
const elReplPrompt = document.getElementById("repl-prompt");
37+
38+
const replHistory = [];
39+
let replHistoryIdx = -1;
3440

3541
// =========================================================================
3642
// Status & Console Helpers
@@ -54,6 +60,8 @@
5460
else if (type === "warn") span.className = "log-warn";
5561
else if (type === "error") span.className = "log-err";
5662
else if (type === "dim") span.className = "log-dim";
63+
else if (type === "prompt") span.className = "log-prompt";
64+
else if (type === "repl-out") span.className = "log-repl-out";
5765

5866
span.textContent = text;
5967
elConsole.appendChild(span);
@@ -519,6 +527,97 @@ if "display_driver" in sys.modules:
519527
applyTheme(next);
520528
}
521529

530+
// =========================================================================
531+
// Interactive REPL Logic (code.InteractiveConsole)
532+
// =========================================================================
533+
534+
let isMoreLines = false;
535+
536+
async function handleReplSubmit(e) {
537+
if (e) e.preventDefault();
538+
if (!elReplInput) return;
539+
540+
const line = elReplInput.value;
541+
const trimmed = line.trim();
542+
543+
if (trimmed) {
544+
replHistory.push(line);
545+
replHistoryIdx = replHistory.length;
546+
}
547+
elReplInput.value = "";
548+
549+
const promptStr = isMoreLines ? "... " : ">>> ";
550+
logConsole(`${promptStr}${line}\n`, "prompt");
551+
552+
try {
553+
const pyodide = await getPyodide();
554+
555+
const pyWrapper = `
556+
import sys, code
557+
558+
_line_input = ${JSON.stringify(line)}
559+
_main_mod = sys.modules.get("__main__")
560+
_main_dict = _main_mod.__dict__ if _main_mod else globals()
561+
562+
if "_pydevices_console" not in globals() or _pydevices_console is None or _pydevices_console.locals is not _main_dict:
563+
_pydevices_console = code.InteractiveConsole(locals=_main_dict)
564+
565+
_more = _pydevices_console.push(_line_input)
566+
567+
# Refresh display if LVGL is active
568+
try:
569+
if "lvgl" in sys.modules:
570+
_lv = sys.modules["lvgl"]
571+
if _lv.is_initialized():
572+
_lv.task_handler()
573+
if "display_driver" in sys.modules:
574+
_dd = sys.modules["display_driver"]
575+
for _drv in getattr(_dd, "_drivers", []):
576+
if hasattr(_drv, "display_drv") and hasattr(_drv.display_drv, "show"):
577+
_drv.display_drv.show()
578+
except Exception:
579+
pass
580+
581+
_more
582+
`;
583+
const more = await pyodide.runPythonAsync(pyWrapper);
584+
isMoreLines = Boolean(more);
585+
586+
if (elReplPrompt) {
587+
elReplPrompt.textContent = isMoreLines ? "..." : ">>>";
588+
}
589+
if (elReplInput) {
590+
elReplInput.placeholder = isMoreLines
591+
? "Continue block (press Enter on empty line to execute)..."
592+
: "Type Python expression or command (e.g. dir(), lv.screen_active()...)";
593+
}
594+
} catch (err) {
595+
logConsole(`${err}\n`, "error");
596+
isMoreLines = false;
597+
if (elReplPrompt) elReplPrompt.textContent = ">>>";
598+
}
599+
}
600+
601+
function handleReplKeydown(e) {
602+
if (e.key === "ArrowUp") {
603+
if (replHistory.length === 0) return;
604+
e.preventDefault();
605+
if (replHistoryIdx > 0) replHistoryIdx--;
606+
else replHistoryIdx = 0;
607+
elReplInput.value = replHistory[replHistoryIdx] || "";
608+
} else if (e.key === "ArrowDown") {
609+
if (replHistory.length === 0) return;
610+
e.preventDefault();
611+
if (replHistoryIdx < replHistory.length - 1) {
612+
replHistoryIdx++;
613+
elReplInput.value = replHistory[replHistoryIdx] || "";
614+
} else {
615+
replHistoryIdx = replHistory.length;
616+
elReplInput.value = "";
617+
}
618+
}
619+
}
620+
522621
// =========================================================================
523622
// DOM Event Bindings
524623
// =========================================================================
@@ -531,6 +630,13 @@ if "display_driver" in sys.modules:
531630
if (elShareBtn) elShareBtn.addEventListener("click", shareCode);
532631
if (elClearConsoleBtn) elClearConsoleBtn.addEventListener("click", clearConsole);
533632

633+
if (elReplForm) {
634+
elReplForm.addEventListener("submit", handleReplSubmit);
635+
}
636+
if (elReplInput) {
637+
elReplInput.addEventListener("keydown", handleReplKeydown);
638+
}
639+
534640
if (elTemplateSelect) {
535641
elTemplateSelect.addEventListener("change", (e) => loadTemplate(e.target.value));
536642
}

0 commit comments

Comments
 (0)