Ascii.Parts (Ascii v0.3.0)

Copy Markdown View Source

The parts of a piece, coloured one by one: a file tree's folders, files and branches, a boot log's OK and WARN, a chart's bars and axes.

A piece in one ink that knows its parts lists them in its meta (Ascii.Meta, parts) and labels each frame's cells with them (the optional Ascii.Piece.parts/2). Give colours for some of them with the :parts option, and those cells are drawn in them:

Ascii.new("file-tree", parts: %{"folder" => "#58a6ff", "file.md" => "#d29922"})
Ascii.new("boot-log", parts: %{"status.ok" => "#3fb950", "status.warn" => "#d29922"})
Ascii.new("boot-log", parts: true)

true takes the colours the piece suggests for its parts, for a dark page, or with paper: true for a light one (suggested/2). A part it suggests none for takes its parent's (a form's frame.input the frame's), or the ink if it has no parent with one. To change a few of them, merge yours over the meta's:

suggested = Ascii.Parts.suggested(Ascii.meta!("file-tree"))
Ascii.new("file-tree", parts: Map.merge(suggested, %{"folder" => "#ffffff"}))

A colour can be a CSS variable, so parts follow a page's theme, light or dark: %{"status.ok" => "var(--color-success, #3fb950)"} (see Ascii.Ink).

Names

A part's name is lowercase words joined by dots, the most general first, as in tree-sitter's highlight names: status, status.ok, file.ts. A colour for a name colours every part under it unless that part has its own: file colours every file, file.ts only TypeScript files. A name given nil takes the base ink, so %{"file" => "#c9d1d9", "file.md" => nil} leaves Markdown files out.

Each piece's docs list its parts. The first word of every name you give must be one of them; the rest can be anything (a file's extension, a flight's remark), and simply never matches if the piece never uses it.

Over the ink

Parts are laid over the :ink: cells in a part with a colour take it, all others the ink (one colour, or a gradient), or the renderer's own ink if there is none. In a frame, that last is a nil entry in the palette (see Ascii.Frame). The picture itself is never changed: parts are read off it after the piece has drawn it.

Labelling a frame

For a piece of your own, parts/2 returns each row of the frame as runs of {name, text}, name being nil for cells in no part. The runs' text must make up the row exactly. The helpers here build runs from a line.

Summary

Types

Colours for parts by name, nil for the base ink, as cast/2 makes them.

A part's name: lowercase words joined by dots, as status.ok.

Some of a row's text, and the part it is in (or nil).

t()

A frame's rows, each as runs of text that make up the row.

Functions

A line labelled by a regex that matches all of it: each capture is in the part named at its place in names (nil for none), and text no capture holds in no part. Captures that matched nothing are left out. nil if the regex does not match.

Colours for a piece's parts, from outside: a map or keyword list of names (atoms or strings) to colours (#rrggbb or a CSS variable, as for :ink, or nil), true for the colours the piece suggests (suggested/2), or nil for none. With paper: true, the suggestions are those for a light page. Never creates an atom.

A line labelled a cell at a time, by a function of each cell's code point (an integer, as ?█) and its column from 0: for pieces whose parts go by where a cell is as well as what it holds. Neighbours in one part are one run.

A line labelled a character at a time, by a function of each character returning its part's name (or nil). Neighbours in one part are one run.

A line labelled by the first of patterns, each {regex, names} as for captures/3, that matches all of it; in no part if none does.

A whole line in one part, or (nil) in none.

A line labelled where a regex matches it: each match in the part name gives (a name, or a function of the matched text returning one), and the rest in no part.

Runs with neighbours in one part made one, and empty runs dropped. Runs can be built a piece at a time, then tidied with this.

The name a colour is looked up by next, if name has none: the name without its last word, or nil for a name of one word.

Runs labelled by fun where they are in no part yet, its runs taking their place: for labelling in passes, the plain text left by one pass given to the next.

The run of a row (as parts/2 gives it) that holds column x, from 0: its part and its text. nil past the row's end.

The colours a piece suggests for its parts (its meta's parts, those it gives one), as parts: true takes them: for a dark page, or with paper: true for a light one. On paper, the shared colours become their light-page pairs, and any other is darkened until it reads on white.

Types

colors()

@type colors() :: %{optional(name()) => String.t() | nil}

Colours for parts by name, nil for the base ink, as cast/2 makes them.

name()

@type name() :: String.t()

A part's name: lowercase words joined by dots, as status.ok.

run()

@type run() :: {name() | nil, String.t()}

Some of a row's text, and the part it is in (or nil).

t()

@type t() :: [[run()]]

A frame's rows, each as runs of text that make up the row.

Functions

captures(line, regex, names)

@spec captures(line :: String.t(), regex :: Regex.t(), names :: [name() | nil]) ::
  [run()] | nil

A line labelled by a regex that matches all of it: each capture is in the part named at its place in names (nil for none), and text no capture holds in no part. Captures that matched nothing are left out. nil if the regex does not match.

iex> Ascii.Parts.captures("  [ OK ] done", ~r/^ *(\[ OK \]) (.*)$/, ["status.ok", "message"])
[{nil, "  "}, {"status.ok", "[ OK ]"}, {nil, " "}, {"message", "done"}]

cast(parts, meta, opts \\ [])

@spec cast(parts :: term(), meta :: Ascii.Meta.t(), opts :: [{:paper, boolean()}]) ::
  {:ok, colors()} | Ascii.Options.error()

Colours for a piece's parts, from outside: a map or keyword list of names (atoms or strings) to colours (#rrggbb or a CSS variable, as for :ink, or nil), true for the colours the piece suggests (suggested/2), or nil for none. With paper: true, the suggestions are those for a light page. Never creates an atom.

cells(line, fun)

@spec cells(line :: String.t(), fun :: (char(), non_neg_integer() -> name() | nil)) ::
  [run()]

A line labelled a cell at a time, by a function of each cell's code point (an integer, as ?█) and its column from 0: for pieces whose parts go by where a cell is as well as what it holds. Neighbours in one part are one run.

iex> Ascii.Parts.cells("▁▁▁▁", fn ?▁, column -> if column < 2, do: "low", else: "high" end)
[{"low", "▁▁"}, {"high", "▁▁"}]

chars(line, fun)

@spec chars(line :: String.t(), fun :: (String.t() -> name() | nil)) :: [run()]

A line labelled a character at a time, by a function of each character returning its part's name (or nil). Neighbours in one part are one run.

iex> Ascii.Parts.chars("██░░", &if(&1 == "█", do: "done", else: "left"))
[{"done", "██"}, {"left", "░░"}]

first(line, patterns)

@spec first(line :: String.t(), patterns :: [{Regex.t(), [name() | nil]}]) :: [run()]

A line labelled by the first of patterns, each {regex, names} as for captures/3, that matches all of it; in no part if none does.

iex> Ascii.Parts.first("[WARN] low", [{~r/^(\[OK\]) (.*)$/, ["ok", "text"]}, {~r/^(\[WARN\]) (.*)$/, ["warn", "text"]}])
[{"warn", "[WARN]"}, {nil, " "}, {"text", "low"}]

line(line, name)

@spec line(line :: String.t(), name :: name() | nil) :: [run()]

A whole line in one part, or (nil) in none.

matches(line, regex, name)

@spec matches(
  line :: String.t(),
  regex :: Regex.t(),
  name :: name() | (String.t() -> name() | nil)
) ::
  [run()]

A line labelled where a regex matches it: each match in the part name gives (a name, or a function of the matched text returning one), and the rest in no part.

iex> Ascii.Parts.matches("a1 b22", ~r/\d+/, "digit")
[{nil, "a"}, {"digit", "1"}, {nil, " b"}, {"digit", "22"}]

merge(runs)

@spec merge(runs :: [run()]) :: [run()]

Runs with neighbours in one part made one, and empty runs dropped. Runs can be built a piece at a time, then tidied with this.

parent(name)

@spec parent(name :: name()) :: name() | nil

The name a colour is looked up by next, if name has none: the name without its last word, or nil for a name of one word.

iex> Ascii.Parts.parent("status.ok")
"status"
iex> Ascii.Parts.parent("status")
nil

refine(runs, fun)

@spec refine(runs :: [run()], fun :: (String.t() -> [run()])) :: [run()]

Runs labelled by fun where they are in no part yet, its runs taking their place: for labelling in passes, the plain text left by one pass given to the next.

run_at(arg1, x)

@spec run_at(runs :: [run()] | nil, x :: non_neg_integer()) :: run() | nil

The run of a row (as parts/2 gives it) that holds column x, from 0: its part and its text. nil past the row's end.

iex> Ascii.Parts.run_at([{nil, "  "}, {"status.ok", "[ OK ]"}, {nil, " up"}], 4)
{"status.ok", "[ OK ]"}

suggested(meta, opts \\ [])

@spec suggested(meta :: Ascii.Meta.t(), opts :: [{:paper, boolean()}]) :: colors()

The colours a piece suggests for its parts (its meta's parts, those it gives one), as parts: true takes them: for a dark page, or with paper: true for a light one. On paper, the shared colours become their light-page pairs, and any other is darkened until it reads on white.

iex> Ascii.Parts.suggested(Ascii.meta!("boot-log"))["status.ok"]
"#3fb950"
iex> Ascii.Parts.suggested(Ascii.meta!("boot-log"), paper: true)["status.ok"]
"#1a7f37"