A per-connection terminal session backed by an in-memory writer.
Where ExRatatui.run/1 and ExRatatui.draw/2 are tied to the OS process'
real tty (raw mode, alternate screen, SIGWINCH), a Session is a
self-contained ratatui terminal whose output goes into a buffer and whose
input arrives as raw bytes from a transport you control. That makes it
the right primitive for serving a TUI over SSH, multiplexing several
TUIs in one BEAM node, or any context where the "terminal" lives somewhere
other than the local process.
Sessions touch nothing on the host tty, so they are safe to create and
drive concurrently from async: true tests, GenServers, or background
tasks.
Lifecycle
session = ExRatatui.Session.new(80, 24)
:ok = ExRatatui.Session.draw(session, [{paragraph, rect}])
bytes = ExRatatui.Session.take_output(session)
events = ExRatatui.Session.feed_input(session, "\e[A")
:ok = ExRatatui.Session.resize(session, 100, 30)
:ok = ExRatatui.Session.close(session)Each call is a thin wrapper over a Rust NIF, so a session is just an
Elixir struct holding a reference/0 to a ResourceArc. When the
struct is garbage collected the underlying resource is freed; close/1
is the deterministic version of the same cleanup and is idempotent.
Transport responsibilities
A session does not own a socket or do any I/O of its own. The transport glue is responsible for:
- Calling
draw/2whenever the app wants to repaint, then handing the result oftake_output/1to the wire. - Feeding inbound transport bytes through
feed_input/2and dispatching the returnedExRatatui.Event.t/0values to the app. - Calling
resize/3when the remote pty changes size. - Calling
close/1when the connection ends.
See ExRatatui.SSH for an OTP :ssh_server_channel-based transport.
Summary
Functions
Closes the session, dropping its rendering terminal.
Renders a list of {widget, rect} tuples into the session's writer.
Feeds raw transport bytes through the session's ANSI input parser.
Creates a new session at the given dimensions.
Resets the session's input parser, discarding any buffered partial escape sequence.
Resizes the session's viewport to width x height.
Returns the session's current {width, height}.
Drains the session's pending output bytes.
Types
@type t() :: %ExRatatui.Session{ref: reference()}
Functions
@spec close(t()) :: :ok
Closes the session, dropping its rendering terminal.
Idempotent — calling close/1 more than once is safe and always
returns :ok. After closing, draw/2 and resize/3 will return
{:error, _}, but feed_input/2 continues to work so a transport
can drain any trailing input bytes.
Examples
iex> session = ExRatatui.Session.new(20, 5)
iex> ExRatatui.Session.close(session)
:ok
iex> ExRatatui.Session.close(session)
:ok
@spec draw(t(), [{ExRatatui.widget(), ExRatatui.Layout.Rect.t()}]) :: :ok | {:error, term()}
Renders a list of {widget, rect} tuples into the session's writer.
Identical in shape to ExRatatui.draw/2, but the encoded ANSI bytes
accumulate in the session's in-memory buffer instead of going to the
real tty. Drain them with take_output/1.
Returns :ok on success, or {:error, reason} if the session has been
closed or a widget cannot be encoded.
Examples
iex> alias ExRatatui.Widgets.Paragraph
iex> alias ExRatatui.Layout.Rect
iex> session = ExRatatui.Session.new(20, 5)
iex> ExRatatui.Session.draw(session, [{%Paragraph{text: "hi"}, %Rect{x: 0, y: 0, width: 20, height: 5}}])
:ok
@spec feed_input(t(), binary()) :: [ExRatatui.Event.t()]
Feeds raw transport bytes through the session's ANSI input parser.
Returns a list of decoded ExRatatui.Event.t/0 structs, in the order
the parser produced them. Bytes that only partially form an escape
sequence stay buffered inside the session — feed the next chunk and
the parser will pick up where it left off. This is essential for SSH
and any other byte-stream transport that may chunk a single keystroke
across multiple packets.
Unlike draw/2, this still works after close/1 — the input parser
outlives the rendering terminal so a transport can drain trailing
input bytes after deciding to tear down rendering.
Examples
iex> session = ExRatatui.Session.new(20, 5)
iex> ExRatatui.Session.feed_input(session, "a")
[%ExRatatui.Event.Key{code: "a", modifiers: [], kind: "press"}]
@spec new(pos_integer(), pos_integer()) :: t()
Creates a new session at the given dimensions.
No OS terminal state is touched — the session writes into an in-memory
buffer that the transport drains via take_output/1. Both dimensions
must be at least 1.
Examples
iex> session = ExRatatui.Session.new(80, 24)
iex> ExRatatui.Session.size(session)
{80, 24}
iex> ExRatatui.Session.close(session)
:ok
@spec reset_parser(t()) :: :ok
Resets the session's input parser, discarding any buffered partial escape sequence.
Used by the SSH transport's Esc timeout: after a bare 0x1B with no
follow-up bytes, the VTE state machine is stuck in the Escape state.
Calling this drops that state so the next byte is parsed from Ground.
Examples
iex> session = ExRatatui.Session.new(20, 5)
iex> ExRatatui.Session.reset_parser(session)
:ok
@spec resize(t(), pos_integer(), pos_integer()) :: :ok | {:error, term()}
Resizes the session's viewport to width x height.
The next draw/2 will paint at the new dimensions and the buffer will
contain a clear-screen sequence the transport must forward. Returns
{:error, reason} if the session has been closed.
Examples
iex> session = ExRatatui.Session.new(20, 5)
iex> :ok = ExRatatui.Session.resize(session, 100, 30)
iex> ExRatatui.Session.size(session)
{100, 30}
@spec size(t()) :: {pos_integer(), pos_integer()}
Returns the session's current {width, height}.
Examples
iex> session = ExRatatui.Session.new(80, 24)
iex> ExRatatui.Session.size(session)
{80, 24}
Drains the session's pending output bytes.
Returns whatever ratatui has written into the in-memory buffer since the
last drain — typically the bytes the transport should ship to the
remote tty. The internal buffer is emptied as a side effect, so a
follow-up call with no intervening draw/2 returns <<>>.
Examples
iex> session = ExRatatui.Session.new(20, 5)
iex> :ok = ExRatatui.Session.draw(session, [])
iex> bytes = ExRatatui.Session.take_output(session)
iex> byte_size(bytes) > 0
true
iex> ExRatatui.Session.take_output(session)
""