Skip to main content

Serve Textual apps over SSH

Project description

textish

Python License Built with asyncssh Powered by Textual

Serve Textual TUI apps over SSH. Point it at any command that runs a Textual app, give it a port, and anyone with an SSH client can connect and use the app in their terminal — no installation required on their end.

from textish import serve

serve("python my_app.py", port=2222)
ssh localhost -p 2222

How it works

Textual ships with a WebDriver that runs an app in a subprocess and communicates over pipes using a simple length-prefixed packet protocol. textish reuses that mechanism entirely. Each SSH connection spawns the Textual app as a fresh subprocess and bridges the SSH channel to the subprocess's stdin/stdout. Nothing inside Textual is patched or monkeypatched — the app has no idea it's being served over SSH.

This is the same basic idea as wish (Charmbracelet's SSH app framework for Go) and inkish, adapted to work with Textual's existing driver infrastructure.


Installation

Requires Python 3.12 or later.

pip install textish

Usage

Command line

textish "python my_app.py"
textish "python my_app.py" --port 3000
textish "python my_app.py" --host 127.0.0.1 --port 3000 --max-connections 10
$ textish --help
usage: textish [-h] [--host HOST] [--port PORT] [--host-key PATH]
               [--max-connections N]
               app_command

Serve a Textual app over SSH.

positional arguments:
  app_command           Shell command that launches your Textual app,
                        e.g. "python my_app.py".

options:
  --host HOST           Address to listen on. (default: 0.0.0.0)
  --port PORT           TCP port to listen on. (default: 2222)
  --host-key PATH       Path to the SSH host key file.
                        Defaults to ~/.ssh/ssh_host_key.
  --max-connections N   Maximum simultaneous SSH sessions.
                        0 means unlimited. (default: 0)

Python API

from textish import serve

# Note: requires a host key at ~/.ssh/ssh_host_key by default
serve("python my_app.py", port=2222)

Async

If you are already inside a running event loop (for example, embedding textish inside a larger async application):

from textish import serve_async

await serve_async("python my_app.py", port=2222)

Configuration object

from textish import serve_config, AppConfig

config = AppConfig(
    app_command="python my_app.py",
    port=2222,
    max_connections=10,
)
serve_config(config)

Host keys

By default, textish looks for a host key at ~/.ssh/ssh_host_key. You can generate one with:

ssh-keygen -t ed25519 -f ssh_host_key -N ""

Or pass an explicit path:

serve("python my_app.py", port=2222, host_keys=["./ssh_host_key"])

Public-key authentication

By default, textish allows all connections without authentication — suitable for private networks. To restrict access, pass an auth_function:

ALLOWED_KEYS = {
    "ssh-ed25519 AAAAC3Nza..."}

def auth(username: str, public_key: str) -> bool:
    return public_key in ALLOWED_KEYS

serve("python my_app.py", port=2222, auth_function=auth)

The function receives the username and the client's public key in OpenSSH format. It may also be async.


Limitations

A few things worth knowing before you deploy this anywhere serious.

One process per connection. Every SSH connection spawns a completely independent subprocess running your app. There is no shared state between clients, and no concept of a persistent session. If a client disconnects and reconnects, they get a brand new app instance from scratch.

No reconnection support. Related to the above — if a client's connection drops mid-session, there is nothing to reconnect to. The subprocess is terminated and any in-progress state is gone.

PTY required. textish only supports interactive shell sessions with a pseudo-terminal. Clients that connect without a PTY (for example, ssh host -p 2222 some-command) will be rejected with an error message. This is a deliberate constraint, not something that is straightforward to lift.

The app must use Textual's WebDriver. textish sets TEXTUAL_DRIVER=textual.drivers.web_driver:WebDriver in the subprocess environment. If your app overrides the driver or uses something incompatible, the handshake will fail and the connection will be dropped.


Development

Install with dev dependencies:

poetry install --with dev

Run the tests:

poetry run pytest

Lint:

poetry run ruff check .

Type check:

poetry run mypy

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

textish-0.2.0.tar.gz (15.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

textish-0.2.0-py3-none-any.whl (16.8 kB view details)

Uploaded Python 3

File details

Details for the file textish-0.2.0.tar.gz.

File metadata

  • Download URL: textish-0.2.0.tar.gz
  • Upload date:
  • Size: 15.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for textish-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e28824e15d707c3401c182f3aa1072aba7ba005cf3f087363879d157afb5e3d5
MD5 3f8341c0472ca0f8477ea8fa3f28f30b
BLAKE2b-256 5d9f5fbc9037260c9c9992bde220e37601e08818b7650098fac06789ecd32bdd

See more details on using hashes here.

Provenance

The following attestation bundles were made for textish-0.2.0.tar.gz:

Publisher: release.yml on Aisha630/textish

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file textish-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: textish-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 16.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for textish-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 da6272ebeaca7f70f73dddd77f10d1ee0ed837f08f7e3719aa6645529843efb9
MD5 21f17aa18c986a4851ee3714dd5a763b
BLAKE2b-256 ba6818ec6df13ba18489a6a83fb43d6f1103010926a90ffc2519216a0284dd72

See more details on using hashes here.

Provenance

The following attestation bundles were made for textish-0.2.0-py3-none-any.whl:

Publisher: release.yml on Aisha630/textish

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page