# `Harlock.Terminal.Termios`
[🔗](https://github.com/thatsme/harlock/blob/v0.2.0/lib/harlock/terminal/termios.ex#L1)

POSIX termios access for `/dev/tty` via a small NIF.

Most Harlock apps don't touch this directly — the runtime owns one
control fd for the app's lifecycle (snapshot on init, restore on
terminate) and input is delivered via `arm_select/1` + `read_nonblock/2`.
The module is documented so you can drive termios from your own
code if you need raw mode outside the Harlock runtime.

See `c_src/README.md` for the design rationale — in particular, why
`tcgetattr` / `tcsetattr` / `ioctl(TIOCGWINSZ)` go through a NIF
instead of `:os.cmd("stty ...")` (the subshell loses the controlling
terminal) and why reads use `enif_select_read` + non-blocking
`read(2)` instead of `:file.read/2` (the latter doesn't deliver
bytes from a spawned process on macOS).

# `attrs`

```elixir
@opaque attrs()
```

Snapshot of termios attributes. Opaque to callers.

# `ref`

```elixir
@opaque ref()
```

Opaque resource — a /dev/tty fd.

# `arm_select`

```elixir
@spec arm_select(ref()) :: :ok | {:error, atom() | {atom(), term()}}
```

Register the fd with the BEAM IO poller for one read-ready notification.
When the fd has data available, BEAM sends `{:tty_ready, ref}` to the
process that called this function. Caller must re-arm after each read.

Only the process that called `open/0` may arm/read — others get
`{:error, :not_owner}`.

# `close`

```elixir
@spec close(ref()) :: :ok
```

Close the fd. Idempotent; the fd is also GC'd via NIF resource.

# `get`

```elixir
@spec get(ref()) :: {:ok, attrs()} | {:error, atom() | {atom(), term()}}
```

Read current termios attributes. The returned binary is opaque.

# `open`

```elixir
@spec open() :: {:ok, ref()} | {:error, atom() | {atom(), term()}}
```

Open /dev/tty for termios control. Returns `{:error, :no_tty}` in
environments without a controlling terminal (CI, piped stdin) so callers
can detect non-interactive contexts cleanly.

# `read_nonblock`

```elixir
@spec read_nonblock(ref(), pos_integer()) ::
  {:ok, binary()} | :wouldblock | :eof | {:error, atom() | {atom(), term()}}
```

Non-blocking `read(2)` of up to `max_bytes`. The fd is `O_NONBLOCK` so
this returns `:wouldblock` when no data is ready (caller should
`arm_select` and wait for the next `{:tty_ready, _}`). `:eof` means the
tty was closed (ssh disconnect, tmux kill, etc.) — surface this to the
app for clean shutdown.

# `set`

```elixir
@spec set(ref(), attrs()) :: :ok | {:error, atom() | {atom(), term()}}
```

Restore termios attributes from a prior `get/1` result.

# `set_raw`

```elixir
@spec set_raw(ref()) :: :ok | {:error, atom() | {atom(), term()}}
```

Put the terminal in raw mode (cfmakeraw + VMIN=1, VTIME=0).

# `winsize`

```elixir
@spec winsize(ref()) ::
  {:ok, pos_integer(), pos_integer()} | {:error, atom() | {atom(), term()}}
```

Current window size in cells, via TIOCGWINSZ.

---

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