# `ExRatatui.LocalInput`
[🔗](https://github.com/mcass19/ex_ratatui/blob/v0.13.0/lib/ex_ratatui/local_input.ex#L1)

Hands the local controlling terminal off to the Rust/crossterm reader for
the duration of a TUI session, so keystrokes are not dropped under fast
typing.

`ExRatatui.run/2` and the local `ExRatatui.App` server call this
automatically — most apps never touch it directly. It is public for the one
case that does need it: an app that drives the terminal lifecycle by hand
instead of going through `run/2`, which must park and resume the reader
itself.

## The problem

On a `:local` transport, ExRatatui reads key events through crossterm in a
NIF, which on Unix reads the controlling terminal directly. But the BEAM
keeps its own terminal reader on the same device: on OTP 26+ the `prim_tty`
reader process (registered as `:user_drv_reader`), which reads the tty
whether started from `iex`, `elixir foo.exs`, or a shell-attached release.
Two readers on one terminal means the kernel hands each keystroke to
whichever `read()` wins, so under fast input some bytes never reach the TUI
— they often surface at the shell prompt once the TUI exits, and drop more
readily on macOS than on Linux.

## The handoff

`detach/0` parks the BEAM reader using the same mechanism OTP itself uses on
SIGTSTP — `prim_tty`'s `disable`/`enable` protocol. While disabled the
reader stops calling `read()`, so crossterm gets every byte. `reattach/1`
resumes it on teardown, bringing the shell back — unlike killing the reader,
which leaves the tty in cooked mode and cannot be undone. termios stays raw
throughout, so the handoff is invisible to the shell.

Always pair a `detach/0` with a `reattach/1` — an `after` block is the safe
shape — or the shell loses its input once the app exits:

    handle = ExRatatui.LocalInput.detach()

    try do
      # init_terminal + manual poll loop here
    after
      ExRatatui.LocalInput.reattach(handle)
    end

## When it is a no-op

When no reader is registered — a release booted with `-noinput`, OTP older
than 26, or stdin that is not a tty — every entry point returns
`:not_detached` and does nothing. The session, SSH, and distributed
transports never share a local terminal, so they never call it. Disable the
handoff wholesale with `config :ex_ratatui, detach_local_input: false`.

# `handle`

```elixir
@type handle() :: {:detached, pid()} | :not_detached
```

Opaque handle returned by `detach/0`, passed back to `reattach/1`.

# `detach`

```elixir
@spec detach() :: handle()
```

Parks the BEAM's local terminal reader so crossterm owns the tty.

Returns a handle to pass to `reattach/1` on teardown. A no-op (returning
`:not_detached`) when detaching is disabled or no reader is registered.

# `reattach`

```elixir
@spec reattach(handle()) :: :ok
```

Resumes a reader previously parked by `detach/0`, restoring shell input.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
