# Terminal window

A terminal window framed in box-drawing glyphs that plays a scripted session: typed commands, streamed output, spinners, block progress bars and coloured ok, warn and error lines.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The asciicn agent skill, for building any ASCII effect on the engine, is at [/.well-known/agent-skills/asciicn/SKILL.md](/.well-known/agent-skills/asciicn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the asciicn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `AsciiShader` ([Write your own](/docs/engine/write-your-own.md)), or draw a CPU grid with `AsciiGrid`, and read this example's source for technique. Its full source is in [https://asciicn.dev/r/terminal-window.json](https://asciicn.dev/r/terminal-window.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://asciicn.dev/r/terminal-window.json
```

## Usage [#usage]

```tsx
import { TerminalWindow } from "@/components/asciicn/terminal-window";
import type { TerminalStep } from "@/components/asciicn/terminal-window";

const script: TerminalStep[] = [
  { kind: "command", text: "pnpm test" },
  { kind: "task", text: "Running 128 tests", duration: 1.5 },
  { kind: "output", text: "128 passed", tone: "ok" },
];

export function TestRun() {
  return <TerminalWindow script={script} title="~/acme — test" rows={8} />;
}
```

Leave `script` out to play the built-in session: `npx shadcn add` installs a component, retries once after a registry timeout, fills a progress bar, lists the files it wrote, builds the site and prints a frame of a torus in the `standard` ramp.

A script is a list of steps, each with a `kind`:

* `command`: typed after the prompt one key at a time, with a small, repeatable jitter between keys, then entered.
* `output`: printed a line at a time. `tone` colours it and sets the mark before it: `ok` (`✔`), `info` (`›`), `warn` (`!`), `error` (`✖`), `muted` or `plain`. Set `wrap: false` to clip long lines, for ASCII art, and `alt` to say what the art is to screen readers.
* `task`: a `Loader` spinner for `duration` seconds, then a result mark. `result` is `ok`, `warn` or `error`, and `done` replaces the text when it ends.
* `progress`: a `ProgressBar` in brackets that fills from 0 to 100% over `duration` seconds, as in `[████████░░] 82%`. It stalls and spurts like a real install.
* `wait`: a pause with nothing printed.

After the last step a prompt waits with a blinking `█` cursor. With `loop`, the session starts over 3.5 seconds later.

The window is `rows` lines tall and never changes size. New lines push older ones off the top, and long commands wrap.

## Examples [#examples]

### Custom script [#custom-script]

A deploy that fails a check, retries with the check skipped, uploads and goes live. Twelve rows, the `light` frame and `size="md"`.

```tsx title="examples/terminal-window-deploy.tsx"
import { TerminalWindow } from "@/components/asciicn/terminal-window";
import type { TerminalStep } from "@/components/asciicn/terminal-window";

// A deploy that hits a failing check, gets fixed and ships.
const DEPLOY: readonly TerminalStep[] = [
  { kind: "command", text: "git push origin main" },
  {
    kind: "output",
    text: "To github.com:acme/web.git\n   8e52937..4f2a1c9  main -> main",
    tone: "muted",
  },
  { kind: "command", text: "acme deploy --prod" },
  { duration: 1, kind: "task", text: "Running checks" },
  {
    kind: "output",
    text: "Contrast 3.9:1 on .badge-muted, needs 4.5:1",
    tone: "error",
  },
  { kind: "output", text: "1 check failed. Deploy stopped.", tone: "muted" },
  { kind: "command", text: "acme deploy --prod --skip a11y" },
  {
    kind: "output",
    text: "Skipping a11y checks for this deploy only",
    tone: "warn",
  },
  { duration: 1.6, kind: "progress", label: "Uploading 96 files", width: 16 },
  {
    done: "Live in 31 regions",
    duration: 1.2,
    kind: "task",
    text: "Propagating",
  },
  { kind: "output", text: "https://acme.dev  (4f2a1c9)", tone: "info" },
];

export default function TerminalWindowDeploy() {
  return (
    <TerminalWindow
      script={DEPLOY}
      title="~/acme/web — deploy"
      rows={12}
      size="md"
      frame="light"
      className="max-w-xl"
    />
  );
}
```

### Landing hero [#landing-hero]

The default session at 1.25x speed with a headline. They sit side by side once the container is 760px wide, and stack below that.

```tsx title="examples/terminal-window-hero.tsx"
import { TerminalWindow } from "@/components/asciicn/terminal-window";

export default function TerminalWindowHero() {
  return (
    // Side by side only when the container is wide enough for both.
    <div className="@container w-full max-w-4xl">
      <div className="grid items-center gap-8 @[760px]:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
        <div className="flex flex-col gap-3">
          <p className="text-muted-foreground font-mono text-xs">
            asciicn · registry
          </p>
          <p className="text-2xl font-semibold tracking-tight text-balance">
            One command. Real glyphs on the first frame.
          </p>
          <p className="text-muted-foreground text-sm text-pretty">
            Add a component with the shadcn CLI, build, and the engine prints
            the same frame your page will draw.
          </p>
        </div>
        <TerminalWindow speed={1.25} rows={16} title="~/acme — zsh" />
      </div>
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `script` | `readonly TerminalStep[]` | `DEFAULT_TERMINAL_SCRIPT` | The session to play. Steps are command, output, task, progress and wait. |
| `title` | `string` | `"~/acme — zsh"` | Text in the title bar, such as a path or a command. Also names the window for screen readers. |
| `speed` | `number` | `1` | Playback speed multiplier. |
| `loop` | `boolean` | `true` | Start over 3.5 seconds after the end. When false, the button in the bottom border becomes replay. |
| `rows` | `number` | `18` | Height of the screen in lines. |
| `prompt` | `string` | `"$"` | The prompt before each command. |
| `frame` | `"light" \| "heavy" \| "double" \| "rounded" \| "ascii"` | `"rounded"` | Box-drawing set for the window frame. |
| `color` | `AsciiColor` | `"grey"` | Frame colour, a palette name or any CSS colour. |
| `size` | `"sm" \| "md" \| "lg"` | `"lg"` | Glyph cell height: 12, 14 or 16px. |
| `cell` | `number` |  | Glyph cell height in CSS pixels. Overrides size. |

`TerminalWindow` also takes the `AsciiBox` props `shadow`, `filled`, `padding` and `labelAlign`, and any `div` attribute.

## Notes [#notes]

* The screen is hidden from screen readers, because it changes many times a second. A visually hidden transcript of the whole session sits next to it, and the window is a group named by `title`.
* The clock runs only while the window is on screen and the tab is visible, so the session starts when the visitor scrolls to it.
* The bottom border holds a pause button, which turns into play when paused and into replay when a session without `loop` ends. It is a real button, reachable with Tab.
* With reduced motion, the window shows the finished session as one still frame, with a steady cursor and no button.
* The text is DOM text in `--ascii-font`, so it stays sharp at any zoom and can be selected and copied.