# Terminal

> Terminal-style output with prompt lines, ANSI colours mapped to tokens, streaming, copy and follow-the-tail mode. Optional command input.

Source: https://docs.nasaqui.com/components/terminal

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/terminal.json
```

A dark, monospace output surface for command output. Lines are plain strings or `{ kind, text }`; a `command`
line shows the prompt, and ANSI escape codes in the text become coloured spans that use `--nq-*` tokens, so the
palette follows the theme and never uses raw hex. While `streaming`, the view follows the newest line, and if the
reader scrolls up it stops following and offers a "Jump to latest" button. Output is capped at `maxLines`
(default 2000) so long-running processes stay fast. Presentational: you own the lines and append to them.

## When to use

- Output of a build, install, migration or script, live or finished.
- A demo shell with `onCommand`, backed by your own handler.

## When not to use

- Structured, filterable logs: use [LogViewer](https://docs.nasaqui.com/components/log-viewer).
- A step-by-step pipeline: use [DeployView](https://docs.nasaqui.com/components/deploy-view).
- A one-line command to copy: use `CommandSnippet` from [code-block-variants](https://docs.nasaqui.com/components/code-block-variants).
- A real shell: this is not a terminal emulator. Cursor movement and full-screen programs are not handled.

## Import

```tsx
import { Terminal } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Terminal } from "@fadymondy/nasaq/web";

export function Build() {
  return (
    <Terminal
      title="~/app"
      streaming
      lines={[
        { kind: "command", text: "pnpm build" },
        "\u001b[32m✓\u001b[0m compiled 214 modules",
        { kind: "error", text: "warning: chunk is larger than 500 kB" },
      ]}
    />
  );
}
```

## Anatomy

```
Terminal        data-slot="terminal"           <div dir="ltr">
├─ header       data-slot="terminal-header"    title, live badge, wrap / clear / copy
├─ output       data-slot="terminal-output"    <div role="log"> scrolling area, follows the tail
│  └─ row       data-slot="terminal-row"       optional line number, prompt, ANSI spans
├─ jump         "Jump to latest"               shown when not following
└─ input        data-slot="terminal-input"     only with onCommand
```

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `lines` | `(string \| { kind?, text, id? })[]` | required | Oldest first. `kind`: `command output error info success`. |
| `title?` | `string` | "Terminal" / "الطرفية" | Header text. |
| `prompt?` | `string` | `"$"` | Shown before command lines and the input. Never copied. |
| `streaming?` | `boolean` | `false` | Live indicator and cursor. |
| `follow?` | `boolean` | `true` | Start pinned to the newest line. |
| `maxLines?` | `number` | `2000` | Older rows are dropped with a note. |
| `lineNumbers?` | `boolean` | `false` | Gutter numbers. |
| `wrap?` | `boolean` | `false` | Wrap long lines. The header has a toggle. |
| `copyable?` | `boolean` | `true` | Copy button. Copies clean text without ANSI codes. |
| `onClear?` | `() => void` | none | Shows a clear button. |
| `onCommand?` | `(command: string) => Promise<void> \| void` | none | Adds an input. It is disabled until the promise settles. Up and down walk history. |
| `height?` | `string \| number` | `"20rem"` | Height of the output area. |
| `labels?` | `Partial<TerminalLabels>` | built-in en/ar | Translations. |

Also exported: `AnsiText` (spans for one string, for use inside your own `<pre>`), `parseAnsi`, `parseAnsiRows`,
`stripAnsi`, `ansiColor`, `ansi256`, and the `useFollowScroll` hook used by [LogViewer](https://docs.nasaqui.com/components/log-viewer) and
[DeployView](https://docs.nasaqui.com/components/deploy-view).

### ANSI support

SGR codes: reset, bold, dim, italic, underline, inverse, strike, the 16 standard and bright colours, 256-colour and
truecolour. A carriage return redraws the row, so progress bars show their last state. Other escapes (cursor
movement, OSC titles) are removed.

## Examples

### Streaming

```tsx
const [lines, setLines] = useState<TerminalLine[]>([{ kind: "command", text: "pnpm test" }]);
// append as chunks arrive: setLines((l) => [...l, chunk])
<Terminal lines={lines} streaming={running} onClear={() => setLines([])} />;
```

### A command box

```tsx
<Terminal lines={lines} onCommand={async (cmd) => { setLines((l) => [...l, { kind: "command", text: cmd }]); await run(cmd); }} />
```

## Accessibility

| Key | Action |
| --- | --- |
| `Tab` | Output region, then the header buttons and input |
| Arrow keys / `PageUp` / `PageDown` | Scroll the focused output |
| `End` | Jump to the newest line and resume following |
| `Up` / `Down` | History, in the input |

- The output is `role="log"` with a name, so screen readers can read it; it does not announce every streamed line.
- Colour is supplementary: errors also carry the `error` kind text and stay readable without colour.
- The jump button is a real button and appears only when following stopped.

## RTL & i18n

- The surface is `dir="ltr"` in Arabic pages; only the chrome (title, tooltips, buttons) is translated.
- Built-in Arabic strings; override with `labels`.
- Header controls sit on the logical inline-end side.

## Styling & tokens

- Surface `bg-nq-surface-soft` with `border-border` and `rounded-surface`; text `text-nq-fg-body`; kinds use `text-nq-danger-text`, `-success-text` and `-info-text`.
- ANSI colours are `var(--nq-*)` values; bright variants mix toward `--nq-fg`.
- Slots: `terminal`, `terminal-header`, `terminal-output`, `terminal-row`, `terminal-input`. `data-streaming` on the root.

## Do / Don't

- **Do** append lines instead of rebuilding the whole array when you can.
- **Do** keep `maxLines` modest for very chatty processes.
- **Don't** print secrets to the terminal; there is no masking.
- **Don't** expect keyboard-driven programs to work.

## Related

- [LogViewer](https://docs.nasaqui.com/components/log-viewer) · [DeployView](https://docs.nasaqui.com/components/deploy-view) · [CodeBlock variants](https://docs.nasaqui.com/components/code-block-variants)

## Lab

https://docs.nasaqui.com/?path=/docs/components-developer-tools-terminal--docs

## Code

### React

```tsx
import { Terminal } from "@fadymondy/nasaq/web";

export function Build() {
  return (
    <Terminal
      title="~/app"
      streaming
      lines={[
        { kind: "command", text: "pnpm build" },
        "\u001b[32m✓\u001b[0m compiled 214 modules",
        { kind: "error", text: "warning: chunk is larger than 500 kB" },
      ]}
    />
  );
}
```

### shadcn

```tsx
import { Terminal } from "@/components/ui/terminal";

export function Build() {
  return (
    <Terminal
      title="~/app"
      streaming
      lines={[
        { kind: "command", text: "pnpm build" },
        "\u001b[32m✓\u001b[0m compiled 214 modules",
        { kind: "error", text: "warning: chunk is larger than 500 kB" },
      ]}
    />
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqTerminal } from "@fadymondy/nasaq/vue";

const lines = [
  { kind: "command" as const, text: "pnpm build" },
  "\u001b[32m✓\u001b[0m compiled 214 modules",
  { kind: "error" as const, text: "warning: chunk is larger than 500 kB" },
];
</script>

<template>
  <NqTerminal title="~/app" streaming :lines="lines" />
</template>
```

### Blade

```blade
@php
    $lines = [
        ['kind' => 'command', 'text' => 'pnpm build'],
        "\e[32m✓\e[0m compiled 214 modules",
        ['kind' => 'error', 'text' => 'warning: chunk is larger than 500 kB'],
    ];
@endphp
<x-nq::terminal title="~/app" streaming clearable command :lines="$lines" />
```

### HTML + Alpine

```html
<div data-slot="terminal" dir="ltr" x-data="nqTerminal(JSON.parse('{\u0022lines\u0022:[{\u0022kind\u0022:\u0022command\u0022,\u0022text\u0022:\u0022pnpm build\u0022},\u0022\\u001b[32m\\u2713\\u001b[0m compiled 214 modules\u0022,{\u0022kind\u0022:\u0022error\u0022,\u0022text\u0022:\u0022warning: chunk is larger than 500 kB\u0022}],\u0022prompt\u0022:\u0022$\u0022,\u0022streaming\u0022:true,\u0022follow\u0022:true,\u0022maxLines\u0022:2000,\u0022wrap\u0022:false,\u0022strings\u0022:{\u0022trimmedOne\u0022:\u00221 earlier line hidden\u0022,\u0022trimmedMany\u0022:\u0022{n} earlier lines hidden\u0022}}'))" x-bind:data-streaming="streaming ? '' : null"
    class="relative flex min-w-0 flex-col overflow-hidden rounded-surface border border-border bg-nq-surface-soft text-start">
    <div data-slot="terminal-header" class="flex h-row shrink-0 items-center justify-between gap-2 border-b border-border ps-3 pe-1.5">
        <span class="flex min-w-0 items-center gap-2 font-mono text-caption text-muted-foreground">
            <span class="truncate">~/app</span>
            <span x-show="streaming"  class="inline-flex shrink-0 items-center gap-1 font-sans text-nq-success-text">
                <svg data-slot="spinner" aria-hidden="true" class="animate-spin motion-reduce:animate-none size-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>                Streaming
            </span>
        </span>
        <span class="flex shrink-0 items-center">
            <button data-slot="button"
     type="button"                         aria-label="Wrap lines" x-on:click="wrap = ! wrap" x-bind:aria-pressed="wrap ? &#039;true&#039; : &#039;false&#039;" x-bind:data-active="wrap ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 data-active:bg-nq-selected">
        <svg data-slot="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="m16 16-2 2 2 2"/>
    <path d="M3 12h15a3 3 0 1 1 0 6h-4"/>
    <path d="M3 18h7"/>
    <path d="M3 6h18"/>
</svg></button>
                            <button data-slot="button"
     type="button"                         aria-label="Clear" x-on:click="clear()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0">
        <svg data-slot="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21"/>
  <path d="m5.082 11.09 8.828 8.828"/>
</svg></button>
                                        <button data-slot="button"
     type="button"                         aria-label="Copy output" x-on:click="copy()" x-bind:data-copied="copied ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 data-copied:text-nq-success-text">
        <svg data-slot="icon" aria-hidden="true" x-show="! copied" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
  <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
</svg>                    <svg style="display: none;" data-slot="icon" aria-hidden="true" x-show="copied" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></button>
                    </span>
    </div>
    <div x-ref="output" data-slot="terminal-output" role="log" aria-label="Terminal output" aria-live="off" tabindex="0"
        style="height: 20rem" x-on:scroll="onScroll()"
        class="min-h-0 overflow-auto py-2 font-mono text-code outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus">
        <div class="min-w-full" x-bind:class="wrap ? 'w-full' : 'w-max'">
            <p x-show="hidden > 0" style="display: none" class="px-3 pb-1 text-caption text-muted-foreground" x-text="trimmedText()"></p>
            <p x-show="empty"  style="display: none"  class="px-3 text-muted-foreground">No output yet</p>
            <template x-for="(row, i) in rows" :key="row.key">
                <div data-slot="terminal-row" x-bind:data-kind="row.kind" x-bind:class="rowClass(row)">
                                        <span x-show="row.prompt" style="display: none" aria-hidden="true" class="me-2 shrink-0 select-none text-nq-accent-text" x-text="prompt"></span>
                    <span class="min-w-0">
                        <template x-for="(s, j) in row.spans" :key="j">
                            <span x-bind:style="spanStyle(s.style)" x-bind:class="spanClass(s.style)" x-text="s.text"></span>
                        </template>
                    </span>
                </div>
            </template>
            <div x-show="streaming"  class="flex min-h-[1lh] px-3" aria-hidden="true">
                <span class="inline-block h-[1lh] w-[0.6em] bg-nq-fg motion-safe:animate-pulse"></span>
            </div>
        </div>
    </div>
    <button data-slot="button"
     type="button"                         style="display: none; bottom: 3.25rem" x-show="showJump" x-on:click="jump()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control-sm px-2.5 absolute end-3 shadow-sm">
        <svg data-slot="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M12 17V3"/>
  <path d="m6 11 6 6 6-6"/>
  <path d="M19 21H5"/>
</svg>        Jump to latest</button>
            <form data-slot="terminal-input" x-on:submit.prevent="submit()" class="flex h-control shrink-0 items-center gap-2 border-t border-border px-3 font-mono text-code">
            <span aria-hidden="true" class="select-none text-nq-accent-text" x-text="prompt">$</span>
            <input x-ref="input" x-model="command" x-on:keydown="onKey($event)" x-bind:disabled="busy ? '' : null"
                x-bind:placeholder="busy ? 'Running' : null" aria-label="Command"
                autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"
                class="min-w-0 flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground disabled:opacity-60">
        </form>
    </div>
```
