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

Inline driver profile (unit T2d).

A sibling of `Raxol.Terminal.Driver`, not a replacement: today's driver
enters the alternate screen at init (`\e[?1049h`) and termbox owns the
whole tty. This module never does either. It puts the tty in raw mode
(no echo, no line buffering, no signal generation), runs T1's capability
probe over the same input fd, and streams parsed key events to a
subscriber -- all while leaving the terminal's native scrollback
completely alone. T2b (printed-history append) and T2a (scroll-region
manager) are separate units layered on top; this module sets no scroll
region itself.

## Constructor options

  * `:dispatcher_pid` -- the default subscriber for parsed input events
    (see below). May be `nil` (headless use).
  * `:subscriber` -- overrides `:dispatcher_pid` as the input-event
    target, for tests that want a separate collector.
  * `:raw_sink` -- optional pid that receives every PRE-parse input
    chunk as `{:inline_raw_input, binary()}`, exactly as it arrived
    from the tty reader, before `InputParser` sees it. Debug/observability
    seam (byte-level input tracing); default `nil` -- when unset, nothing
    changes on the input path.
  * `:device` -- output sink. Default `:stdio`. Accepts `:stdio` or any
    `t:IO.device/0` (a `StringIO` pid works great in tests -- everything
    here is written via plain `IO.write/2`, never raw port writes).
    **This is the suite design's hard requirement**: the output device
    is a parameter, not a hardcoded `:stdio` write, so Tier A can
    capture bytes with no pty and no termbox.
  * `:stty` -- module implementing `save/0`, `raw!/0`, `restore/1`
    (default `Raxol.Terminal.Driver.Stty`). Inject a recording stub in
    tests so OS-level tty state is never touched by a pure test run.
    Note: `raw!/0`'s termios flags include `-isig`, so once raw mode is
    entered the kernel stops generating SIGINT/SIGTSTP from Ctrl-C /
    Ctrl-Z -- interrupt delivery and job control become this driver's
    responsibility (or the app's) until teardown restores the saved
    settings.
  * `:tty?` -- override real-tty detection (default
    `Raxol.Terminal.TerminalUtils.has_terminal_device?/0`). Drives the
    default of `:install_reader?` and `:stty_enabled?` below.
  * `:stty_enabled?` -- whether to actually invoke the injected `:stty`
    module (default: same as `:tty?`). Kept independent of `:tty?` so a
    test can force the byte-emission path (`tty?: true`) while keeping
    `stty_enabled?: false` -- the real OS tty of whatever process is
    running the test suite is never touched by Tier A.
  * `:install_reader?` -- whether to hook the prim_tty trace-based stdin
    reader (default: same as `:tty?`). Independent for the same reason.
  * `:rows` -- terminal height used for the final teardown cursor move
    (default: detected via `:io.rows/0`, falling back to the injected
    `:stty` module's `size/0`, falling back to 24).
  * `:probe?` -- whether to run the T1 capability probe at startup
    (default `true`). Tests that only care about teardown ordering can
    pass `false` to skip the (bounded, ~100ms local / ~1s SSH) wait.
  * `:capabilities` -- a pre-computed `%Raxol.Terminal.Capabilities{}`.
    When given, the probe is skipped entirely and this record is cached
    directly -- the fastest test seam, and also useful for embedding
    contexts that already know the answer.
  * `:probe_opts` -- forwarded verbatim to
    `Raxol.Terminal.Capabilities.Probe.new/2` (`:budget_ms`,
    `:extend_ms`, `:now_ms`, `:tmux_passthrough?`, `:platform`).
  * `:probe_env` -- the env map the probe seeds from (default
    `System.get_env/0`).

## The output-device + teardown seam

`emit_teardown/2` is the pure(-ish) seam the suite design calls for: it
takes the output device and a driver state and writes the canonical
teardown byte sequence (`Raxol.Terminal.InlineDriver.Sequences`),
restores the OS tty via the injected stty module, and returns an updated
state with `torn_down?: true`. It is idempotent -- a state that already
has `torn_down?: true` is returned unchanged and nothing is written, so
calling it twice (a signal handler AND `terminate/2`, say) can never
double-emit `\e[r` or reposition the cursor twice (LC-N-DOUBLE).

`terminate/2` is the only caller in this module. It runs whenever THIS
process terminates for a reachable reason: a direct `GenServer.stop/1`
on the driver, or a crash inside any `handle_manager_*` callback (a
raised error inside a GenServer callback still runs that same process's
own `terminate/2` -- orthogonal to `trap_exit`, which only gates
*external* exit signals). `System.halt` and `SIGKILL` run no cleanup at
all -- documented residual, not a gap in this module (`kill -9` cannot
be caught by anything running in the killed process).

## Teardown-on-quit through Lifecycle: graceful stop FIXED (T28a), SIGTERM open (T28b)

The driver's teardown itself is correct and deterministic; whether
Lifecycle *invokes* it on shutdown was unit T28's subject, since split
into two facets with different fates:

  * **`Raxol.stop/1`** (in-process graceful stop): **deterministic
    since T28a** (merged). `Lifecycle` now traps exits and drives
    teardown from its own `terminate/2`, driver-first, so a dependent's
    `:shutdown` exit can no longer race back through the link and kill
    `Lifecycle` before this driver is stopped -- this `terminate/2`
    runs by construction. (Pre-T28a this was a measured ~50% miss under
    ExUnit load.) Enforced by the now-unskipped graceful-stop test in
    `test/harness/t2d_teardown_positive_test.exs`.

  * **OTP default SIGTERM -> `init:stop/0`** (the real VM tree-unwind a
    production `kill -TERM` / container stop triggers): teardown is
    still **never reached**. The tree unwind does not route through
    `Lifecycle` at all, so this `terminate/2` is skipped every time
    (measured under a real pty: no `\e[r`, no modes-off in the
    capture). This is the stdio-shutdown race the termbox `driver.ex`
    already flags (~line 494) surfacing as a total miss on the inline
    path. Tracked as **unit T28b** (the first SIGTERM handler attempt
    self-deadlocked in `:erl_signal_server` and was reworked). Until
    T28b lands, production SIGTERM teardown relies on the app arranging
    its own handler that calls `Raxol.stop/1` (as the Tier B
    `LC-P-SIGTERM` test does); the gap stays pinned by the remaining
    skipped `@tag :pending_t28b` test. The driver's own teardown is
    meanwhile proven deterministically by stopping the driver process
    directly (`LC-P-CLEAN`).

## Input contract

Parsed input events are sent to the subscriber as
`{:inline_input, %Raxol.Core.Events.Event{}}` messages -- a simple
pid/message contract, intentionally thin. Unit T13a wires the real
Dispatcher; until then this seam is the whole contract.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `emit_teardown`

```elixir
@spec emit_teardown(IO.device(), Raxol.Terminal.InlineDriver.State.t()) ::
  Raxol.Terminal.InlineDriver.State.t()
```

Writes the canonical teardown byte sequence to `device` and restores the
OS tty (via `state.stty_module`, unless `state.stty_enabled?` is
`false`). Idempotent: a `state` with `torn_down?: true` is returned
unchanged, writing nothing (LC-N-DOUBLE).

# `handle_manager_cast`

# `isig_report`

```elixir
@spec isig_report(GenServer.server()) :: %{
  boot_confirmed?: boolean(),
  reasserts: non_neg_integer(),
  isig_off?: boolean()
}
```

The isig diagnosis for embedder-level reporting (the live demo's boot
POST line): which mechanism last held `-isig` and how often the
event-clocked guard has had to re-assert it.

  * `boot_confirmed?` -- the post-reader-arm verify loop saw `-isig`
    hold for 3 consecutive reads at claim time;
  * `reasserts` -- how many times the per-input-event guard found
    ISIG flipped back ON mid-session and re-asserted;
  * `isig_off?` -- the LIVE flags right now, read through the same
    injectable reader the guard uses.

# `probe_cursor`

```elixir
@spec probe_cursor(
  GenServer.server(),
  keyword()
) ::
  {:ok, Raxol.Terminal.InlineDriver.CursorReport.position()}
  | {:error, :timeout | :no_tty}
```

Asks the real terminal where the shell left the cursor: writes DSR-6
(`Sequences.cursor_position_request/0`, `CSI 6n`) to the driver's
device and reads the CPR reply (`CSI row ; col R`) off the driver's
own input stream. This is the substrate for GUEST-BOOT placement
(`Raxol.UI.Rendering.PaintAuthority.InlineAuthority`'s `:boot_cursor`
option): boot the surface exactly where the user's shell stopped,
instead of pushing a blank screen first.

Returns:

  * `{:ok, {row, col}}` — 1-based, straight from the terminal's reply.
  * `{:error, :no_tty}` — the driver was constructed with
    `tty?: false` (piped/CI). ZERO bytes are written: a device that
    cannot answer must not be probed at all.
  * `{:error, :timeout}` — no CPR arrived within `:budget_ms`
    (default 300ms). The budget is a
    LIVENESS bound on the device round-trip, not a rendering clock —
    see the module attribute note. On timeout every byte read while
    waiting has already been forwarded to the subscriber as ordinary
    input; nothing is dropped.

## The consumption contract (why this lives on the driver)

The CPR reply arrives interleaved with real keystrokes on the same
fd this driver already owns. Scanning happens INSIDE the driver
process (`Raxol.Terminal.InlineDriver.CursorReport`, the pure
scanner), before `InputParser` ever sees the bytes, because a CPR is
not representable as "just another event": `InputParser` decodes a
row-1 reply (`\e[1;<n>R`) as a modified F3 keypress — the classic
DSR/F3 wire collision — so letting it through would deliver a
phantom key event to the app. Keystrokes interleaved with (or split
around) the reply are forwarded to the subscriber in arrival order,
never dropped.

## Caller discipline

  * Probe BEFORE the first paint, at most once per claim: this writes
    bytes to the device, so it is strictly opt-in (nothing in
    `init_manager/1` ever emits it).
  * The result is only placement-honest while nothing else has
    written to the device between the reply and the boot that
    consumes it.
  * Residual: a reply that arrives AFTER the deadline lapses flows
    down the ordinary input path, where `InputParser` consumes it
    silently — except the row-1 form, which surfaces as a phantom
    modified-F3 keypress. Bound the exposure by probing when the
    terminal is idle (boot), which is the only supported call site.

# `start_link`

---

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