Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Online playground for peepoScript, an operator-first emote-keyworded language." />
<title>peepoScript playground</title>
<link rel="icon" href="peepo.png" />
<link rel="stylesheet" href="style.css" />
Expand All @@ -21,6 +22,7 @@ <h1>peepoScript <span class="tag">playground</span></h1>
<label class="examples">
examples
<select id="examples">
<option value="" disabled selected>load an example…</option>
<option value="countdown">countdown</option>
<option value="greet">greet</option>
<option value="operators">operators</option>
Expand All @@ -29,17 +31,21 @@ <h1>peepoScript <span class="tag">playground</span></h1>
</div>
<div class="editor-wrap">
<div id="gutter" aria-hidden="true"></div>
<div id="editor" contenteditable="true" spellcheck="false"
aria-label="peepoScript source"></div>
<div id="editor" contenteditable="true" spellcheck="false" role="textbox"
aria-multiline="true" aria-label="peepoScript source"></div>
</div>
<div class="editor-actions">
<button id="run-btn" disabled>run (ctrl+enter)</button>
<button id="clear-btn">clear output</button>
<button id="run-btn" disabled title="Run the editor contents (Ctrl/Cmd+Enter)">run (ctrl+enter)</button>
<button id="share-btn" title="Copy a link to this snippet">share link</button>
<button id="clear-btn" title="Clear the output pane">clear output</button>
</div>
</section>

<section class="pane output-pane">
<div class="pane-title"><span>output</span></div>
<div class="pane-title">
<span>output</span>
<button id="copy-btn" disabled title="Copy output to clipboard">copy</button>
</div>
<div id="output" aria-live="polite"></div>
</section>
</main>
Expand Down
97 changes: 89 additions & 8 deletions web/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const gutter = document.getElementById("gutter");
const output = document.getElementById("output");
const runBtn = document.getElementById("run-btn");
const clearBtn = document.getElementById("clear-btn");
const copyBtn = document.getElementById("copy-btn");
const shareBtn = document.getElementById("share-btn");
const statusEl = document.getElementById("emote-status");

const acEl = document.createElement("div");
Expand All @@ -36,6 +38,7 @@ function spawnWorker() {
worker.terminate();
worker = spawnWorker();
runBtn.disabled = false;
runBtn.textContent = "run (ctrl+enter)";
appendOutput("interpreter restarted; saved bindings are gone.", "err");
};
w.postMessage({ type: "keywords" });
Expand All @@ -54,11 +57,15 @@ function onWorkerMessage(e) {
clearTimeout(pendingRun.timer);
pendingRun = null;
runBtn.disabled = false;
runBtn.textContent = "run (ctrl+enter)";
renderResult(msg.result);
}
break;
case "boot-error":
appendOutput(`failed to load peepo.wasm: ${msg.error}`, "err");
statusEl.textContent = "interpreter failed to load";
statusEl.classList.add("error");
updateClearBtn();
break;
}
}
Expand Down Expand Up @@ -106,6 +113,7 @@ async function loadEmotes(keywords) {

normalize(); // re-render whatever is already in the editor
runBtn.disabled = false;
runBtn.textContent = "run (ctrl+enter)";
}

function emoteUrl(emote) {
Expand Down Expand Up @@ -472,6 +480,10 @@ editor.addEventListener("paste", (e) => {

editor.addEventListener("blur", () => hideAutocompleteSoon());

document.addEventListener("mousedown", (e) => {
if (!acEl.contains(e.target) && e.target !== editor) hideAutocomplete();
});

// The editor is the only scroller; the gutter mirrors it vertically and
// never moves horizontally (wide lines scroll under fixed numbers, like
// a real editor).
Expand Down Expand Up @@ -661,17 +673,23 @@ editor.addEventListener("keydown", (e) => {
if (e.key === "Tab") {
// No popup yet: complete directly when there is exactly one
// candidate, otherwise open the popup for the word at the caret.
e.preventDefault();
// If there is no word at the caret, fall through to normal tab
// insertion so indentation still works.
const hit = currentWordBeforeCaret();
if (!hit) return;
if (!hit) {
e.preventDefault();
document.execCommand("insertText", false, " ");
return;
}
e.preventDefault();
const cands = completionCandidates(hit.word).filter((n) => n !== hit.word);
if (cands.length === 1) acceptAutocomplete(cands[0]);
else updateAutocomplete();
}
});

document.addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.ctrlKey) {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
runCode();
}
Expand All @@ -682,10 +700,16 @@ document.addEventListener("keydown", (e) => {
function runCode() {
if (runBtn.disabled || pendingRun) return;
const code = editorSource(editor);
if (!code.trim()) return;
if (!code.trim()) {
appendOutput("nothing to run", "meta");
updateClearBtn();
return;
}

output.textContent = "";
updateClearBtn();
runBtn.disabled = true;
runBtn.textContent = "running…";
runId += 1;
const id = runId;
const timer = setTimeout(() => {
Expand All @@ -694,6 +718,7 @@ function runCode() {
worker.terminate();
worker = spawnWorker();
runBtn.disabled = false;
runBtn.textContent = "run (ctrl+enter)";
appendOutput(
"run exceeded 5s and was killed (infinite loop?). " +
"the worker restarted, so saved bindings are gone.",
Expand All @@ -710,6 +735,7 @@ function renderResult(result) {
if (result.output) appendOutput(result.output.replace(/\n$/, ""));
if (result.value) appendOutput(result.value);
for (const err of result.errors ?? []) appendOutput(err, "err");
updateClearBtn();
}

function appendOutput(text, cls) {
Expand All @@ -718,10 +744,26 @@ function appendOutput(text, cls) {
div.textContent = text;
output.append(div);
output.scrollTop = output.scrollHeight;
updateClearBtn();
}

function updateClearBtn() {
clearBtn.disabled = !output.textContent.trim();
if (copyBtn) copyBtn.disabled = !output.textContent.trim();
}
runBtn.addEventListener("click", runCode);
clearBtn.addEventListener("click", () => (output.textContent = ""));
clearBtn.addEventListener("click", () => { output.textContent = ""; updateClearBtn(); });
if (copyBtn) copyBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(output.textContent);
const prev = copyBtn.textContent;
copyBtn.textContent = "copied";
setTimeout(() => (copyBtn.textContent = prev), 1200);
} catch {
copyBtn.textContent = "failed";
setTimeout(() => (copyBtn.textContent = "copy"), 1200);
}
});

// ------------------------------------------------------------ examples

Expand Down Expand Up @@ -760,9 +802,48 @@ const examplesSelect = document.getElementById("examples");
function loadExample(key) {
setEditorSource(EXAMPLES[key] ?? EXAMPLES.countdown);
output.textContent = "";
updateClearBtn();
}

examplesSelect?.addEventListener("change", (e) => loadExample(e.target.value));
updateClearBtn();

// default
loadExample("countdown");
examplesSelect?.addEventListener("change", (e) => {
loadExample(e.target.value);
// Reset so the same example can be reloaded later.
if (examplesSelect) examplesSelect.value = "";
});

function encodeSource(src) { return btoa(unescape(encodeURIComponent(src))); }
function decodeSource(s) { return decodeURIComponent(escape(atob(s))); }

// share link handling
if (shareBtn) shareBtn.addEventListener("click", async () => {
const code = editorSource(editor);
const url = `${location.origin}${location.pathname}#code=${encodeSource(code)}`;
history.replaceState(null, "", `#code=${encodeSource(code)}`);
try {
await navigator.clipboard.writeText(url);
const prev = shareBtn.textContent;
shareBtn.textContent = "link copied";
setTimeout(() => (shareBtn.textContent = prev), 1400);
} catch {
const prev = shareBtn.textContent;
shareBtn.textContent = "copied url";
setTimeout(() => (shareBtn.textContent = prev), 1400);
}
});

// default / deep-link load
const hash = location.hash || "";
const match = hash.match(/^#code=(.+)$/);
if (match) {
try {
setEditorSource(decodeSource(match[1]));
output.textContent = "";
updateClearBtn();
} catch {
loadExample("countdown");
}
} else {
loadExample("countdown");
}
52 changes: 45 additions & 7 deletions web/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ main {

@media (max-width: 900px) {
main { grid-template-columns: 1fr; grid-template-rows: none; }
.pane { min-height: 240px; max-height: 60vh; }
#editor, #output { max-height: 45vh; }
}

@media (min-width: 901px) {
Expand All @@ -80,9 +82,10 @@ main {
flex-direction: column;
background: var(--panel);
border: 1px solid var(--panel-edge);
border-radius: 8px;
border-radius: 10px;
overflow: hidden;
min-height: 320px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
}

.pane-title {
Expand All @@ -96,6 +99,13 @@ main {
color: var(--muted);
border-bottom: 1px solid var(--panel-edge);
}
.pane-title button {
text-transform: none;
letter-spacing: 0;
padding: 0.25rem 0.6rem;
font-size: 0.72rem;
font-weight: 500;
}
.pane-title .examples {
display: flex;
align-items: center;
Expand Down Expand Up @@ -168,14 +178,15 @@ main {
#autocomplete {
display: none;
position: fixed;
z-index: 10;
min-width: 220px;
max-height: 260px;
z-index: 20;
min-width: 240px;
max-height: 300px;
overflow-y: auto;
background: #1f1f23;
background: rgba(31, 31, 35, 0.92);
backdrop-filter: blur(6px);
border: 1px solid var(--panel-edge);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
border-radius: 10px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
}

.ac-item {
Expand Down Expand Up @@ -219,11 +230,31 @@ button {
border-radius: 6px;
padding: 0.5rem 1rem;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease, transform 0.05s ease;
}
button:hover:not(:disabled) { background: var(--accent); }
button:active:not(:disabled) { transform: translateY(1px); }
button:disabled { opacity: 0.45; cursor: not-allowed; }

/* Secondary button for non-destructive utilities. */
#clear-btn {
background: transparent;
border: 1px solid var(--panel-edge);
color: var(--muted);
}
#clear-btn:hover:not(:disabled) { background: var(--panel-edge); color: var(--text); }

/* Keyboard focus must be visible; default outlines were removed
for the editor, so restore them for interactive controls. */
button:focus-visible,
.examples select:focus-visible,
#editor:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}

#output {
flex: 1;
padding: 0.9rem;
Expand All @@ -237,6 +268,13 @@ button:disabled { opacity: 0.45; cursor: not-allowed; }
#output .err { color: var(--error); }
#output .meta { color: var(--muted); font-style: italic; }

/* Friendly empty state so the right pane never looks broken. */
#output:empty::before {
content: "run your code to see output here";
color: var(--muted);
font-style: italic;
}

footer {
padding: 0.5rem 1.25rem 0.9rem;
color: var(--muted);
Expand Down
Loading