jenai
Open a real terminal in a Google Colab cell — including the free tier.
!pip install jenai
%load_ext jenai
%xterm
What it is
A terminal server, a single-page client, and a notebook magic. The client is
one <div>; there is no toolbar, no status bar and no settings panel. The
server is a hand-written asyncio HTTP/1.1 + RFC 6455 implementation that drives
a real pty from the event loop itself — no threads in the data path, no
StreamReader, and one WebSocket per tab carrying raw bytes.
| old | new | |
|---|---|---|
| transport | tornado + HTTP long-poll | one WebSocket, binary frames |
| client input | base64 in a URL path, batched every 100 ms | sent on the keystroke, as UTF-8 bytes |
| client output | base64 in a JSON body, polled | raw pty bytes |
| server data path | thread-pool read() on the pty master |
loop.add_reader on the master fd |
| page | webpack bundle + separate assets | one inlined index.html (93 KiB brotli) |
| reload | started a new shell | re-attaches to the running one |
Measured on the same machine, against the previous implementation:
| metric | legacy | new |
|---|---|---|
| echo round trip (p50 / p99) | 3.7 ms / 7.6 ms | see make bench |
| input ceiling | ~32 KiB per paste, base64 in a URL | unlimited (verified to 1 MiB and beyond) |
| added input latency | up to 100 ms of client batching | none |
| first-paint payload | 426 KiB JS + HTML | 91 KiB, single request |
Run make bench for the live numbers on your own hardware.
Options
%xterm height=1000 port=10001
| option | description |
|---|---|
height |
height of the terminal panel, in pixels |
port |
server port |
Development
Requires Python 3.11+ and Node 20+.
make install # pip install -e '.[dev]' plus the client toolchain
make build # bundle the client into jenai/client/dist/index.html
make test # the whole suite (add -m "not slow" to skip shells/browser)
make lint # ruff check, ruff format --check, mypy --strict, tsc --noEmit
make bench # echo latency, ping, output and input throughput
make profile # cProfile report: server, codec and bundle
Layout
jenai/
app.py CLI entry point, uvloop setup, readiness handshake
server.py asyncio.Protocol HTTP+WebSocket server
wsproto.py RFC 6455 codec
httpproto.py HTTP/1.1 parsing and rendering
assets.py in-memory, pre-compressed static assets
pty_session.py pty sessions driven from the event loop
wire.py control-message protocol shared with the browser
notebook.py the %xterm magic
client/
build.ts esbuild -> one inlined index.html
src/ TypeScript: transport, terminal, protocol
Testing
tests/ runs against real sockets, real forked shells and — where Playwright
is installed — a real headless Chromium. The browser tests are not decoration:
they are what caught the client never wiring onData to the transport, which
no Python test could have seen.
make test # everything
make test-fast # no shells, no browser
make test-browser # Playwright only
pip install playwright && playwright install chromium # for the browser tests
Design notes
- Binary frames carry pty bytes. The old client base64-encoded input into a URL path and output into JSON; both put a ceiling on what a user could type or what the server could send. Binary frames have neither the ceiling nor the encoding cost.
- No client-side batching. The old client accumulated keystrokes for 100 ms before sending. Every keystroke now goes out immediately, and xterm.js coalesces renders on its own schedule.
- Back-pressure pauses the pty. If a browser falls behind, the server stops
reading the pty master instead of buffering output in Python, so a runaway
catcannot balloon the server's memory. - A page reload re-attaches. Session ids live in
sessionStorage; the server keeps the shell alive across the reconnect. - The bundle is inlined. One request, no waterfall, and
.gz/.brsiblings the server serves straight from memory.
Screenshots
Credits
Built on xterm.js and forked from colab-xterm, which got the idea.
Metadata
Release files for jenai 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jenai-1.0.0.tar.gz | 413.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jenai-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 806.2 kB
Release files / jenai-1.0.0.tar.gz
| Download URL | jenai-1.0.0.tar.gz |
|---|---|
| Size | 413.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3afd0c693f69fd89d75557c9b1a44bbbda0ef538e8b7d3a7ea18f55bf6d06303
|
|
BLAKE2b-256 checksum How to use checksums |
1ccfb5b6605c5ab81229dfc7fc4b6c8c8087c0256863e3f1f35ac31fd6f2cebc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / jenai-1.0.0-py3-none-any.whl
| Download URL | jenai-1.0.0-py3-none-any.whl |
|---|---|
| Size | 393.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
85969f3d11bffa4d6dd0de36df43b45ae95406f0dcc702ba3dd1f2af1021eac9
|
|
BLAKE2b-256 checksum How to use checksums |
d704d40a4c2a4b4530df668c7a77ba53318f69775b7e4385ede00f8ef8385e9d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|