Examples
Three runnable programs in examples/, in the order they are worth reading.
Example |
What it is for |
|---|---|
|
The smallest thing that works: one factory, one view, a text field and a counter. Read it to see the shape; Quickstart walks through the same code. |
dashboard ( |
A live server dashboard, including a table of everyone connected to the server drawing it. One shared sampler feeding N windows. |
chat ( |
A multi-user chat room with no user accounts, because SSH already authenticated everyone. N writers feeding N windows. |
The first is about the transport. The other two are about the thing the transport makes possible.
Many apps, one process
Both of the larger examples exist to demonstrate one idea that a local Wijjit app never has to confront: N live apps in a single process, sharing state.
A local app is alone. Its state is its own, and the only thing that changes it is the person at the keyboard. Over SSH, the interesting state usually belongs to the server - the machine’s load, the room’s transcript, the queue’s depth - and every connected app is a view onto it, changed by things none of those users did.
So the shape is the same in both examples, and it is small:
hub = Hub() # module scope: the shared state
def make_app(session):
app = Wijjit(backend=session.backend)
@app.view("main", default=True)
def main():
return render(...) # reads hub.<something> directly
hub.join(session.session_id, app)
return app
Nothing is copied into the app. Each view reads the shared object at render time, so the moment the hub’s state changes, every window is already out of date by exactly one redraw.
Getting the redraw
That last redraw is the part with a wrinkle in it.
Each session is its own asyncio task, and while its user is not typing it is
parked inside read_input_async(timeout=...). Whoever changed the shared state
is on a different task - another session, or a background sampler - and cannot
simply call render.
refresh() is what bridges that. It sets a flag, and the
target’s own loop acts on it when its input read next times out:
def broadcast(self):
for subscriber in self.subscribers.values():
subscriber.app.refresh() # safe from any task on this loop
The timeout is the push latency, and it is worth knowing which number you are getting:
|
Worst-case delay before a pushed change is on screen |
|---|---|
unset (the default) |
0.5s - the loop’s fallback poll |
set to |
|
Wijjit takes REFRESH_INTERVAL as a config override on the constructor:
app = Wijjit(backend=session.backend, REFRESH_INTERVAL=0.2) # ~100 ms
The tradeoff is that a session with an interval set also redraws on that
cadence whether or not anything changed. chat_ssh.py sets 0.2 because a
chat room that lags feels broken; dashboard_ssh.py sets 0.5 because its
sampler only produces a new number once a second and anything tighter would burn
frames for nothing. At a few hundred concurrent sessions this is a dial worth
measuring rather than guessing.
Unsubscribing, and why it is not obvious
A hub holding apps must let go of them, or it leaks a whole Wijjit app per disconnect and keeps pushing to windows nobody is looking at.
There is no teardown callback on the app factory, and the tempting in-app signals are all wrong:
Checking ``app.running`` races. The server calls your factory and then starts the app’s task, so a subscriber exists for a moment while
runningis stillFalse. A broadcast landing in that window would evict a session that had not started yet.A Ctrl+Q handler only covers the polite exit. It misses idle timeouts, dropped TCP connections, and
stop().
The signal that has neither problem is
ServerConfig.on_event.
It is documented as a metrics hook, but session.ended fires on every way out
and carries the same session_id your factory registered under:
def on_server_event(event, fields):
if event == "session.ended":
session_id = fields.get("session_id")
if isinstance(session_id, str):
hub.leave(session_id)
WijjitSSH(make_app, host_keys=..., auth=..., on_event=on_server_event)
Both examples use exactly this, and both are worth testing by killing a client rudely - closing the terminal rather than pressing Ctrl+Q - because that is the path a naive implementation gets wrong.
See Logging and metrics for the full event table.