Quickstart¶
A Textual app in a browser, from nothing, in about five minutes. If you already have an app, read Porting an existing app instead.
1. A project¶
build copies a package directory, so the app has to live in one. A single .py file is
not enough to reconstruct the import path.
mkdir -p hello/hello_app && cd hello
python -m venv .venv && source .venv/bin/activate
pip install textual textual-wasm
# hello_app/app.py
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Footer, Header, Label
class Hello(App[None]):
TITLE = "Hello"
BINDINGS = [Binding("space", "cheer", "Cheer")]
def compose(self) -> ComposeResult:
yield Header()
yield Label("Press space.", id="message")
yield Footer()
def action_cheer(self) -> None:
self.query_one("#message", Label).update("Hello from WebAssembly.")
# hello_app/__init__.py
from hello_app.app import Hello
__all__ = ["Hello"]
Run it the ordinary way first, so you know the app itself works:
python -c "from hello_app.app import Hello; Hello().run()"
2. Scan for what will break¶
$ textual-wasm doctor hello_app.app:Hello
no source findings
no blocking issues
Worth doing before the build, because the failures it catches do not raise: os.system()
returns 0 and does nothing, run_in_executor runs inline on the only thread. See
Limitations.
3. Build and serve¶
$ textual-wasm build hello_app.app:Hello hello_app -o dist/
built dist - 9 files, 240 KiB, 11 requirement(s)
packages: textual_wasm, hello_app
pyodide 314.0.6 from CDN
serve it with: textual-wasm dev dist
$ textual-wasm dev dist/
serving dist on http://127.0.0.1:8000
Open it. Pyodide takes a few seconds on the first load — it is fetching a CPython interpreter — and the page says so while it does. After that the app is running in the tab.
Tip
dev is the standard library’s own HTTP server. Requiring Node to look at static files would
undo the point of a Python tool that builds a static site.
4. Deploy it¶
dist/ is finished. There is no further build step.
$ ls dist/
app.json entry.py index.html main.mjs sources.json styles/
Copy it to GitHub Pages, S3, Netlify, or a directory on any web server. Two things to know:
Serve
.wasmasapplication/wasm. Most hosts do; a few old configurations do not, and the failure is a MIME type message, not a 404.No COOP/COEP headers are needed. The default build is main-thread only so the simplest deployment works. See Limitations for what that costs.
5. Check that it behaves the same¶
textual-wasm check --app hello_app.app:Hello \
--ready-marker "Press space." --keys space --settled-marker "Hello from WebAssembly."
The markers are how a machine knows what it is looking at: text that means the app has drawn, keystrokes to send, and text that means those keystrokes were handled. Without the last one a capture races the app and sometimes reads the screen from before the keypress.
What comes back is a runtime-by-runtime table and two comparisons — the Python runtimes check by check, and a real terminal against the browser cell by cell. Building applications explains what to do when a row disagrees.
Where to go next¶
Building applications — the build in depth: titles, custom pages, dependencies, sizes.
Embedding and page customisation — putting the terminal inside a page you already have.
Examples — three complete projects, running live on this site.