textual-serve

repository·main·Indexed 18 days ago

https://github.com/textualize/textual-serve

A library that turns Textual TUIs into web applications by running Textual apps in a web browser with minimal code changes. It provides a Server class to host applications, an AppService class to manage the lifecycle of app subprocesses via websockets, and a DownloadManager for handling file deliveries.

Tokens
2.7K
Snippets
7
Records
12
Agent score
64%

What's inside textual-serve

  1. How textual-serve works

    main

    When a user visits the application URL, the server launches an instance of the Textual app in a subprocess and communicates with it via a websocket.

    Key characteristics:

    • Concurrency: You can serve multiple Textual apps across all available CPUs on your system.
    • Security: It uses a custom protocol to communicate with Textual apps rather than exposing a raw shell in the browser. This prevents malicious users from executing unintended commands.
  2. Create a server to run Textual apps in the browser

    main

    You can turn any Textual application into a web application by using the Server class from textual_serve.server. You provide the shell command required to launch your Textual app, and the server handles the rest.

    1. Import Server from textual_serve.server.
    2. Instantiate Server with the command used to run your app (e.g., python -m textual or a specific script path).
    3. Call the .serve() method.
    from textual_serve.server import Server
    
    # Replace the command with whatever launches your app
    server = Server("python -m textual")
    server.serve()
  3. Configure the Server class

    main

    The Server class accepts several parameters to customize the web application hosting environment:

    parameterdescription
    commandA shell command to launch a Textual app.
    hostThe host of the web application (defaults to "localhost").
    portThe port for the web application (defaults to 8000).
    titleThe title shown in the web app on load; leave as None to use the command.
    public_urlThe public URL, if the server is behind a proxy. None for the local URL.
    statics_pathPath to statics folder, relative to server.py. Default uses directory in module.
    templates_pathPath to templates folder, relative to server.py. Default uses directory in module.
  4. Manage Textual app lifecycles with AppService

    main

    The AppService class is responsible for creating and managing a single Textual app subprocess. It handles the communication between the running Textual app (via stdin/stdout) and the client browser (via provided websocket callbacks).

    When a user connects to the websocket, an AppService instance is created to manage that specific session. It manages process startup, terminal resizing, focus/blur events, and file delivery.

    Key Lifecycle Methods:

    • start(width, height): Launches the subprocess and begins the event loop. Do not call run() manually.
    • stop(): Sends a quit meta packet to the process, cancels pending downloads, and waits for the process to exit.

    Communication Flow:

    • To the App: Use send_bytes(), send_meta(), set_terminal_size(), focus(), or blur() to send commands to the subprocess via stdin.
    • From the App: The service listens to the subprocess stdout and triggers on_data(), on_meta(), or on_packed() to relay information back to the browser via the callbacks provided during initialization.
    # Conceptual initialization pattern
    service = AppService(
        command="python my_app.py",
        write_bytes=websocket.send_bytes,
        write_str=websocket.send_text,
        close=websocket.close,
        download_manager=my_download_manager
    )
    await service.start(width=80, height=24)
  5. Handle AppService meta packets

    main

    The AppService automatically processes meta packets (M type) received from the Textual app's stdout. The following meta types are handled internally:

    • exit: Triggers the remote_close() callback to close the websocket.
    • open_url: Extracts url and new_tab from the payload and sends a JSON string to the browser via remote_write_str in the format: ["open_url", {"url": "...", "new_tab": ...}].
    • deliver_file_start: Triggers the DownloadManager to create a download entry and notifies the browser via remote_write_str with ["deliver_file_start", "{delivery_key}"].
  6. Initialize AppService

    main

    To create an AppService instance, you must provide the command to run and several asynchronous callbacks used to communicate with the client browser.

    Parameters:

    • command (str): The shell command used to launch the Textual app subprocess.
    • write_bytes (Callable[[bytes], Awaitable[None]]): Callback to write raw bytes to the client browser websocket.
    • write_str (Callable[[str], Awaitable[None]]): Callback to write string data to the client browser websocket.
    • close (Callable[[], Awaitable[None]]): Callback to close the client browser websocket.
    • download_manager (DownloadManager): An instance of DownloadManager to handle file deliveries.
    • debug (bool, optional): If True, enables debug and devtools in the TEXTUAL environment variable and sets TEXTUAL_LOG to textual.log.
  7. Initialize and run a Server to serve Textual apps

    main

    The Server class is the primary entrypoint for serving a Textual application in a web browser. You instantiate it with the command used to run your app and then call .serve() to start the webserver.

    Constructor Arguments

    • command (str): The shell command used to launch your Textual application.
    • host (str): The host address for the web application. Defaults to "localhost".
    • port (int): The port for the server. Defaults to 8000.
    • title (str | None): The title of the application. If not provided, it defaults to the command string.
    • public_url (str | None): An optional explicit URL for the application. If not provided, it is automatically constructed based on the host and port.
    • statics_path (str | os.PathLike): Path to the static files folder. Defaults to ./static.
    • templates_path (str | os.PathLike): Path to the Jinja2 templates folder. Defaults to ./templates.

    Methods

    • serve(debug: bool = False): Starts the local webserver. This method blocks until the server is closed (e.g., via Ctrl+C). If debug is set to True, it enables debug logging and allows the use of Textual dev tools.
    from textual_serve import Server
    
    # Define the command to run your Textual app
    server = Server(
        command="python my_app.py",
        host="0.0.0.0",
        port=8080,
        title="My Awesome App"
    )
    
    # Start the server
    server.serve(debug=True)
  8. Manage file downloads with DownloadManager

    main

    The DownloadManager class acts as the bridge between the web server and app processes during file downloads. A single server instance uses one DownloadManager to handle all active downloads across all running app processes.

    To implement a download, the app process must first trigger the creation of a download via create_download, and then provide data chunks through chunk_received in response to deliver_chunk_request meta packets sent by the manager.

    # Note: This is a conceptual usage pattern based on the API surface
    manager = DownloadManager()
    
    # 1. Prepare the download (usually called when the app requests a download)
    await manager.create_download(
        app_service=my_app_service,
        delivery_key="unique_key_123",
        file_name="report.pdf",
        open_method="download",
        mime_type="application/pdf"
    )
    
    # 2. The manager will eventually call `download(delivery_key)` to stream chunks to the client.
    # 3. The app process provides data via `chunk_received`
    await manager.chunk_received("unique_key_123", b"binary_data_here")
    # Or for text:
    await manager.chunk_received("unique_key_123", "text_content")
  9. Send commands to the Textual app process

    main

    Use these methods to send instructions from the browser/server to the running Textual application via its stdin.

    MethodDescription
    send_bytes(data: bytes) -> boolSends raw bytes to the process using a D (Data) packet type. Returns True if successful.
    send_meta(data: dict) -> boolSends a JSON-encoded dictionary to the process using an M (Meta) packet type. Returns True if successful.
    set_terminal_size(width: int, height: int)Sends a meta packet with type: "resize" to update the app's terminal dimensions.
    focus()Sends a meta packet with type: "focus" to simulate a terminal focus event.
    blur()Sends a meta packet with type: "blur" to simulate a terminal blur event.
  10. Customize Server lifecycle with on_startup and on_shutdown

    main

    When subclassing Server, you can hook into the application lifecycle by overriding the on_startup and on_shutdown methods. These are called by the underlying aiohttp application during the server's lifecycle.

    • on_startup(self, app: web.Application): Called when the server is starting up.
    • on_shutdown(self, app: web.Application): Called when the server is shutting down.
    from textual_serve import Server
    from aiohttp import web
    
    class MyCustomServer(Server):
        async def on_startup(self, app: web.Application) -> None:
            print("Server is starting up!")
    
        async def on_shutdown(self, app: web.Application) -> None:
            print("Server is shutting down!")
    
    server = MyCustomServer(command="python my_app.py")
    server.serve()