Skip to main content
████████╗███████╗██████╗ ███╗   ███╗██╗███╗   ██╗ █████╗ ██╗██████╗ ███████╗
╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██║████╗  ██║██╔══██╗██║██╔══██╗██╔════╝
   ██║   █████╗  ██████╔╝██╔████╔██║██║██╔██╗ ██║███████║██║██║  ██║█████╗  
   ██║   ██╔══╝  ██╔══██╗██║╚██╔╝██║██║██║╚██╗██║██╔══██║██║██║  ██║██╔══╝  
   ██║   ███████╗██║  ██║██║ ╚═╝ ██║██║██║ ╚████║██║  ██║██║██████╔╝███████╗
   ╚═╝   ╚══════╝╚═╝  ╚═╝╚═╝     ╚═╝╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝╚═╝╚═════╝ ╚══════╝

A Unix compatible, batteries-included Python library for serving CLI applications in a browser.

Instantly web-enable terminal applications with as few as two lines of code.

PyPI - Version PyPI - Python Version License: MIT

How It Works

Terminaide builds on three core design principles:

  • Instant Web Enablement: Any Python function or script becomes web-accessible without modification
  • Zero Infrastructure: Self-contained with automatic ttyd management and single-port architecture for easy cloud/container deployment
  • Transparent Execution: Preserves execution context (directory, environment variables, virtual environments) as if running locally

When you serve a Python function or script(s) with Terminaide, several things happen behind the scenes:

  1. Function Wrapping: Functions are automatically wrapped in temporary scripts, allowing any Python callable to run in a terminal without modification.

  2. Unified Port Access: A proxy layer routes both terminal and web traffic through a single port, with each terminal getting its own isolated backend process.

  3. Context Preservation: Code runs from the appropriate working directory, maintaining relative imports and file paths as expected.

  4. Virtual Environment Detection: Scripts automatically use their associated virtual environments (.venv, venv, Poetry environments) when available, isolating dependencies without manual configuration.

  5. Environment Inheritance: Environment variables are automatically forwarded to terminal sessions, with optional customization.

  6. Resource Management: All processes, temporary files, and connections are automatically created and cleaned up as needed.

Security

Terminaide serves interactive, writable shells — so its defaults are built around a single principle: unauthenticated terminals must be impossible by construction.

Default posture

  • Loopback everywhere: serve_script()/serve_function() bind 127.0.0.1 (local-only), and every ttyd backend process binds loopback regardless — terminal traffic only ever flows through the proxy. Direct access to the ttyd ports from the network is impossible unless you opt in via ttyd_options.interface.
  • Exposure requires a token: bind a non-loopback host (host="0.0.0.0") without credentials and a session token is auto-generated, printed to the server console as a ready-to-click URL, and required on all terminal routes — the Jupyter model. Loopback-only servers stay frictionless (no token).
  • Verified binary: the ttyd binary is downloaded from a pinned release with SHA-256 verification; unverified downloads are never installed.
  • Minimal disclosure: /health returns {"status": "ok"} unless you opt into the full operational payload with health_verbose=True (or TERMINAIDE_HEALTH_VERBOSE=1).
  • Session isolation: dynamic-route URL parameters are passed per-connection (never shared between sessions), WebSocket connections are rate-limited per client IP (30/min by default), and credential-looking environment variables inherited by unauthenticated terminals trigger a startup warning.

Authentication options

# Auto-generated token (default when exposed) - printed to the console:
serve_script("deploy.py", host="0.0.0.0")

# Your own token (also via TERMINAIDE_TOKEN env var):
serve_script("deploy.py", host="0.0.0.0", auth_token="my-secret")

# ttyd HTTP basic-auth credentials instead of a token:
serve_script("deploy.py", host="0.0.0.0",
            ttyd_options={"credential_required": True,
                          "username": "u", "password": "p"})

# Explicitly unauthenticated (loud warning; only behind your own auth or isolation):
serve_script("deploy.py", host="0.0.0.0", auth_token="")

A validated ?token= is stored in an HttpOnly cookie, so menu navigation and terminal iframes work without the token in every URL. X-Terminaide-Token is accepted for programmatic clients. The token never reaches your terminal's argv or parameter files.

File caches and other knobs

By default, Terminaide creates temporary files only within the package directory (terminaide/cache/). To use external directories, configure them explicitly:

from terminaide import serve_function, TerminaideConfig

config = TerminaideConfig(
    ephemeral_cache_dir="/secure/cache",      # For temporary scripts
    monitor_log_path="/secure/logs/app.log"  # For monitor logs
)
serve_function(my_function, config=config)

# Or use environment variables
import os
os.environ["TERMINAIDE_CACHE_DIR"] = "/secure/cache"
os.environ["TERMINAIDE_MONITOR_LOG"] = "/secure/logs/app.log"

Other security-relevant options: allow_embedding (X-Frame-Options; terminals are clickjacking targets), ws_rate_limit_per_minute (None disables), and forward_env (scope the environment your terminals inherit — see the startup warning).

Installation

Install it from PyPI via your favorite package manager:

pip install terminaide
# or
poetry add terminaide

Terminaide automatically installs and manages its own ttyd binary within the package — downloaded from a pinned release with SHA-256 verification — with no reliance on system-installed versions, to ensure a consistent, tamper-checked experience across environments:

  • On Linux: Pre-built binaries are downloaded automatically
  • On macOS: The binary is compiled from source (requires Xcode Command Line Tools xcode-select --install)

Usage

Terminaide offers three types of Python servers: Script, Function and Apps.

Script Server

The absolute simplest way to use Terminaide is to serve an existing Python script that you don't want to modify. Scripts automatically use their associated virtual environments when available:

from terminaide import serve_script

serve_script("../other_project/client.py")  # Local-only: binds 127.0.0.1, open http://localhost:8000

# To expose to the network, bind explicitly — a session token is auto-generated
# and printed to the console (or set your own with auth_token=...):
serve_script("deploy.py", host="0.0.0.0")

Function Server

If you want total self-containment, you can also pass any Python function to serve_function() and it's instantly accessible. Just make sure to wrap your imports in the function that you're serving:

from terminaide import serve_function

def hello():
    from rich.console import Console

    console = Console()

    name = input("What's your name? ")

    console.print(f"Hello, {name}!")
    
serve_function(hello)

Apps Server

The Apps Server integrates multiple terminal routes into an existing FastAPI application, allowing you to serve scripts, functions, and index pages alongside regular web endpoints. The Apps Server requires a FastAPI app and terminal_routes dictionary (which is both functions and script compatible):

import uvicorn
from fastapi import FastAPI
from terminaide import serve_apps

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Welcome to my app"}

def hello():
    name = input("What's your name? ")
    print(f"Hello, {name}!")

serve_apps(
    app,
    terminal_routes={
        "/script": "my_script.py",    # Script-based terminal
        "/hello": hello,              # Function-based terminal
        "/tool": {                    # Dynamic terminal with query params
            "script": "tool.py", 
            "dynamic": True,
            "args_param": "with"          # Use ?with=arg1,arg2 instead of ?args=arg1,arg2
        }
    },
    # host declares your app's bind for the auth decision: non-loopback + no
    # credentials => terminal routes get a token. Default "0.0.0.0" (assume
    # exposed); pass "127.0.0.1" for purely local apps to skip token auth.
    host="0.0.0.0",
    auth_token=None,   # or set a fixed token / TERMINAIDE_TOKEN env var
)

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

Config

All three serving functions accept the same configuration options, including command-line arguments via the args parameter and dynamic URL query parameters via dynamic: True. When dynamic: True, arguments can be passed via URL query parameters (e.g., ?args=--verbose,--output,file.txt), with the parameter name customizable via args_param:

{
    "port": 8000,                    # Web server port (default: 8000)
    "title": "My Terminal App",      # Terminal window title (default: auto-generated)
    "log_level": "info",             # Logging level: "debug", "info", "warning", "error", None (default: "info")
    "args": ["--verbose", "file.txt"], # Command-line arguments (default: None)
    "dynamic": True,                 # Enable URL query parameter arguments (default: False)
    "args_param": "args",            # Query parameter name for dynamic arguments (default: "args")
    "host": "127.0.0.1",            # Server bind / declared bind (loopback default for direct modes; "0.0.0.0" = exposed)
    "auth_token": None,              # Terminal token: None = auto (required when exposed), "" = disabled, "your-token" = fixed
    "health_verbose": False,        # Full operational payload in /health (default: minimal {"status": "ok"})
    "allow_embedding": False,       # Allow framing by other origins (X-Frame-Options)
    "ws_rate_limit_per_minute": 30, # Per-IP WebSocket connection limit (None = unlimited)
    
    # Keyboard mapping (CMD to CTRL on Mac)
    "keyboard_mapping": {
        "mode": "smart",             # "none" (default), "smart", "all", or "custom"
        "custom_mappings": {         # Override mappings when mode is "custom"
            "z": True,               # Map CMD+Z to CTRL+Z
            "arrowleft": True,       # Map CMD+Left to Home (beginning of line)
            "c": False,              # Don't map CMD+C (leave as system shortcut)
        }
    },
    
    "theme": {                       # Terminal appearance
        "background": "black",       # Background color (default: "black")
        "foreground": "white",       # Text color (default: "white")  
        "cursor": "white",           # Cursor color (default: "white")
        "cursor_accent": "#ff0000",  # Secondary cursor color (default: None)
        "selection": "#333333",      # Selection highlight color (default: None)
        "font_family": "monospace",  # Terminal font (default: None)
        "font_size": 14,             # Desktop font size in pixels (default: 18)
        "mobile_font_size": 10       # Mobile font size in pixels (default: 16)
    }
}

Keyboard Mapping (Mac CMD → CTRL):

On Mac, terminal applications expect CTRL+key shortcuts, but Mac users naturally use CMD+key. Terminaide provides intelligent keyboard mapping with clipboard integration for seamless copy/paste workflow:

# Smart mode: Optimized defaults with clipboard sync
serve_script("my_app.py", keyboard_mapping={"mode": "smart"})
# Clipboard sync: CMD+C → browser copy + terminal CTRL+C (copy errors/logs)
# Paste: CMD+V → browser paste only (no double paste)
# Editing: CMD+Z/Y/X/S/F → terminal CTRL+key
# Navigation: CMD+arrows → terminal navigation (Home/End/CTRL+Home/CTRL+End)
# Browser shortcuts preserved: CMD+W/R/T (close/refresh/new tab)

# All mode: Maps all CMD combinations to terminal CTRL
serve_script("my_app.py", keyboard_mapping={"mode": "all"})

# Custom mode: Granular behavior control
serve_script("my_app.py", keyboard_mapping={
    "mode": "custom",
    "custom_mappings": {
        "c": "both",       # Copy: browser + terminal (clipboard sync)
        "v": "browser",    # Paste: browser only (clean paste)
        "a": "terminal",   # Select all: terminal CTRL+A (select all in terminal)
        "z": "terminal",   # Undo: terminal only
        "x": "both",       # Cut: browser + terminal (if desired)
        "arrowleft": "terminal",  # Navigation: terminal only
        # Behavior options: "both", "browser", "terminal", or omit to disable
    }
})

**Clipboard Integration Strategy:**

The keyboard mapping system intelligently handles clipboard operations to provide the best user experience:

- **`"both"`**: Browser action + terminal signal (ideal for CMD+C to copy terminal output AND send interrupt)
- **`"browser"`**: Browser action only (ideal for CMD+V paste and CMD+A select all)  
- **`"terminal"`**: Terminal signal only (traditional mapping, blocks browser action)
- **Omitted keys**: No mapping (preserves browser shortcuts like CMD+W, CMD+T)

This approach ensures clipboard sync between browser and terminal while avoiding conflicts and double actions.

**Backward Compatibility:**

Existing configurations using boolean values (`True`/`False`) are automatically converted:
- `True` → `"terminal"` (terminal-only behavior)
- `False` → `"none"` (no mapping)

```python
# Legacy format (still works)
keyboard_mapping={"mode": "custom", "custom_mappings": {"c": True, "w": False}}
# Equivalent to:
keyboard_mapping={"mode": "custom", "custom_mappings": {"c": "terminal"}}

# In Apps Server with per-route configuration
serve_apps(app, {
    "/editor": {
        "script": "editor.py",
        "keyboard_mapping": {"mode": "smart"}
    }
})

Dynamic Arguments Usage Examples:

# Default parameter name (uses ?args=...)
serve_script("cli.py", dynamic=True)
# Visit: http://localhost:8000?args=--verbose,--output,result.txt

# Custom parameter name (uses ?with=...)
serve_script("cli.py", dynamic=True, args_param="with")
# Visit: http://localhost:8000?with=--verbose,--output,result.txt

# In Apps Server
serve_apps(app, {
    "/tool": {
        "script": "tool.py",
        "dynamic": True,
        "args_param": "params"  # Uses ?params=...
    }
})

Additionally, the Apps Server accepts several options for managing multiple terminals, routing, and FastAPI integration. You can pass these as keyword arguments to serve_apps() or bundle them in a TerminaideConfig object for reusability.

{
    # Apps Mode Specific Parameters
    "ttyd_port": 7681,                     # Base port for ttyd processes
    "mount_path": "/",                     # Base path for terminal mounting  
    "preview_image": "default.png",        # Default preview image for social media
    "template_override": "custom.html",    # Custom HTML template file
    "trust_proxy_headers": True,           # Trust proxy headers for authentication
    
    # Cache Configuration (Security)
    "ephemeral_cache_dir": "/custom/cache", # Override for ephemeral script storage (default: package cache)
    "monitor_log_path": "/custom/logs/monitor.log", # Override for monitor log file location (default: package cache)
    
    # TTYD Process Options
    "ttyd_options": {
        "writable": True,                  # Allow terminal input
        "interface": "0.0.0.0",           # Bind interface
        "check_origin": True,              # Check WebSocket origin
        "max_clients": 1,                  # Maximum concurrent clients per terminal
        "credential_required": False,      # Require authentication
        "username": None,                  # Authentication username
        "password": None,                  # Authentication password
        "force_https": False               # Force HTTPS connections
    }
}

Utilities

Terminaide also includes a few utilities for turning your Apps Server into a fully functional, stylish website in pure Python.

Auto Index

AutoIndex creates navigable menu pages with ASCII art titles and keyboard navigation using pure Python. It provides a unified API that renders as either a web page or terminal interface based on the type parameter.

{
    "type": "html" | "curses",  # Required: rendering mode
    "menu": [...],              # Required: flat list of menu items
    "title": "MY APP",          # Title text (default: 'Index')
    "subtitle": "Welcome!",     # Text below title
    "instructions": "Navigate", # Menu instruction text
    "epititle": "...",          # Text/link below menu (see below)
    "supertitle": "v1.0",       # Text above title
    "preview_image": "img.png"  # Preview image path (HTML only)
}

The epititle parameter can be configured in three ways:

  • Plain text: epititle="Server status: OK"
  • Terminal route: epititle={"path": "/monitor", "title": "Server Monitor", "function": monitor_func}
  • External link: epititle={"url": "https://github.com", "title": "View on GitHub", "new_tab": True}

In HTML mode, AutoIndex can automatically create terminal routes from menu items that contain functions or scripts, eliminating the need to define routes twice:

from terminaide import AutoIndex, serve_apps

def calculator():
    print("Calculator running...")

def editor():
    print("Editor running...")

# HTML mode with automatic route extraction
serve_apps(app, {
    "/": AutoIndex(
        type="html",
        title="Developer Tools",
        menu=[
            # These create both menu items AND terminal routes
            {"path": "/calc", "title": "Calculator", "function": calculator},
            {"path": "/edit", "title": "Editor", "function": editor, "keyboard_mapping": {"mode": "smart"}},
            {"path": "/logs", "title": "View Logs", "script": "logs.py"},
            {"path": "https://docs.python.org", "title": "Python Docs", "new_tab": True}
        ],
        # Epititle can also define a terminal route
        epititle={"path": "/monitor", "title": "Server Monitor", "function": monitor_func}
    )
})
# Creates: index page at "/" plus terminals at "/calc", "/edit", "/logs", and "/monitor"

In Curses mode, AutoIndex creates an interactive terminal menu that directly executes functions or scripts when selected. Menu items can reference functions, scripts, or Python module paths:

# Curses mode for direct terminal execution
curses_menu = AutoIndex(
    type="curses", 
    title="CLI TOOLS",
    menu=[
        {"function": calculator, "title": "Calculator"},
        {"script": "editor.py", "title": "Text Editor"}
    ],
    instructions="Arrow keys to navigate"
)

# Serve the curses menu as a terminal route
serve_apps(app, {
    "/": curses_menu,  # Interactive menu accessible at root
})

# Or run directly in terminal
curses_menu.show()

Server Monitor

If you want real time visibility into your terminal applications, ServerMonitor wraps your process to capture all output while still displaying it normally. Create a ServerMonitor instance to start logging, then use ServerMonitor.read() in another terminal to view logs with a rich interface featuring scrolling, colors, and keyboard navigation.

from terminaide import serve_apps, ServerMonitor
from fastapi import FastAPI

def my_app():
    ServerMonitor(title="My App")
    print("Hello from monitored app!")

serve_apps(FastAPI(), {
    "/app": my_app,
    "/logs": ServerMonitor.read
})

TerminASCII

Terminaide uses the terminascii() function to generate stylized ASCII art banners from text. This built-in utility creates decorative headers and titles using the "ansi-shadow" font, perfect for adding visual appeal to terminal applications:

from terminaide import terminascii

# Generate ASCII art banner
banner = terminascii("HELLO WORLD")
print(banner)

Terminarcade

The examples/ directory contains working examples that demonstrate all Terminaide features. These serve as both development tests and usage examples:

poe serve-function      # Function mode - demo of serve_function()
poe serve-script        # Script mode - demo of serve_script()
poe serve-apps          # Apps mode - HTML page at root with multiple terminals
poe spin                # Run in Docker container (requires Docker Desktop)

Explore the demo source code to see advanced usage patterns and implementation examples.

Cloud & Container Deployment

Terminaide is cloud-ready when: token auth on, ttyd loopback (default), TLS in front, IMDSv2 on the box. Concretely:

The checklist

  1. Terminate TLS in front. Put an ALB / nginx / CloudFlare in front of the app and pass trust_proxy_headers=True so generated URLs and cookie flags respect X-Forwarded-Proto. Never serve terminals over plain HTTP across a network — keystrokes (including passwords typed into CLIs) and the session token travel in cleartext otherwise.

  2. Pin your token for scripted deployments. Auto-generated tokens are great for humans; for containers and CI, set TERMINAIDE_TOKEN so the URL is deterministic. For Docker, the token appears in docker logs:

    docker run -p 8000:8000 -e TERMINAIDE_TOKEN=my-token terminaide
    # then open http://localhost:8000/?token=my-token
    
  3. Security group: one port. Only the app port (443/8000) needs to be open. The ttyd backends are loopback-bound by default — the historical footgun of opening a 7681+ port range is structurally gone. Never open port ranges.

  4. On EC2: enable IMDSv2 (hop limit 1) and attach a least-privilege IAM role. A writable terminal means the terminal user can reach the instance metadata service — scope the role as if the terminal user will read its credentials, because they can.

  5. Scope the environment. Terminals inherit the server's environment (forward_env=True by default — that's the contract: your scripts, your environment). The startup warning names credential-looking variables; pass forward_env=["PATH", "HOME", "MY_TOOL_API_KEY"] to hand terminals only what they need.

  6. Monitor with health_verbose=True behind your own auth if you want the full operational payload (per-route ports, process health, and the active auth token) — the public /health stays minimal by default.

Topology

Internet → ALB (TLS) → terminaide (FastAPI, one port)
                        ├─ token-auth middleware
                        └─ proxy → ttyd backends (127.0.0.1 only) → your scripts

A note on trust models

Serving yourself your own tools over the internet (token-authenticated) is the supported cloud use case. Serving terminals to untrusted public users is a different threat model: anyone typing at the terminal is executing commands as the server user. If you go there, plan for sandboxing, per-session credentials, and input validation — treat it as a separate product tier, not a configuration.

Integrations

Terminaide pairs well with:

  • Ngrok for exposing local terminal sessions to remote users securely.
  • Lazy Beanstalk for simple cloud deployments to AWS Elastic Beanstalk.

Metadata

Release files for terminaide 2.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for terminaide 2.0.1
File Size Uploaded
terminaide-2.0.1.tar.gz 376.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for terminaide 2.0.1
File Interpreter ABI Platform
terminaide-2.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 758.6 kB

Release files / terminaide-2.0.1.tar.gz

Download URL terminaide-2.0.1.tar.gz
Size 376.3 kB
Tags Source
SHA-256 checksum
How to use checksums
c2a3a6b9b0c1a42463fe44bb79fb1126d7ae798e1ebb478fa72ada99c898c15a
BLAKE2b-256 checksum
How to use checksums
376f761d6aff3201c019da1cfb47d1bcb91436a15b2594688013c7399b5afec4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release files / terminaide-2.0.1-py3-none-any.whl

Download URL terminaide-2.0.1-py3-none-any.whl
Size 382.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e3fa3bcd1a899d1b7199f70fb596199fca00e3f151b54cfcad4d2a43fb9025fe
BLAKE2b-256 checksum
How to use checksums
460bfcb9f50760187aa202336469a464e59731e4bd8cff228bdd7fb9ddc72bca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.0.1 This release

2 release files

2.0.0

2 release files

1.4.8

2 release files

1.4.7

2 release files

1.4.6

2 release files

1.4.5

2 release files

1.4.4

2 release files

1.4.3

2 release files

1.4.2

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page