RasterExRatatui.Session (RasterExRatatui v0.4.0)

Copy Markdown View Source

An ExRatatui app running on a raster, driven from the caller's own process.

A session is what RasterExRatatui.Surface wraps: the ExRatatui.CellSession, the app server, the writer that delivers its renders, and the RasterExRatatui.Raster that turns them into pixels. It has no process of its own. A consumer that already owns a process in charge of a panel (a screen manager, a device loop) starts a session in it, folds the session's messages with handle/2, and writes the pixels it gets back however the panel wants.

defmodule Badge.Screen do
  use GenServer

  alias RasterExRatatui.{Raster, Session}

  def init(app) do
    Process.flag(:trap_exit, true)
    raster = Raster.new(size: {400, 300}, format: RasterExRatatui.PixelFormat.Mono)
    {:ok, session} = Session.start(raster, app: app, keep_frame: true)
    {:ok, session}
  end

  def handle_info(msg, session) do
    case Session.handle(session, msg) do
      {:render, _patches, session} ->
        Badge.Display.show(Session.frame(session))
        {:noreply, session}

      {:exit, reason, session} ->
        Badge.Display.show(crash_frame(reason))
        {:noreply, session}

      :unknown ->
        {:noreply, session}
    end
  end

  def terminate(_reason, session), do: Session.stop(session)
end

Lifecycle

  1. start/2 creates the cell session for the raster's grid, starts the app server linked to the caller, and returns the session. The caller must trap exits: the app's exit then arrives as an {:EXIT, pid, reason} message that handle/2 turns into {:exit, reason, session} instead of taking the caller down.
  2. Every render of the app arrives in the caller's mailbox as {RasterExRatatui.Session, ref, diff}. handle/2 takes that message, every other render of the same session already waiting behind it, folds them into the raster as one, and returns the patches. Renders are never queued up: a slow panel shows fewer, later frames, never every frame later and later.
  3. await/2 waits for the first render (or the app's exit) with a timeout, for consumers that want the first frame before they show anything.
  4. stop/1 stops the app server, waiting up to shutdown_timeout: for its terminate/2 before killing it, and closes the cell session.

When the app exits, its server closes the cell session on the way out; a consumer that wants the app back starts a new session on the same raster (start(raster(session), opts)) and the new app's first render repaints everything.

Options

start/2 takes:

  • :app (required) — the ExRatatui.App module to run
  • :app_opts — keyword list passed to the app's mount/1 (or reducer init/1), default []. The session adds surface: to it, a map describing the panel: :size (the physical panel in pixels), :cell_size (the effective cell in pixels), :grid_size (cells), :format, :scale, and :rotate (see RasterExRatatui.Raster's rotation section)
  • :keep_frame — keep a full frame of the panel up to date (default false). frame/1 then returns it at no cost, and render/2 applies each patch to it, for panels that only take whole frames
  • :shutdown_timeout — milliseconds stop/1 waits for the app server before killing it (default 4_000)

The pieces stay reachable: raster/1 is the current raster (RasterExRatatui.Raster.frame/1 renders what the panel shows), server/1 the app server's pid, and drain/2 and render/2 are the two halves of handle/2, for a consumer that gathers renders and rasterises them on its own schedule.

Summary

Types

A payload the session folds: a cell diff from the app, or a snapshot.

t()

Functions

Waits up to timeout milliseconds for the next render of the session's app, or its exit, and handles it.

The first half of handle/2: classifies a message without rasterising anything.

The whole panel as one row-major buffer of packed pixels: the kept frame with keep_frame: true, rendered from the raster otherwise.

The grid the app renders to, {cols, rows}.

Handles a message from the caller's mailbox.

The session's current RasterExRatatui.Raster.

The second half of handle/2: folds payloads (diffs or snapshots, in order) into the raster and returns the patches that repaint what changed.

Changes the panel size, in pixels. The raster and the cell session are rebuilt at the new grid, the app receives an ExRatatui.Event.Resize, and its next render repaints the whole panel.

Forwards an ExRatatui.Event (a %Key{}, a %Mouse{}, …) to the app, like input from a terminal. Dropped once the app has exited.

The app server's pid, or nil once it has exited.

Starts an app on raster from the calling process (see the moduledoc for the options).

Stops the app server, waiting up to the session's shutdown_timeout: for its terminate/2 before killing it, and closes the cell session. Leaves no {:EXIT, …} from the app in the caller's mailbox, even when the app died on its own just before. Safe after {:exit, _, _}, when only the cell session is left to close.

Types

payload()

@type payload() :: RasterExRatatui.Raster.payload()

A payload the session folds: a cell diff from the app, or a snapshot.

t()

@type t() :: %RasterExRatatui.Session{
  app: module(),
  cell_session: ExRatatui.CellSession.t(),
  frame: binary() | nil,
  raster: RasterExRatatui.Raster.t(),
  ref: reference(),
  server: pid() | nil,
  shutdown_timeout: non_neg_integer()
}

Functions

await(session, timeout \\ 5000)

@spec await(t(), timeout()) ::
  {:render, [RasterExRatatui.Patch.t()], t()}
  | {:exit, term(), t()}
  | {:timeout, t()}

Waits up to timeout milliseconds for the next render of the session's app, or its exit, and handles it.

Returns what handle/2 would, or {:timeout, session}. Other messages stay in the mailbox. Typical right after start/2, for the first frame.

drain(session, arg2)

@spec drain(t(), term()) ::
  {:diffs, [ExRatatui.CellSession.Diff.t()], t()}
  | {:exit, term(), t()}
  | :unknown

The first half of handle/2: classifies a message without rasterising anything.

Returns {:diffs, diffs, session} for a render, with every render of the session still waiting in the mailbox gathered behind it, oldest first (renders from before a resize/2 are dropped, since the next one repaints the new grid in full); {:exit, reason, session} for the app server's exit; :unknown otherwise. render/2 takes the diffs whenever the consumer is ready to draw.

frame(session)

@spec frame(t()) :: binary()

The whole panel as one row-major buffer of packed pixels: the kept frame with keep_frame: true, rendered from the raster otherwise.

grid_size(session)

@spec grid_size(t()) :: RasterExRatatui.Raster.size()

The grid the app renders to, {cols, rows}.

handle(session, msg)

@spec handle(t(), term()) ::
  {:render, [RasterExRatatui.Patch.t()], t()} | {:exit, term(), t()} | :unknown

Handles a message from the caller's mailbox.

Returns:

  • {:render, patches, session} for a render of this session's app: the RasterExRatatui.Patch list that repaints what changed (empty when nothing did), after folding every render of the session still waiting in the mailbox into the same call. With keep_frame: true, frame/1 is up to date as well
  • {:exit, reason, session} when the app server exited; the session no longer has a server, and its cell session is closed
  • :unknown for any other message

Examples

def handle_info(msg, %{session: session} = state) do
  case RasterExRatatui.Session.handle(session, msg) do
    {:render, patches, session} -> {:noreply, %{state | session: write(patches, session)}}
    {:exit, reason, session} -> {:stop, reason, %{state | session: session}}
    :unknown -> {:noreply, state}
  end
end

raster(session)

@spec raster(t()) :: RasterExRatatui.Raster.t()

The session's current RasterExRatatui.Raster.

render(session, payloads)

@spec render(t(), [payload()]) :: {[RasterExRatatui.Patch.t()], t()}

The second half of handle/2: folds payloads (diffs or snapshots, in order) into the raster and returns the patches that repaint what changed.

With keep_frame: true, the patches are also applied to the kept frame.

resize(session, size)

@spec resize(t(), RasterExRatatui.Raster.size()) :: t()

Changes the panel size, in pixels. The raster and the cell session are rebuilt at the new grid, the app receives an ExRatatui.Event.Resize, and its next render repaints the whole panel.

send_event(session, event)

@spec send_event(t(), ExRatatui.Event.t()) :: :ok

Forwards an ExRatatui.Event (a %Key{}, a %Mouse{}, …) to the app, like input from a terminal. Dropped once the app has exited.

server(session)

@spec server(t()) :: pid() | nil

The app server's pid, or nil once it has exited.

start(raster, opts)

@spec start(RasterExRatatui.Raster.t(), keyword()) :: {:ok, t()} | {:error, term()}

Starts an app on raster from the calling process (see the moduledoc for the options).

Returns {:ok, session}, or {:error, reason} when the app fails to mount, in which case nothing is left running. Raises ArgumentError without an :app.

The app server is linked to the caller, which must trap exits before calling this.

stop(session)

@spec stop(t()) :: :ok

Stops the app server, waiting up to the session's shutdown_timeout: for its terminate/2 before killing it, and closes the cell session. Leaves no {:EXIT, …} from the app in the caller's mailbox, even when the app died on its own just before. Safe after {:exit, _, _}, when only the cell session is left to close.