# `Raxol.Terminal.InlineDriver.ReaderGate`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.7.0/lib/raxol/terminal/inline_driver/reader_gate.ex#L1)

Quiesce/resume the BEAM's prim_tty stdin reader, so an external
process (an `$EDITOR` handed the tty by the harness composer) is not
robbed of keystrokes while it owns the terminal.

## The problem this closes

`Raxol.Terminal.InlineDriver` arms the prim_tty reader process
(registered as `:user_drv_reader`) with a standing `{:read, :infinity}`
and traces its sends -- that is how raw keystrokes reach the inline
input path at all. But the reader holds an outstanding read on the
SAME tty an editor would read from: with both readers live, the kernel
splits typed bytes between them arbitrarily, and the editor sees only
a random subset of what the user types. The reader must be disabled
for the duration of the handoff, and re-enabled on resume.

## The protocol (OTP's own, not an invention)

This is byte-for-byte the wire protocol `:prim_tty.disable_reader/1` /
`:prim_tty.enable_reader/1` speak to the reader process -- the exact
mechanism OTP's own shell uses for its open-in-editor feature (Ctrl+O
in `erl`; see `user_drv:open_editor/2` in the kernel application). We
cannot call `:prim_tty` directly because its `state()` record lives
inside `user_drv`'s gen_statem state, so this module speaks the
reader's message protocol against the registered pid instead -- the
same coupling `InlineDriver.start_stdin_reader/0` already accepts by
sending `{:read, :infinity}` raw.

The call shape is an alias-monitor request:
`ref = monitor(process, reader, alias: :reply_demonitor)`, send
`{ref, :disable | :enable}`, await `{ref, reply}`. While disabled the
reader blocks in a selective receive waiting ONLY for the enable
message (verified against OTP kernel `prim_tty.erl`'s reader loop):
it consumes no tty bytes, so kernel input queues for the editor. Its
armed `{:read, :infinity}` state and the erlang trace flag both
persist across the disable/enable bracket (same process, state map
retained), so no re-arm is needed on resume.

## Failure semantics

  * `nil` reader (headless, piped stdin, `install_reader?: false`) --
    `:ok`, a documented no-op: there is nothing reading the tty, so
    there is nothing to gate.
  * Reply timeout -- `{:error, :timeout}`. A `disable/2` timeout must
    ABORT the suspend (never hand the tty to an editor while the BEAM
    reader still competes for its bytes); an `enable/2` timeout leaves
    input degraded but is survivable (the caller may notify rather
    than crash).
  * Reader died -- `{:error, {:reader_down, reason}}`.

There is one accepted race, the same one OTP accepts: a keystroke
arriving in the instant before the disable is processed may already
have been consumed by the reader. The window is a single scheduler
hop; the byte is delivered to the harness (which is mid-suspend and
ignores it) rather than lost to the kernel.

## Version coupling: pinned range and the drift failure mode

This wire protocol is OTP-private. Tested/verified range: **OTP 26
through 29** (prim_tty itself first shipped the reader in OTP 26).
The reader loop was rewritten in OTP 28 -- `reader_loop/6` on OTP
26/27, `reader_loop/2` on 28+ -- but the disable/enable branch this
module speaks survived that rewrite unchanged, which is what makes
one implementation correct across the whole range. Below the floor,
`disable/2`/`enable/2` refuse with `{:error, {:unsupported_otp,
release}}` rather than sending a message whose receiver semantics
were never verified.

If a FUTURE OTP changes the reader's message shape, the failure mode
is fail-closed by construction, not silent corruption: the gate's
call times out (`{:error, :timeout}`, bounded), and the one caller
that matters treats a disable failure as ABORT-the-suspend -- the tty
is never handed to an editor with the gate in an unknown state. What
becomes of the unmatched message differs by release and neither case
weakens that: OTP 28+ has a catch-all clause that discards it, while
OTP 26/27 has none, so it simply sits in the reader's mailbox. The
gate sends at most one message per call, so that is a bounded
remainder, not a leak. Bumping the pinned range above is a deliberate
act: re-read `prim_tty.erl`'s reader loop for the new release (the
OTP 28 rewrite is the precedent for how much can move) and extend the
ceiling in this doc; the protocol suite (`reader_gate_test.exs`,
scripted readers) pins the wire shape this module SPEAKS, and the pty
round-trip test exercises it against the REAL reader.

# `disable`

```elixir
@spec disable(pid() | nil, timeout()) :: :ok | {:error, term()}
```

# `enable`

```elixir
@spec enable(pid() | nil, timeout()) :: :ok | {:error, term()}
```

---

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