ExRatatui.Session (ExRatatui v0.7.1)

Copy Markdown View Source

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:

  1. Calling draw/2 whenever the app wants to repaint, then handing the result of take_output/1 to the wire.
  2. Feeding inbound transport bytes through feed_input/2 and dispatching the returned ExRatatui.Event.t/0 values to the app.
  3. Calling resize/3 when the remote pty changes size.
  4. Calling close/1 when 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

t()

@type t() :: %ExRatatui.Session{ref: reference()}

Functions

close(session)

@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

draw(session, widgets)

@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

feed_input(session, bytes)

@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"}]

new(width, height)

@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

reset_parser(session)

@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

resize(session, width, height)

@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}

size(session)

@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}

take_output(session)

@spec take_output(t()) :: binary()

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)
""