# Interactive terminal

A terminal is an interactive login shell inside a sandbox that you drive with raw bytes instead of one command at a time.

Use it when the session has to keep state between keystrokes: a REPL, a
program that asks questions, or a terminal you render in a browser. For one-off
commands use [Run commands](/docs/sandbox/commands) instead.

Every example reads `IMPELLO_API_KEY` from the environment. Make a key at
[dashboard.impello.ai/keys](https://dashboard.impello.ai/keys).

## Create a terminal

`sandbox.pty.create()` starts `/bin/bash -i -l` attached to a pseudo-terminal
of the size you give it and returns a handle. The handle's `pid` identifies the
terminal in every later call.

<CodeTabs>
<Tab label="TypeScript">

```ts
import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')
const decoder = new TextDecoder()

const terminal = await sandbox.pty.create({
  cols: 80,
  rows: 24,
  onData: (data) => {
    process.stdout.write(decoder.decode(data, { stream: true }))
  },
})

console.log(terminal.pid)
```

</Tab>
<Tab label="Python">

```python
from impello import PtySize, Sandbox

sandbox = Sandbox.create("base")

terminal = sandbox.pty.create(PtySize(rows=24, cols=80))

print(terminal.pid)
```

</Tab>
</CodeTabs>

Output arrives as bytes, not text. In TypeScript `onData` receives a
`Uint8Array` for every chunk the shell writes; decode it with a streaming
`TextDecoder` so a multi-byte character split across two chunks survives. The
sync Python `create()` takes no callback — read the output by iterating the
handle, or pass `on_pty` to `wait()`, both shown below. Python hands you the
raw `bytes` either way, so decode them with an incremental decoder for the same
reason.

The shell starts with `TERM=xterm-256color`, `LANG=C.UTF-8` and
`LC_ALL=C.UTF-8`. Anything you pass in `envs` overrides those.

### Options

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>TypeScript</th><th>Python</th><th>Default</th><th>What it does</th></tr>
  </thead>
  <tbody>
    <tr><td>`cols`, `rows`</td><td>`size: PtySize`</td><td>required</td><td>Terminal size in characters. `PtySize` takes `rows` then `cols`.</td></tr>
    <tr><td>`onData`</td><td>`on_data` (async only)</td><td>required in TypeScript and async Python</td><td>Called with each chunk of output. The sync Python `create()` has no callback.</td></tr>
    <tr><td>`timeoutMs`</td><td>`timeout`</td><td>`60000` ms / `60` s</td><td>How long the SDK stays attached to the terminal.</td></tr>
    <tr><td>`user`</td><td>`user`</td><td>the template's user</td><td>Linux user the shell runs as. The `base` template's user is `user`.</td></tr>
    <tr><td>`envs`</td><td>`envs`</td><td>`{}` plus `TERM`, `LANG` and `LC_ALL`</td><td>Environment variables for the shell.</td></tr>
    <tr><td>`cwd`</td><td>`cwd`</td><td>that user's home directory</td><td>Working directory the shell starts in.</td></tr>
  </tbody>
</table>
</div>

<Callout kind="warning">
The SDK stays attached for 60 seconds by default. When the limit passes the
stream closes and `wait()` throws `TimeoutError` (`TimeoutException` in
Python); the shell is not killed. Re-attach with `pty.connect(pid)` or stop it
with `pty.kill(pid)`.
</Callout>

Raise `timeoutMs` (`timeout` in Python, in seconds) to stay attached longer. In
Python, `timeout=0` removes the limit entirely. TypeScript has no such value —
a `timeoutMs` of `0` is a deadline that has already passed, so the call fails
at once; pass a large number instead.

The thing that ends the session is the sandbox's own timeout: when it runs out
the terminal goes with everything else inside it — killed, or paused if the
sandbox's `lifecycle` says so. See [Timeouts](/docs/sandbox/timeouts).

## Send input

Send keystrokes as bytes with `pty.sendInput()` in TypeScript or
`pty.send_stdin()` in Python — a name difference beyond the casing. Include the
newline; the shell does not run a line until it sees one.

<CodeTabs>
<Tab label="TypeScript">

```ts
const encoder = new TextEncoder()

await sandbox.pty.sendInput(terminal.pid, encoder.encode('echo hello\n'))
await sandbox.pty.sendInput(terminal.pid, encoder.encode('exit\n'))

await terminal.wait()
await sandbox.kill()
```

</Tab>
<Tab label="Python">

```python
import codecs

decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")

sandbox.pty.send_stdin(terminal.pid, b"echo hello\n")
sandbox.pty.send_stdin(terminal.pid, b"exit\n")

terminal.wait(on_pty=lambda data: print(decoder.decode(data), end=""))
sandbox.kill()
```

</Tab>
</CodeTabs>

Control characters are bytes like any other: send `\x03` for Ctrl-C and `\x04`
for Ctrl-D. `wait()` returns once the shell exits, and a non-zero exit code
raises `CommandExitError` (`CommandExitException` in Python); see
[Errors](/docs/errors).

The handle is a `CommandHandle`, the same class a background command returns,
but a terminal has no separate stdin. `handle.sendStdin()` and
`handle.closeStdin()` (`handle.send_stdin()` and `handle.close_stdin()` in
Python) throw `SandboxError` (`SandboxException` in Python) on
it. Always send input through `sandbox.pty`.

## Resize

Call `resize()` whenever the window the terminal is drawn in changes size, so
full-screen programs redraw at the right width.

<CodeTabs>
<Tab label="TypeScript">

```ts
await sandbox.pty.resize(terminal.pid, { cols: 120, rows: 40 })
```

</Tab>
<Tab label="Python">

```python
sandbox.pty.resize(terminal.pid, PtySize(rows=40, cols=120))
```

</Tab>
</CodeTabs>

## Reconnect

A terminal keeps running after the handle that created it goes away, so you can
pick it up from another process or after a network drop. `connect()` takes the
pid and returns a fresh handle.

`sandbox.commands.list()` returns every running command, terminal and template
start command as a `ProcessInfo`. A terminal is the entry whose `args` start
with `-i`: commands run as `/bin/bash -l -c <command>`, terminals as
`/bin/bash -i -l`.

<CodeTabs>
<Tab label="TypeScript">

```ts
import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.connect(process.env.SANDBOX_ID as string)
const decoder = new TextDecoder()

const running = await sandbox.commands.list()
const shell = running.find((p) => p.args[0] === '-i')

if (shell) {
  const terminal = await sandbox.pty.connect(shell.pid, {
    onData: (data) => {
      process.stdout.write(decoder.decode(data, { stream: true }))
    },
    timeoutMs: 60 * 60 * 1000,
  })
  await terminal.wait()
}
```

</Tab>
<Tab label="Python">

```python
import codecs
import os

from impello import Sandbox

sandbox = Sandbox.connect(os.environ["SANDBOX_ID"])
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")

running = sandbox.commands.list()
shell = next((p for p in running if p.args[:1] == ["-i"]), None)

if shell:
    terminal = sandbox.pty.connect(shell.pid, timeout=0)
    for _, _, data in terminal:
        if data is not None:
            print(decoder.decode(data), end="")
```

</Tab>
</CodeTabs>

The sync Python handle is iterable and yields `(stdout, stderr, pty)` tuples
with one field set; for a terminal it is always the third.

`connect()` takes the same `timeoutMs` (`timeout`) as `create()`, with the same
60 second default. The examples above pass their own bound so they stay
attached for longer. Call `handle.disconnect()` to stop receiving output
without killing the shell.

## Kill

`pty.kill()` sends `SIGKILL` to the shell. It returns `true` when the terminal
was killed and `false` when no process has that pid.

<CodeTabs>
<Tab label="TypeScript">

```ts
await sandbox.pty.kill(terminal.pid)
```

</Tab>
<Tab label="Python">

```python
sandbox.pty.kill(terminal.pid)
```

</Tab>
</CodeTabs>

`handle.kill()` does the same for the handle's own pid. Killing the sandbox
kills every terminal in it.

## Async Python

`AsyncSandbox` has the same `pty` module with every method awaited. The async
`create()` and `connect()` take an `on_data` callback, because there is no sync
iterator to read from, and `wait()` takes no callbacks.

```python
import asyncio
import codecs

from impello import AsyncSandbox, PtySize

decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")


async def main():
    sandbox = await AsyncSandbox.create("base")

    terminal = await sandbox.pty.create(
        PtySize(rows=24, cols=80),
        on_data=lambda data: print(decoder.decode(data), end=""),
    )

    await sandbox.pty.send_stdin(terminal.pid, b"echo hello\n")
    await sandbox.pty.send_stdin(terminal.pid, b"exit\n")
    await terminal.wait()
    await sandbox.kill()


asyncio.run(main())
```

Next: [Filesystem](/docs/sandbox/filesystem).
