# Terminal

The `Terminal` service abstracts standard input and output: reading a line of
text, reading raw key events, displaying messages, and inspecting the terminal
dimensions. It is the foundation for interactive command-line programs. Provide
`NodeTerminal.layer` on Node.js, then write your program against the abstract
`Terminal` interface.

```ts
import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
import { Effect, Terminal } from "effect"

const greet = Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal

  // `display` writes text to stdout. It does not add a newline, so include
  // one yourself when you want the cursor to move down.
  yield* terminal.display("What is your name? ")

  // `readLine` reads one line from stdin. It fails with `QuitError` if the
  // user cancels (typically Ctrl+C), which is why it lives in the error
  // channel rather than throwing.
  const name = yield* terminal.readLine

  yield* terminal.display(`Hello, ${name.trim()}!\n`)
})

NodeRuntime.runMain(greet.pipe(Effect.provide(NodeTerminal.layer)))
```

## Reading input

`readLine` is the workhorse for prompts: it resolves with the typed line and
fails with [`Terminal.QuitError`](https://effect.plants.sh/error-management/) when the user quits. Build
prompt loops by recursing on the effect — validation failures simply re-prompt:

```ts
import { Effect, PlatformError, Terminal } from "effect"

// Repeatedly prompt until the user enters an integer in range.
const promptNumber = Effect.fn("promptNumber")(function*(
  message: string
): Effect.Effect<number, Terminal.QuitError | PlatformError.PlatformError> {
  const terminal = yield* Terminal.Terminal

  yield* terminal.display(`${message} `)
  const input = yield* terminal.readLine

  const parsed = Number.parseInt(input.trim(), 10)

  if (Number.isNaN(parsed) || parsed < 1 || parsed > 100) {
    // Re-prompt on invalid input.
    yield* terminal.display("Please enter an integer from 1 to 100.\n")
    return yield* promptNumber(message)
  }

  return parsed
})
```

## A complete interactive program

Putting prompting and output together, here is a number-guessing game written in
idiomatic v4 style. The secret is drawn from `Random` (never `Math.random`), and
the game loop is a self-recursive effect:

```ts
import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
import { Effect, PlatformError, Random, Terminal } from "effect"

const guessingGame = Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal

  // Draw the secret from the Effect Random service for determinism in tests.
  // `nextIntBetween` is inclusive on both ends by default.
  const secret = yield* Random.nextIntBetween(1, 100)

  const loop: Effect.Effect<
    void,
    Terminal.QuitError | PlatformError.PlatformError
  > =
    Effect.gen(function*() {
      yield* terminal.display("Guess a number (1-100): ")
      const guess = Number.parseInt((yield* terminal.readLine).trim(), 10)

      if (guess === secret) {
        yield* terminal.display("Correct! Well played.\n")
      } else {
        yield* terminal.display(guess < secret ? "Too low.\n" : "Too high.\n")
        yield* loop // keep going until the guess is right
      }
    })

  yield* terminal.display("I'm thinking of a number...\n")
  yield* loop
})

NodeRuntime.runMain(guessingGame.pipe(Effect.provide(NodeTerminal.layer)))
```
**Note:** `readLine` fails with `QuitError` when the user presses Ctrl+C. `runMain`
treats it as a normal cancellation, so the program exits cleanly without an
error report. To take a different action, catch it with
`Effect.catchTag("QuitError", ...)`.

## Raw key events

When a line-based prompt is not enough — for menus, single-keystroke commands,
or custom editors — use `readInput`. It yields a scoped [`Queue`](https://effect.plants.sh/concurrency/)
of `UserInput` events, each carrying the raw character (as an `Option`) and
parsed key metadata including modifier state:

```ts
import { Effect, Option, Queue, Terminal } from "effect"

const readKeys = Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal

  // `readInput` is scoped: the input subscription is released when the scope
  // closes. `Queue.take` blocks until the next key event arrives.
  const queue = yield* terminal.readInput

  yield* terminal.display("Press keys (q to quit)...\n")

  const loop: Effect.Effect<void> = Effect.gen(function*() {
    const event = yield* Queue.take(queue)

    const char = Option.getOrElse(event.input, () => "")
    yield* terminal.display(
      `key=${event.key.name} char=${JSON.stringify(char)} ctrl=${event.key.ctrl}\n`
    )

    if (event.key.name !== "q") yield* loop
  })

  yield* loop
}).pipe(Effect.scoped) // provide and close the scope that owns the subscription
```

## Terminal dimensions

`columns` and `rows` report the size of the terminal, which is useful for
formatting output such as progress bars or tables:

```ts
import { Effect, Terminal } from "effect"

const printSize = Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal
  const columns = yield* terminal.columns
  const rows = yield* terminal.rows
  yield* terminal.display(`Terminal is ${columns}x${rows}\n`)
})
```

For richer command-line applications — argument parsing, subcommands, and
built-in prompts — see the [CLI](https://effect.plants.sh/cli/) section, which builds on `Terminal`.

## Error handling

Two error types flow through the `Terminal` API:

- **`QuitError`** comes only from `readLine`. It signals user-requested
  cancellation (usually Ctrl+C). Because it is a typed failure, you decide what
  happens: re-prompt, clean up, or let it propagate. `NodeRuntime.runMain`
  treats an unhandled `QuitError` as a graceful exit.
- **`PlatformError`** comes from `display` and represents an underlying I/O
  failure when writing to stdout.

```ts
import { Effect, Terminal } from "effect"

const ask = Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal
  yield* terminal.display("Continue? ")
  return yield* terminal.readLine
}).pipe(
  // Handle cancellation by tag instead of letting it propagate.
  Effect.catchTag("QuitError", () => Effect.succeed("n")),
  // Handle a write failure on `display`.
  Effect.catchTag("PlatformError", (error) =>
    Effect.die(error)
  )
)
```

When the failure type is `unknown` (for example after `Effect.catch`), use the
`isQuitError` guard to narrow it:

```ts
import { Terminal } from "effect"

declare const error: unknown

if (Terminal.isQuitError(error)) {
  // error is now narrowed to Terminal.QuitError
}
```

## Reference

The `Terminal` namespace exports the service tag, its interface members, the
input event types, and the cancellation error.

### Terminal (service tag)

`Context.Service` tag for the terminal capability. Access it in a generator
with `yield*`, then use its members. Platform layers such as
`NodeTerminal.layer` provide the concrete implementation.

```ts
import { Effect, Terminal } from "effect"

const program = Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal
  yield* terminal.display("ready\n")
})
// => requires Terminal in its environment
```

### Terminal (interface)

The shape provided by every implementation. Its members are documented below.

```ts
import type { Terminal } from "effect"

// interface Terminal {
//   readonly columns: Effect<number>
//   readonly rows: Effect<number>
//   readonly readInput: Effect<Queue.Dequeue<UserInput, Cause.Done>, never, Scope>
//   readonly readLine: Effect<string, QuitError>
//   readonly display: (text: string) => Effect<void, PlatformError>
// }
declare const _: Terminal.Terminal
```

### terminal.columns

An `Effect<number>` resolving to the number of columns of the terminal.

```ts
import { Effect, Terminal } from "effect"

Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal
  const columns = yield* terminal.columns
  // => 80
})
```

### terminal.rows

An `Effect<number>` resolving to the number of rows of the terminal.

```ts
import { Effect, Terminal } from "effect"

Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal
  const rows = yield* terminal.rows
  // => 24
})
```

### terminal.readLine

`Effect<string, QuitError>` that reads a single line from stdin. Fails with
`QuitError` if the user requests to quit (Ctrl+C).

```ts
import { Effect, Terminal } from "effect"

Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal
  const line = yield* terminal.readLine
  // => "the text the user typed" (without the trailing newline)
})
```

### terminal.readInput

`Effect<Queue.Dequeue<UserInput, Cause.Done>, never, Scope>` that subscribes to
raw input events. It is scoped — the subscription is released when the scope
closes — so run it under `Effect.scoped`. The dequeue completes with
`Cause.Done` when input ends.

```ts
import { Effect, Queue, Terminal } from "effect"

Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal
  const queue = yield* terminal.readInput
  const event = yield* Queue.take(queue)
  // => UserInput { input: Option<string>, key: Key }
}).pipe(Effect.scoped)
```

### terminal.display

`(text: string) => Effect<void, PlatformError>` that writes text to stdout. It
does **not** append a newline — include `\n` yourself when you want one.

```ts
import { Effect, Terminal } from "effect"

Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal
  yield* terminal.display("no newline here")
  yield* terminal.display("…and this continues on the same line\n")
})
```

### UserInput

The event type produced by `readInput`. It carries the raw character (`input`,
an `Option<string>` — absent for non-printing keys) and the parsed `key`.

```ts
import { Option, type Terminal } from "effect"

declare const event: Terminal.UserInput

const char = Option.getOrElse(event.input, () => "")
// => "a"  (or "" for keys like Enter / arrows)
const name = event.key.name
// => "a"
```

### Key

Parsed key metadata on each `UserInput`: the key `name` plus the `ctrl`,
`meta`, and `shift` modifier flags.

```ts
import { type Terminal } from "effect"

declare const key: Terminal.Key

key.name // => "c"
key.ctrl // => true   (e.g. when Ctrl+C was pressed)
key.meta // => false
key.shift // => false
```

### QuitError

The typed error produced by `readLine` when the user quits (usually Ctrl+C). It
is a `Schema.ErrorClass` with `_tag: "QuitError"`, so it can be matched with
`Effect.catchTag("QuitError", ...)`.

```ts
import { Effect, Terminal } from "effect"

const handled = Effect.gen(function*() {
  const terminal = yield* Terminal.Terminal
  return yield* terminal.readLine
}).pipe(
  Effect.catchTag("QuitError", () => Effect.succeed("<cancelled>"))
)
// => Effect<string, never, Terminal>
```

### isQuitError

`(u: unknown) => u is QuitError` — a type guard that narrows an unknown value to
`QuitError`. Useful when a failure has been widened to `unknown`.

```ts
import { Terminal } from "effect"

Terminal.isQuitError(new Terminal.QuitError())
// => true
Terminal.isQuitError(new Error("boom"))
// => false
```

### make

`(impl) => Terminal` constructs a `Terminal` implementation from the concrete
capabilities (`columns`, `rows`, `readInput`, `readLine`, `display`). Use it
when writing a platform adapter or a test implementation. Most applications use
`NodeTerminal.layer` instead of calling `make` directly.

```ts
import { Cause, Effect, Queue, Terminal } from "effect"

// A minimal test terminal that feeds canned input.
const testTerminal = Terminal.make({
  columns: Effect.succeed(80),
  rows: Effect.succeed(24),
  readLine: Effect.succeed("hello"),
  // `readInput` returns a `Queue.Dequeue<UserInput, Cause.Done>`.
  readInput: Queue.make<Terminal.UserInput, Cause.Done>(),
  display: (_text) => Effect.void
})
// => Terminal
```