Ascii.Piece behaviour (Ascii v0.3.0)

Copy Markdown View Source

The piece contract, upstream's in Elixir.

Upstream, a piece's default function takes the options, precomputes, and returns a closure frame(t, env). Here init/1 is the outer function: it receives every option (the defaults merged with what was asked for) and returns a state. frame/3 is the closure: given the state, the play time t in seconds and the Ascii.Env, it returns the picture as iodata, the colours, and the next state.

The picture is rows lines of exactly cols characters joined by \n. Colours are nil unless the piece has a palette and env.color? is set; then they are a binary of cols * rows palette indices, row by row.

State is threaded because upstream closures keep state between frames: a simulation that steps by how far t moved, a cache of the last minute drawn. A piece behaves exactly as its upstream closure does when called with the same sequence of t, including t going backwards.

Example

defmodule MySpinner do
  @behaviour Ascii.Piece

  @meta %Ascii.Meta{
    name: "spinner",
    category: :ui,
    note: "a line turning while something loads",
    cols: 10,
    rows: 1,
    fps: 6
  }

  @impl true
  def meta, do: @meta

  @impl true
  def init(_options), do: nil

  @impl true
  def frame(state, t, _env) do
    {[Enum.at(~w(| / - \\), rem(floor(t * 3), 4)), " loading "], nil, state}
  end
end

Summary

Callbacks

What the piece shows, in words, for a screen reader: read off the options, the data a component draws ("load: 72%", "boot log: 9 ok, 1 warning"). nil when the options hold nothing to tell (the piece plays its own script), for Ascii.describe/1 to say what the piece is.

The picture at t seconds, its colours, and the next state.

The options after an event, for a piece that can be used: a key or a click (see Ascii.event/0) moving a cursor, opening a folder, ticking a box. What it changes is in the options it returns, so an app reads what was chosen from them. :ignore for an event it has no use for.

Precomputes from every option, merged with the defaults.

The piece's Ascii.Meta.

The parts of a frame in one ink, for a piece whose meta lists parts: each row as runs of {name, text} making up the row (see Ascii.Parts). Given the frame as drawn, and the options it was drawn with; called only when parts are to be coloured.

The frame's size for these options, {cols, rows}, for a piece whose size follows its options (a width to fit a terminal, a row a series). Without it, every frame is the meta's size.

The state for new options while playing (see Ascii.put_options/2), keeping what it can of state: a needle swinging to a new reading, a scroll carrying on. Without it, init/1 starts afresh.

Callbacks

describe(options)

(optional)
@callback describe(options :: map()) :: String.t() | nil

What the piece shows, in words, for a screen reader: read off the options, the data a component draws ("load: 72%", "boot log: 9 ok, 1 warning"). nil when the options hold nothing to tell (the piece plays its own script), for Ascii.describe/1 to say what the piece is.

frame(state, t, env)

@callback frame(state :: term(), t :: float(), env :: Ascii.Env.t()) ::
  {iodata(), colors :: binary() | nil, state :: term()}

The picture at t seconds, its colours, and the next state.

handle_event(event, options)

(optional)
@callback handle_event(event :: Ascii.event(), options :: map()) ::
  {:ok, options :: map()} | :ignore

The options after an event, for a piece that can be used: a key or a click (see Ascii.event/0) moving a cursor, opening a folder, ticking a box. What it changes is in the options it returns, so an app reads what was chosen from them. :ignore for an event it has no use for.

init(options)

@callback init(options :: map()) :: state :: term()

Precomputes from every option, merged with the defaults.

meta()

@callback meta() :: Ascii.Meta.t()

The piece's Ascii.Meta.

parts(frame, options)

(optional)
@callback parts(frame :: Ascii.Frame.t(), options :: map()) :: Ascii.Parts.t()

The parts of a frame in one ink, for a piece whose meta lists parts: each row as runs of {name, text} making up the row (see Ascii.Parts). Given the frame as drawn, and the options it was drawn with; called only when parts are to be coloured.

size(options)

(optional)
@callback size(options :: map()) :: {cols :: pos_integer(), rows :: pos_integer()}

The frame's size for these options, {cols, rows}, for a piece whose size follows its options (a width to fit a terminal, a row a series). Without it, every frame is the meta's size.

update(state, options)

(optional)
@callback update(state :: term(), options :: map()) :: state :: term()

The state for new options while playing (see Ascii.put_options/2), keeping what it can of state: a needle swinging to a new reading, a scroll carrying on. Without it, init/1 starts afresh.