# `Ascii`
[🔗](https://github.com/houllette/ascii_art/blob/main/lib/ascii.ex#L1)

ASCII-art UI components and animated art for Elixir, ported from
[ascii.rest](https://ascii.rest) by [@bas3line](https://github.com/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`).

# `error`

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

Why a piece could not be played.

# `event`

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

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

  * `{:key, key}` - a key, by [ExRatatui](https://hexdocs.pm/ex_ratatui)'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`

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

A piece: its slug or its module.

# `describe`

```elixir
@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 (`c: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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

A piece's `Ascii.Meta`.

# `meta!`

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

Like `meta/1`, raising for an unknown piece.

# `new`

```elixir
@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!`

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

Like `new/2`, raising on an error.

# `part_at`

```elixir
@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`

```elixir
@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`

```elixir
@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!`

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

Like `put_options/2`, raising on an error.

# `render`

```elixir
@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!`

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

Like `render/3`, raising on an error.

# `resolve`

```elixir
@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`

```elixir
@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`

```elixir
@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!`

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

Like `stream/2`, raising on an error.

# `upstream_commit`

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

The upstream commit this port follows.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
