Add interactive WASM playground for uutils coreutils

A browser-based terminal that runs real Rust coreutils compiled to
WebAssembly (wasm32-wasip1) using @bjorn3/browser_wasi_shim.

Features:
- xterm.js terminal with command history and line editing
- Pipe support (cmd1 | cmd2 | cmd3), file redirects (>)
- Persistent virtual filesystem across commands
- cd/pwd builtins with virtual working directory
- Locale support (locale builtin + selector)
- UTF-8/multibyte correct output (raw ConsoleStdout, fixed args_sizes_get)
- Click-to-run example buttons
- Inline wasm_example shortcode for blog posts
This commit is contained in:
Sylvestre Ledru
2026-04-03 20:59:37 +02:00
parent a52c902b3f
commit 5c22a5b3d6
7 changed files with 1073 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
+++
title = "Playground"
template = "page.html"
+++
<p>Try <a href="https://github.com/uutils/coreutils">uutils coreutils</a> directly in your browser! This interactive terminal runs Rust coreutils via WebAssembly — no installation needed.</p>
<!-- Language selector hidden until l10n is embedded in the WASM binary
<div class="playground-toolbar">
<label for="locale-select">Language:</label>
<select id="locale-select" onchange="setLocale(this.value)">
<option value="en-US">English</option>
<option value="fr-FR">French</option>
</select>
</div>
-->
<div id="wasm-playground"></div>
<script src="/js/wasm-terminal.js" defer></script>
<script defer>
document.addEventListener("DOMContentLoaded", function() {
initPlayground("wasm-playground");
});
</script>
<noscript>
<p>JavaScript is required to run the interactive playground.</p>
</noscript>
## Try it out
Click an example to run it in the terminal:
<div class="playground-examples">
<button class="playground-example">seq 1 20 | factor</button>
<button class="playground-example">sort fruits.txt | uniq -c | sort -rn</button>
<button class="playground-example">cut -d, -f2 csv.txt | tail -4 | sort -n</button>
<button class="playground-example">echo 'rust coreutils in the browser' | tr a-z A-Z</button>
<button class="playground-example">echo 'こんにちは世界' | base64 | base64 -d</button>
<button class="playground-example">echo 'Hello, world!' | sha256sum</button>
<button class="playground-example">shuf -n3 names.txt | nl</button>
<button class="playground-example">seq 100 | shuf -n5 | sort -n</button>
<button class="playground-example">paste -d: names.txt numbers.txt | head -5</button>
<button class="playground-example">echo 'مرحبا بالعالم' | wc -c -w</button>
<button class="playground-example">seq 1 15 | fmt -w 40</button>
<button class="playground-example">echo '🍎,🍌,🍒,🥝' | cut -d🍌 -f2</button>
<button class="playground-example">printf '🍒 cherry\n🍎 apple\n🍌 banana\n' | sort -k2</button>
<button class="playground-example">date</button>
</div>
<script>
document.querySelectorAll('.playground-example').forEach(function(btn) {
btn.addEventListener('click', function() {
var cmd = btn.textContent;
document.getElementById('wasm-playground').scrollIntoView({ behavior: 'smooth' });
if (window.runInTerminal) {
window.runInTerminal(cmd);
}
});
});
</script>
## Available commands
The following commands run as **real Rust coreutils compiled to WebAssembly**:
`arch` `b2sum` `base32` `base64` `basenc` `basename` `cat` `cksum`
`comm` `cp` `csplit` `cut` `date` `dir` `dircolors` `dirname`
`echo` `expand` `expr` `factor` `false` `fmt` `fold` `head`
`join` `link` `ln` `ls` `md5sum` `mkdir` `mv` `nl` `nproc` `numfmt`
`od` `paste` `pathchk` `printenv` `printf` `pr` `ptx` `pwd`
`readlink` `realpath` `rm` `rmdir`
`seq` `sort` `split` `tail` `touch` `tr` `tsort`
`sha1sum` `sha224sum` `sha256sum` `sha384sum` `sha512sum`
`shred` `shuf` `sleep` `sum` `tee` `true` `truncate`
`uname` `unexpand` `uniq` `unlink` `vdir` `wc`
The following are **shell builtins** implemented in JavaScript:
- `help` — list available commands and examples
- `clear` — clear the terminal screen
Some commands are not yet available in the WASM build because they
depend on platform-specific syscalls not fully supported by
WebAssembly/WASI.<br />
`chcon` and `runcon` are excluded as they require
SELinux, which is not relevant in a browser environment.<br />
We are actively working on adding more commands as we improve WASI
compatibility in uutils coreutils.
+50
View File
@@ -0,0 +1,50 @@
/**
* Inline "Run" button handler for uutils code examples.
* Lightweight alternative to the full terminal playground.
*/
(function () {
let runtimeLoaded = false;
async function ensureRuntime() {
if (runtimeLoaded) return;
// Load the terminal runtime which provides uutilsExecute
const script = document.createElement("script");
script.src = "/js/wasm-terminal.js";
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
runtimeLoaded = true;
}
async function runExample(button) {
const container = button.closest(".wasm-example");
if (!container) return;
const command = container.dataset.command;
const outputEl = container.querySelector(".wasm-example-output");
const preEl = outputEl.querySelector("pre");
// Show loading state
button.disabled = true;
button.textContent = "Running...";
outputEl.style.display = "block";
preEl.textContent = "Loading...";
try {
await ensureRuntime();
const output = await window.uutilsExecute(command);
preEl.textContent = output || "(no output)";
} catch (e) {
preEl.textContent = "Error: " + e.message;
}
button.disabled = false;
button.textContent = "Run";
}
// Expose globally
window.runWasmExample = runExample;
})();
File diff suppressed because it is too large Load Diff
+127
View File
@@ -519,3 +519,130 @@ main img {
background-color: #c04828; background-color: #c04828;
border-color: #c04828; border-color: #c04828;
} }
/* WASM PLAYGROUND */
.playground-toolbar {
display: flex;
align-items: center;
gap: 0.5em;
margin: 1em 0 0;
font-size: 0.9em;
}
.playground-toolbar select {
padding: 0.3em 0.5em;
border-radius: 0.3em;
border: 1px solid var(--header-border-color);
background: var(--highlight-bg-color);
color: var(--highlight-fg-color);
font-size: 1em;
cursor: pointer;
}
#wasm-playground {
width: 100%;
min-height: 480px;
border-radius: 0.5em;
overflow: hidden;
margin: 1.5em 0;
border: 1px solid var(--header-border-color);
}
#wasm-playground .wasm-loading {
display: flex;
align-items: center;
justify-content: center;
height: 480px;
background: #1e1e2e;
color: #cdd6f4;
font-family: monospace;
font-size: 1.1em;
}
/* INLINE WASM EXAMPLES */
.wasm-example {
margin: 1em 0;
}
.wasm-example-code {
display: flex;
align-items: stretch;
gap: 0;
}
.wasm-example-code pre {
flex: 1;
padding: 0.8em 1em;
background: var(--highlight-bg-color);
color: var(--highlight-fg-color);
border-radius: 0.3em 0 0 0.3em;
font-family: "Fira Code", monospace;
font-size: 0.9em;
overflow-x: auto;
margin: 0;
}
.wasm-run-btn {
padding: 0.8em 1.5em;
background: var(--accent-color);
color: white;
border: none;
border-radius: 0 0.3em 0.3em 0;
cursor: pointer;
font-size: 0.9em;
font-weight: 600;
transition: background-color 0.2s;
white-space: nowrap;
}
.wasm-run-btn:hover {
background: #d45530;
}
.wasm-run-btn:disabled {
opacity: 0.6;
cursor: wait;
}
.wasm-example-output {
margin-top: 0;
}
.wasm-example-output pre {
padding: 0.8em 1em;
background: #1e1e2e;
color: #cdd6f4;
border-radius: 0 0 0.3em 0.3em;
font-family: "Fira Code", monospace;
font-size: 0.9em;
overflow-x: auto;
max-height: 300px;
overflow-y: auto;
margin: 0;
}
/* PLAYGROUND CLICKABLE EXAMPLES */
.playground-examples {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin: 1em 0 2em;
}
.playground-example {
padding: 0.4em 0.8em;
background: var(--highlight-bg-color);
color: var(--highlight-fg-color);
border: 1px solid var(--header-border-color);
border-radius: 0.3em;
font-family: "Fira Code", monospace;
font-size: 0.85em;
cursor: pointer;
transition: border-color 0.2s, background-color 0.2s;
}
.playground-example:hover {
border-color: var(--accent-color);
background: var(--accent-color);
color: white;
}
+1
View File
@@ -43,6 +43,7 @@
<a href="/bsdutils">Bsdutils</a> <a href="/bsdutils">Bsdutils</a>
</div> </div>
</div> </div>
<a href="/playground">Playground</a>
</div> </div>
<div class="spacer"></div> <div class="spacer"></div>
<div class="navigation-block"> <div class="navigation-block">
+1
View File
@@ -18,6 +18,7 @@
<a href="/{{page.title}}/docs">Documentation</a> <a href="/{{page.title}}/docs">Documentation</a>
{%- endif %} {%- endif %}
<a href="https://github.com/uutils/{{page.title}}/releases">Releases</a> <a href="https://github.com/uutils/{{page.title}}/releases">Releases</a>
{% if page.title == "coreutils" %}<a href="/playground">Try it!</a>{% endif %}
</div> </div>
{{ page.content | safe }} {{ page.content | safe }}
{%- endblock main -%} {%- endblock main -%}
+10
View File
@@ -0,0 +1,10 @@
<div class="wasm-example" data-command="{{ command }}">
<div class="wasm-example-code">
<pre><code>$ {{ command }}</code></pre>
<button class="wasm-run-btn" onclick="runWasmExample(this)">Run</button>
</div>
<div class="wasm-example-output" style="display:none">
<pre></pre>
</div>
</div>
<script>if(!document.querySelector('script[src="/js/wasm-example.js"]')){var s=document.createElement("script");s.src="/js/wasm-example.js";s.defer=true;document.head.appendChild(s);}</script>