Ascii (Ascii v0.3.0)

Copy Markdown View Source

ASCII-art UI components and animated art for Elixir, ported from ascii.rest by @bas3line.

The UI, data and type pieces are components: a file tree, a boot log, an htop-style panel, gauges, sparklines, progress bars, a form, a terminal, a calendar, a departures board. They draw your data, given as options and taken up while they play (put_options/2), in your colours (:ink, :parts); the ones that can be used take keys and clicks (handle_event/2); and each tells what it shows, for screen readers (describe/1). The rest are art: scenes, logos, distros, 3D shapes and simulations. Ascii.Compose puts frames together into one.

Every piece is a function of time: give it t in seconds and it draws a frame, exactly rows lines of cols characters, and for coloured pieces a palette index for every cell. Frames are plain data (Ascii.Frame), so they go wherever Elixir does: a LiveView (Ascii.Phoenix), a terminal UI (Ascii.ExRatatui), any terminal (Ascii.Render.ANSI, Ascii.Player), or a framebuffer on a device.

iex> Ascii.pieces(category: :ui) |> Enum.member?("file-tree")
true

Ascii.render!("gauge", 2.0, options: %{label: "cpu", value: 72}) |> IO.puts()
Ascii.render!("donut", 1.5) |> IO.puts()

{:ok, anim} = Ascii.new("night-coast", fps: 10)
{frame, anim} = Ascii.frame(anim, 0.1)

Anywhere a piece is expected, pass its slug (the upstream file name, such as "night-coast") or its module (Ascii.Pieces.NightCoast).

Summary

Types

Why a piece could not be played.

Input for a piece that can be used (Ascii.Piece.handle_event/2).

A piece: its slug or its module.

Functions

What the animation shows, in words, for a screen reader: told from its options by a piece that can (Ascii.Piece.describe/1), such as a gauge's reading or a boot log's steps; else the piece's name and note. The Phoenix players and components label their picture with it.

The frame at t seconds of play time, and the animation to draw the next one from. Pieces with state (simulations, mostly) step by how far t moved since the last frame, so play t forward.

An event for a piece that can be used: a file tree's cursor moving or a folder opening, a form's box ticked, a date picked. The piece takes up what it changes as new options (as put_options/2 would), so what was chosen is in anim.options (a tree's cursor, a form's values). :ignore if the piece has no use for the event, or takes none at all.

One cycle of a piece that closes a loop (see Ascii.Meta): its frames from t = 0, evenly spaced through the period, and the rate to show them at, the animation's own or as near as a whole number of frames allows. Played over and over, they are the piece, so a player can draw a loop once and repeat it rather than draw every frame anew.

A piece's Ascii.Meta.

Like meta/1, raising for an unknown piece.

Starts playing a piece.

Like new/2, raising on an error.

The part of the frame at column x, row y (see Ascii.Parts), and the text of its run there: {"file.ex", "ascii.ex"} for a file in a tree, {"status.warn", "[ WARN ]"} in a boot log, {nil, text} for cells in no part. nil outside the frame, or for a piece that does not know its parts. For a click: which file, which bar, which day.

Every piece's slug, sorted. category: keeps one category.

New options for a playing animation, for live data in a dashboard or a terminal UI: a reading for a gauge, the latest samples for a sparkline.

Like put_options/2, raising on an error.

One frame of a piece as text, from a fresh start. Takes the options of new/2, and :now.

Like render/3, raising on an error.

The module for a piece, given its slug or a module that implements Ascii.Piece (registered or not).

Frame 0 of a piece: what a page shows before it plays, and what a reader who prefers reduced motion sees. Takes the options of new/2, and :now.

The piece's frames as a lazy stream, one every 1/fps seconds from 0. A still (fps 0) gives one frame. Takes the options of new/2.

Like stream/2, raising on an error.

The upstream commit this port follows.

Types

error()

@type error() :: {:error, :unknown_piece} | {:error, {:invalid_option, term()}}

Why a piece could not be played.

event()

@type event() :: {:key, String.t()} | {:click, non_neg_integer(), non_neg_integer()}

Input for a piece that can be used (Ascii.Piece.handle_event/2).

  • {:key, key} - a key, by ExRatatui's names: "up", "down", "left", "right", "enter", "esc", "tab", "back_tab" (shift and tab), "backspace", "delete", "home", "end", "page_up", "page_down", or the character typed, " " for space. Ascii.ExRatatui.event/2 and the Phoenix hook give keys by these names.
  • {:click, x, y} - a click on the cell in column x, row y of the frame, from 0.

piece()

@type piece() :: String.t() | module()

A piece: its slug or its module.

Functions

describe(animation)

@spec describe(anim :: Ascii.Animation.t()) :: String.t()

What the animation shows, in words, for a screen reader: told from its options by a piece that can (Ascii.Piece.describe/1), such as a gauge's reading or a boot log's steps; else the piece's name and note. The Phoenix players and components label their picture with it.

iex> Ascii.new!("donut") |> Ascii.describe()
"donut: a lit torus turning on two axes, after donut.c"

frame(anim, t, opts \\ [])

@spec frame(
  anim :: Ascii.Animation.t(),
  t :: number(),
  opts :: [{:now, DateTime.t()}]
) ::
  {Ascii.Frame.t(), Ascii.Animation.t()}

The frame at t seconds of play time, and the animation to draw the next one from. Pieces with state (simulations, mostly) step by how far t moved since the last frame, so play t forward.

opts may give :now, a DateTime for pieces that read the clock.

handle_event(anim, event)

@spec handle_event(anim :: Ascii.Animation.t(), event :: event()) ::
  {:ok, Ascii.Animation.t()} | :ignore

An event for a piece that can be used: a file tree's cursor moving or a folder opening, a form's box ticked, a date picked. The piece takes up what it changes as new options (as put_options/2 would), so what was chosen is in anim.options (a tree's cursor, a form's values). :ignore if the piece has no use for the event, or takes none at all.

{:ok, anim} = Ascii.handle_event(anim, {:key, "down"})
anim.options.cursor

loop(anim)

@spec loop(anim :: Ascii.Animation.t()) ::
  {:ok, [Ascii.Frame.t()], fps :: number()} | :error

One cycle of a piece that closes a loop (see Ascii.Meta): its frames from t = 0, evenly spaced through the period, and the rate to show them at, the animation's own or as near as a whole number of frames allows. Played over and over, they are the piece, so a player can draw a loop once and repeat it rather than draw every frame anew.

The period is measured with the piece's default options and frame rate. With others, which can change it (a longer text takes longer to scroll), the frames of a second cycle are drawn too, and the loop stands only if they match the first. :error means the piece is to be drawn frame by frame.

iex> {:ok, frames, fps} = Ascii.new!("elixir") |> Ascii.loop()
iex> {length(frames), fps}
{150, 30}

meta(piece)

@spec meta(piece :: piece()) :: {:ok, Ascii.Meta.t()} | {:error, :unknown_piece}

A piece's Ascii.Meta.

meta!(piece)

@spec meta!(piece :: piece()) :: Ascii.Meta.t()

Like meta/1, raising for an unknown piece.

new(piece, opts \\ [])

@spec new(piece :: piece(), opts :: keyword()) :: {:ok, Ascii.Animation.t()} | error()

Starts playing a piece.

Options

  • :options - the piece's own options, a map or keyword list, checked against its defaults (see Ascii.Options).
  • :fps - frames a second for stream/2 and players, 0 to 60. Defaults to the piece's own.
  • :paper - the picture is dark ink on a light ground, so shaded pieces flip their ramps. Defaults to false. A coloured piece drawn in colour over its own ground takes this from the ground instead, as upstream.
  • :color - draw coloured pieces in colour (true, the default) or as text in one ink.
  • :ink - the colour of a piece drawn in one ink, where it is shown in colour: "#rrggbb", or a CSS variable ("var(--accent, #7fdbca)") for a page, or several colours (a list, or comma-separated) for a gradient across the picture. Without it, the renderer's own ink. A coloured piece drawn in colour keeps its palette. See Ascii.Ink.
  • :gradient - which way several inks run: :horizontal (the default), :vertical or :diagonal.
  • :parts - colours for the parts of a piece in one ink that knows them (its meta's parts): a map of part names to colours, or true for the ones it suggests (for a light page, with :paper). Over the :ink, or the renderer's own. See Ascii.Parts.

new!(piece, opts \\ [])

@spec new!(piece :: piece(), opts :: keyword()) :: Ascii.Animation.t()

Like new/2, raising on an error.

part_at(animation, frame, x, y)

@spec part_at(
  anim :: Ascii.Animation.t(),
  frame :: Ascii.Frame.t(),
  x :: integer(),
  y :: integer()
) ::
  {Ascii.Parts.name() | nil, String.t()} | nil

The part of the frame at column x, row y (see Ascii.Parts), and the text of its run there: {"file.ex", "ascii.ex"} for a file in a tree, {"status.warn", "[ WARN ]"} in a boot log, {nil, text} for cells in no part. nil outside the frame, or for a piece that does not know its parts. For a click: which file, which bar, which day.

pieces(opts \\ [])

@spec pieces(opts :: [{:category, Ascii.Meta.category()}]) :: [String.t()]

Every piece's slug, sorted. category: keeps one category.

iex> "night-coast" in Ascii.pieces(category: :scenes)
true

put_options(anim, options)

@spec put_options(anim :: Ascii.Animation.t(), options :: map() | keyword()) ::
  {:ok, Ascii.Animation.t()} | error()

New options for a playing animation, for live data in a dashboard or a terminal UI: a reading for a gauge, the latest samples for a sparkline.

options are checked as new/2 checks them and merged over the animation's own, so only what changes need be given. A piece that can carries its state over (a gauge's needle swings from where it was to the new reading); any other starts afresh with the new options. A piece whose size follows its options takes its new size.

anim = Ascii.new!("gauge", options: %{value: 40})
anim = Ascii.put_options!(anim, value: 72)

put_options!(anim, options)

@spec put_options!(anim :: Ascii.Animation.t(), options :: map() | keyword()) ::
  Ascii.Animation.t()

Like put_options/2, raising on an error.

render(piece, t \\ 0.0, opts \\ [])

@spec render(piece :: piece(), t :: number(), opts :: keyword()) ::
  {:ok, String.t()} | error()

One frame of a piece as text, from a fresh start. Takes the options of new/2, and :now.

iex> {:ok, text} = Ascii.render("spinners")
iex> text |> String.split("\n") |> length()
13

render!(piece, t \\ 0.0, opts \\ [])

@spec render!(piece :: piece(), t :: number(), opts :: keyword()) :: String.t()

Like render/3, raising on an error.

resolve(slug)

@spec resolve(piece :: piece()) :: {:ok, module()} | {:error, :unknown_piece}

The module for a piece, given its slug or a module that implements Ascii.Piece (registered or not).

still(piece, opts \\ [])

@spec still(piece :: piece(), opts :: keyword()) :: {:ok, Ascii.Frame.t()} | error()

Frame 0 of a piece: what a page shows before it plays, and what a reader who prefers reduced motion sees. Takes the options of new/2, and :now.

With the piece's default options it is drawn once and kept for the life of the VM, so later calls have it at once: a scene takes up to a second to set up, and a page that shows stills, or a player's first render, pays for that once rather than on every request. A piece that reads the clock is drawn every time, as are other options. An :ink and :parts are laid over the kept frame, so every ink shares it.

iex> {:ok, frame} = Ascii.still("elixir")
iex> {frame.cols, frame.rows, frame.t}
{38, 28, 0.0}

stream(piece, opts \\ [])

@spec stream(piece :: piece(), opts :: keyword()) ::
  {:ok, Enumerable.t(Ascii.Frame.t())} | error()

The piece's frames as a lazy stream, one every 1/fps seconds from 0. A still (fps 0) gives one frame. Takes the options of new/2.

iex> {:ok, frames} = Ascii.stream("spinners")
iex> frames |> Enum.take(3) |> Enum.map(& &1.t)
[0.0, 0.08333333333333333, 0.16666666666666666]

stream!(piece, opts \\ [])

@spec stream!(piece :: piece(), opts :: keyword()) :: Enumerable.t(Ascii.Frame.t())

Like stream/2, raising on an error.

upstream_commit()

@spec upstream_commit() :: String.t()

The upstream commit this port follows.