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
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).
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
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.
iex> Ascii.Parts.captures(" [ OK ] done", ~r/^ *(\[ OK \]) (.*)$/, ["status.ok", "message"])
[{nil, " "}, {"status.ok", "[ OK ]"}, {nil, " "}, {"message", "done"}]
@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.
@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", "▁▁"}]
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", "░░"}]
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"}]
A whole line in one part, or (nil) in none.
@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"}]
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.
iex> Ascii.Parts.parent("status.ok")
"status"
iex> Ascii.Parts.parent("status")
nil
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.
@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 ]"}
@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"