wijjit-ssh
Flask for SSH apps. Serve Wijjit
TUI applications over SSH: Wijjit draws the UI, asyncssh handles the transport
and PTY, and every connection gets its own live app instance.
from wijjit import Wijjit, render_template_string
from wijjit_ssh import WijjitSSH, SSHSession, AuthorizedKeys, ensure_host_key
def make_app(session: SSHSession) -> Wijjit:
app = Wijjit(backend=session.backend) # <- routes I/O to the channel
@app.view("main", default=True)
def main():
return render_template_string(
"{% frame %}{% text %}Hi {{ who }}!{% endtext %}{% endframe %}",
who=session.username,
)
return app
WijjitSSH(
make_app,
host_keys=[ensure_host_key("ssh_host_key")], # generated on first run
auth=AuthorizedKeys("~/.ssh/authorized_keys"),
).run(port=8022)
ssh -p 8022 you@localhost
That is the whole idea: a function that builds an app, and a client that lands
straight in it. No shell, no exec, no SFTP, no port forwarding - a session
only ever runs your Wijjit app.
The backend seam
Wijjit’s event loop talks to “the terminal” through a
TerminalBackend - a small seam covering four
things: frame output, key/mouse input, terminal size, and whether the app owns
the process terminal. Locally that is LocalTerminalBackend.
RemoteTerminalBackend implements the same seam
against an SSH channel:
Concern |
Local backend |
Remote (SSH) backend |
|---|---|---|
Frame output |
|
|
Input |
real stdin, via prompt_toolkit on a thread |
raw channel bytes decoded on the event loop - no thread, no prompt_toolkit |
Size |
|
negotiated PTY size, refreshed on resize, published per task |
Terminal ownership |
|
|
Because Wijjit’s render context and terminal-size override are contextvar-based, N concurrent sessions of different sizes coexist in one process without stepping on each other - each runs as its own asyncio task.
Status
Early, but no longer a prototype. The input path is production-shaped,
authentication is pluggable and fail-closed, resources are bounded by default,
and shutdown drains rather than kills. What is not done yet is backpressure:
a client that stops reading buffers frames in asyncssh without bound. See
Resource limits for what is enforced today.
Getting started
Guide
Examples
Project
Links
Changelog: CHANGELOG.md