External sources Linx.Tty draws on — kernel docs, prior-art tooling,
and the Erlang/Elixir conventions that shape the API.
A living doc — add to it as new sources inform a decision.
Kernel side
The syscalls and concepts Linx.Tty exposes:
termios(3)— the structure and functions (tcgetattr,tcsetattr,cfmakeraw, …) for terminal attribute control.tty(4)— the controlling terminal abstraction;/dev/ttysemantics.tty_ioctl(4)— theTIOC*ioctls (TIOCGWINSZ,TIOCSWINSZ,TIOCSCTTY, …) exposed for terminal control.pty(7)— pseudoterminal overview. Pair toLinx.Process'sstdio: :pty.ptmx(4)—/dev/ptmxand the multiplexor PTY creation path. (Used byLinx.Processoriginally; a future standalone Linx.Tty.openpt/0 would land here.)
Prior art
- conmon — the per-container
agent in podman/CRI-O. Its PTY relay (master fd in the agent,
byte-pumping over a control channel) is the same architectural shape
Linx.Process+Linx.Ttycompose into. - Go's
creack/pty— a widely used PTY library; useful reference for the Linux PTY allocation sequence and theTIOCSCTTY/setsidordering. - Rust's
nix::pty— another reference implementation; useful for cross-checking termios constant interpretations. - Python's
ptymodule and the venerablettymodule — minimal Python wrappers around the same syscalls; instructive for what the minimum useful PTY surface looks like. docker attach/kubectl exec -it— the user-experience target. Their implementations are the canonical "TTY-aware byte relay";Linx.Tty.attach/2follows the same shape: open local raw, pump bytes both ways, restore on exit.
NIF mechanics
- erl_nif manual — the
C API for ERL_NIF_INIT, term construction (
enif_make_atom,enif_alloc_binary, …), and the integer-conversion helpers.
Erlang TTY (the other one)
- Erlang's
ttydriver in ERTS — what the BEAM's group-leader process uses behind the scenes for shell IO.Linx.Ttydeliberately bypasses this by opening/dev/ttydirectly; the reference is included so future readers understand the layering choice. :prim_tty— the modern Erlang TTY primitives. Different abstraction, different goals (Erlang's interactive shell, notdocker attach); included for completeness.
I/O protocol and group leaders (attach(:group_leader, _))
The SSH-compatible attach mode pumps bytes through the caller's
group leader instead of /dev/tty. The 2026-05-27 SSH probe on a
Nerves rpi5 (probe script: docs/tty/probes/T6_ssh_probe.exs)
revealed that the GL over nerves_ssh is not ssh_cli but
kernel's :group gen_statem — the same line-editor module that
fronts local iex. ssh_cli is the transport below it; cooked-mode
line-discipline lives in :group. References:
- The Erlang I/O Protocol
—
{io_request, From, ReplyAs, Request}/{io_reply, ReplyAs, Reply}, the get_chars / put_chars / setopts request shapes, and the contract every group leader implements. :io— public-API wrappers (get_chars/3,setopts/2,columns/1,rows/1) the pump uses.kernel's:groupmodule — the gen_statem that is the group leader, both locally and over SSH. Three states::server(idle),:xterm(rich line editor with key_map / history; used whenecho=true),:dumb(byte-oriented; used whenecho=falseordumb=true). Reading the kernel-10.6.3 copy at~/.nerves/artifacts/nerves_system_rpi5-portable-2.0.3/staging/usr/lib/erlang/lib/kernel-10.6.3/src/group.erlwas what unlocked the:group_leadermechanism: the routing logic inserver/3(line 244) andget_chars_dumb/5(line 1152). No OTP-internals coupling is required —:io.setopts(echo: false)reaches the state field through documented public API.kernel's:user_drvmodule — the supervisor for the:group+ IO-driver pair, both locally (where it wraps:prim_tty) and over SSH (where it wraps an ssh-channel IO driver). The ancestor of:group; useful target for "disable the reader" surgery if:group's setopts knobs don't suffice.- Erlang
sshuser's guide —ssh_cli— the SSH shell-channel handler. Below the:group/:user_drvstack; produces the byte stream those processes line-edit. The original sketch wrongly placed line-discipline here. nerves_ssh— the Nerves wrapper around:ssh.daemonthat ships in the default Nerves config. Composesssh_subsystem_fwup(firmware updates), an iex subsystem (the shell we attach into), and authorized-key handling. The reason:sshd_sup(its own supervisor) and:ssh_sup(the OTP ssh app's top supervisor) both appear in a Nerves SSH iex's GL$ancestors.
Why /dev/tty and not fd 0
The reasoning lives in Linx.Tty under "Guiding principles."
The short version: BEAM's stdio is mediated by an Erlang group leader;
going through fd 0 would race the group leader and depend on its
buffering behaviour. /dev/tty is the controlling terminal
abstraction — every C program that wants direct terminal access (vi,
less, ssh, passwd, …) opens it for exactly this reason.
The trade-off shows up when /dev/tty is not the user's terminal —
SSH, :remsh, embedded device consoles where the BEAM is wired to
a serial port the user can't reach. The attach(:group_leader, _)
is the deliberate "fall back to the group leader anyway" mode for
those environments, with the corresponding loss of raw-mode fidelity.