Skip to main content
capsule.pty() opens a real pseudo-terminal (PTY) inside the capsule over a WebSocket connection. You can run any interactive program — a shell, a REPL, a TUI — send keystrokes, resize the terminal, and read raw output bytes in real time. Sessions survive WebSocket disconnects: the process keeps running on the capsule, and you can reconnect to it later using its tag.

Opening a PTY session

capsule.pty() is a context manager. On entry it opens a WebSocket and starts the specified command inside a PTY. On exit it sends SIGKILL to the process and closes the connection.
import sys
from wrenn import Capsule

with Capsule(template="minimal", wait=True) as capsule:
    with capsule.pty(cmd="/bin/bash", cols=120, rows=40, cwd="/home/user") as term:
        term.write(b"ls -la\n")
        for event in term:
            if event.type == "output":
                sys.stdout.buffer.write(event.data)
            elif event.type == "exit":
                break

Parameters

ParameterTypeDefaultDescription
cmdstr"/bin/bash"Command to run inside the PTY.
argslist[str] | NoneNoneAdditional arguments for cmd.
colsint80Initial terminal column count.
rowsint24Initial terminal row count.
envsdict[str, str] | NoneNoneAdditional environment variables.
cwdstr | NoneNoneWorking directory for the process.

Iterating events

Iterating over the session yields PtyEvent objects:
Event typeFieldsDescription
startedpid, tagProcess has started. pid and tag become available on the session.
outputdata: bytesRaw bytes written to the PTY by the process.
exitexit_codeProcess has exited. Iteration stops automatically.
errordata, fatalA stream-level error. Fatal errors stop the iterator.
term.tag and term.pid are None until the started event is received. If you need them before iterating, consume the first event explicitly.

Sending input

term.write(b"echo hello\n")
term.write(b"\x03")  # Ctrl-C

Resizing the terminal

Call resize() whenever the host terminal dimensions change:
term.resize(cols=200, rows=50)

Killing the process

term.kill()  # sends SIGKILL

Session tag and PID

After the started event, term.tag and term.pid are populated:
with capsule.pty(cmd="/bin/bash") as term:
    for event in term:
        if event.type == "started":
            print(f"shell PID: {term.pid}, tag: {term.tag}")
            term.write(b"echo reconnect-demo\n")
        elif event.type == "output":
            sys.stdout.buffer.write(event.data)
        elif event.type == "exit":
            break

saved_tag = term.tag  # save for reconnect

Reconnecting to an existing session

The PTY process keeps running on the capsule after you disconnect. Use capsule.pty_connect() with the saved tag to reattach:
with capsule.pty_connect(saved_tag) as term:
    term.write(b"echo reconnected\n")
    for event in term:
        if event.type == "output":
            sys.stdout.buffer.write(event.data)
        elif event.type == "exit":
            break
Sessions persist until the process exits or the capsule is destroyed. You can disconnect and reconnect as many times as you need without losing the process state.

Accessing services running inside the capsule

If a process inside the capsule listens on a port, use capsule.get_url() to get a proxy URL you can connect to from outside:
# Start a server inside the capsule
capsule.commands.run("python -m http.server 8080", background=True)

# Get the proxy URL
url = capsule.get_url(8080)
print(url)  # "wss://8080-cl-abc123.app.wrenn.dev"

Complete example

import sys
from wrenn import Capsule

with Capsule(template="minimal", wait=True) as capsule:
    # Open an interactive bash session
    with capsule.pty(cmd="/bin/bash", cols=120, rows=40) as term:
        term.write(b"export PS1='$ '\n")
        term.write(b"cd /tmp && python3 -c \"print('hello from PTY')\"\n")
        term.write(b"exit\n")

        for event in term:
            if event.type == "started":
                print(f"[started pid={term.pid} tag={term.tag}]")
            elif event.type == "output":
                sys.stdout.buffer.write(event.data)
                sys.stdout.buffer.flush()
            elif event.type == "exit":
                print(f"\n[exited code={event.exit_code}]")
                break