# node:readline

`node:readline` and `node:readline/promises` match [Node's `readline`](https://nodejs.org/api/readline.html): line
iteration over any readable stream, prompts and questions, and the cursor helpers. `node:tty` is covered
[below](#terminals-with-nodetty).

## Read lines

An interface is async-iterable, one line per step, with the line ending removed. This counts lines and words on stdin:

```ts
// lines.ts
import { createInterface } from "node:readline";

const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
let count = 0;
let words = 0;
for await (const line of rl) {
  count++;
  words += line.split(/\s+/).filter(Boolean).length;
}
console.log(`${count} lines, ${words} words`);
```

```bash
printf 'the quick brown fox\njumps over\n\nthe lazy dog\n' | elide lines.ts
```

```text
4 lines, 9 words
```

Any readable stream works as `input`; for a file, pass `fs.createReadStream(path)`. `crlfDelay: Infinity` treats
`\r\n` as one line break.

## Ask a question

`question()` from `node:readline/promises` writes the query and resolves with the next line:

```ts
// confirm.ts
import { createInterface } from "node:readline/promises";

const rl = createInterface({ input: process.stdin, output: process.stdout });
const answer = await rl.question("Delete 3 files? [y/N] ");
rl.close();
console.log(answer.trim().toLowerCase() === "y" ? "Deleted." : "Kept.");
```

```bash
echo y | elide confirm.ts
```

```text
Delete 3 files? [y/N] Deleted.
```

Pass `{ signal }` to give up on an answer; the promise rejects with an `AbortError`:

```ts
const answer = await rl.question("Name? ", { signal: AbortSignal.timeout(10_000) });
```

The callback form, `require("node:readline").createInterface(…).question(query, callback)`, works the same way.

> [!NOTE]
> With piped input, readline reads ahead: lines that arrive before the next `question()` are emitted as `line`
> events, not answers. To script several answers, read them with `for await` or a `line` listener.

## Prompt loop

`prompt()` writes the prompt, and each `line` event delivers one entry. `close()` ends the loop and emits `close`:

```ts
// calc.ts
import { createInterface } from "node:readline";

const ops: Record<string, (x: number, y: number) => number> = {
  "+": (x, y) => x + y,
  "-": (x, y) => x - y,
  "*": (x, y) => x * y,
  "/": (x, y) => x / y,
};

const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: "calc> " });
rl.prompt();
rl.on("line", (line) => {
  const expr = line.trim();
  if (expr === "exit") return rl.close();
  const [a, op, b] = expr.split(" ");
  const apply = ops[op];
  console.log(apply ? apply(Number(a), Number(b)) : "usage: NUMBER (+|-|*|/) NUMBER");
  rl.prompt();
});
rl.on("close", () => console.log("bye"));
```

```bash
printf '2 + 3\n6 * 7\nexit\n' | elide calc.ts
```

```text
calc> 5
calc> 42
calc> bye
```

## Redraw a line

`cursorTo`, `moveCursor`, `clearLine` and `clearScreenDown` write the matching escape sequences to a stream. Draw
only when the output is a terminal, so logs and pipes get clean text:

```ts
// progress.ts
import { clearLine, cursorTo } from "node:readline";
import { setTimeout as sleep } from "node:timers/promises";

const total = 5;
for (let done = 0; done <= total; done++) {
  if (process.stdout.isTTY) {
    cursorTo(process.stdout, 0);
    clearLine(process.stdout, 1);
    process.stdout.write(`[${"#".repeat(done).padEnd(total, ".")}] ${done}/${total}`);
  }
  await sleep(100);
}
if (process.stdout.isTTY) process.stdout.write("\n");
console.log("Done.");
```

In a terminal the bar fills in place; `elide progress.ts | cat` prints only `Done.`.

## Read single keys

`emitKeypressEvents` turns raw input into `keypress` events with the key's name and modifiers. Put the terminal in
raw mode to receive each key as it is pressed, and restore it before exiting:

```ts
// keys.ts
import { emitKeypressEvents } from "node:readline";

if (!process.stdin.isTTY) {
  console.error("keys.ts needs a terminal");
  process.exit(1);
}
emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
console.log("Press keys; q or Ctrl+C quits.");

process.stdin.on("keypress", (text, key) => {
  if (key.name === "q" || (key.ctrl && key.name === "c")) {
    process.stdin.setRawMode(false);
    process.exit(0);
  }
  console.log(`${key.ctrl ? "Ctrl+" : ""}${key.name ?? text}`);
});
```

## Terminals with `node:tty`

`node:tty` matches [Node's `tty`](https://nodejs.org/api/tty.html): `isatty(fd)`, `ReadStream` and
`WriteStream`. `process.stdin.isTTY`, `process.stdout.isTTY`, `process.stdout.columns` and `process.stdout.rows`
report the terminal:

```ts
// tty.ts
import { isatty } from "node:tty";

console.log("stdin is a terminal:", isatty(0));
console.log("stdout is a terminal:", isatty(1));
if (process.stdout.isTTY) console.log("size:", process.stdout.columns, "x", process.stdout.rows);
```

In a 100-by-30 terminal, `elide tty.ts` prints:

```text
stdin is a terminal: true
stdout is a terminal: true
size: 100 x 30
```

## Differences from Node

- `readline/promises` does not export the `Readline` class; use the cursor helpers from `node:readline`.
- `import { stdin, stdout } from "node:process"` fails; use the `process` global, as in the examples above.

## See also

- [Build a command-line tool](/docs/cli-tools): arguments, input, prompts, color and exit codes together.
- [`process`](/docs/api-globals-process) for `argv`, `env`, `exitCode` and the standard streams.

<!-- api-reference:start 11e946c0119e -->
## Reference

**Status:** Partial · Not measured · since 1.1.0

### Exports

| Export | Kind | Status | Notes |
| --- | --- | --- | --- |
| `ReadlineInterface` | class | Full | `close()`, `pause()`, `resume()`, `question()`, `prompt()`, `write()`, `line`, `cursor` |
| `createInterface()` | function | Full | Creates a new readline.Interface instance |
| `clearLine()` | function | Full | Clears the current line of the given TTY stream in a specified direction |
| `clearScreenDown()` | function | Full | Clears the screen from the current position of the cursor down |
| `cursorTo()` | function | Full | Moves the cursor to the specified position in the given TTY stream |
| `moveCursor()` | function | Full | Moves the cursor relative to its current position in the given TTY stream |

### `tty`

| Export | Kind | Status | Notes |
| --- | --- | --- | --- |
| `isatty()` | function | Full | Returns true if the given fd is associated with a TTY |
| `ReadStream` | property | Full | The ReadStream class for reading from a TTY |
| `WriteStream` | property | Full | The WriteStream class for writing to a TTY |

### Not implemented

`readline/promises.Readline`.
<!-- api-reference:end -->
