Notty is a terminal library that revolves around construction and composition of displayable images.
This module provides the core
image abstraction, standalone
rendering, and escape sequence parsing. It does not
depend on any platform code, and does not interact with the environment.
Input and output are provided by
v0.2.2 — homepage
Visual characteristics of displayed text.
Rectangles of styled characters.
A is for attribute.
I is for image.
You can ignore it, unless you are porting
Notty to a new platform not
supported by the existing IO backends.
Dump images to string buffers.
Parse and decode escape sequences in character streams.
Print a red-on-black
"Wow!" above its right-shifted copy:
let wow = I.string A.(fg red ++ bg black) "Wow!" in I.(wow <-> (void 2 0 <|> wow)) |> Notty_unix.output_image
image value is a rectangle of styled character cells. It has a
width and height, but is not anchored to an origin. A single character with
associated display attributes, or a short fragment of text, are simple
examples of images.
Images are created by combining text fragments with display attributes, and composed by placing them beside each other, above each other, and over each other.
Once constructed, an image can be rendered, and only at that point it obtains absolute placement.
I for more details.
attr values describe the styling characteristics of fragments of
They combine a foreground and a background
color with a
styles. Either color can be unset, which corresponds to
the terminal's default foreground (resp. background) color.
Attributes are used to construct primitive images.
A for more details.
These are taken to be characters in the ranges
0x80-0x9f (C1). This is the
general category Cc.
As control characters directly influence the cursor positioning, they cannot be used to create images.
This, in particular, means that images cannot contain
Notty does not use Terminfo. If your terminal is particularly
idiosyncratic, things might fail to work. Get in touch with the author to
Notty assumes that the terminal is using UTF-8 for input and output.
Things might break arbitrarily if this is not the case.
For performance considerations, consult the performance model.
Uucp.Break.tty_width_hint to guess the width of text
fragments when computing geometry, and it suffers from the same
Uucpand the terminal have to agree on the Unicode version.
When in doubt, see
Unicode has special interaction with horizontal cropping:
U+0020(SPACE). Hence, character-cell-accurate cropping is possible even in the presence of characters that horizontally occupy more than one cell.
We assume a toplevel with
Notty support (
"Rad!" with default foreground and background:
I.string A.empty "Rad!"
Everything has to start somewhere.
"Rad!" in rad letters:
I.string A.(fg lightred) "Rad!"
let a1 = A.(fg lightwhite ++ bg red) and a2 = A.(fg red)
" stuff!" in different colors:
I.(string a1 "Rad" <|> string a2 " stuff!")
The second word hanging on a line below:
I.(string a1 "Rad" <|> (string a2 "stuff!" |> vpad 1 0))
let square = "\xe2\x96\xaa" let rec sierp n = if n > 1 then let ss = sierp (pred n) in I.(ss <-> (ss <|> ss)) else I.(string A.(fg magenta) square |> hpad 1 0)
A triangle overlaid over its shifted copy:
let s = sierp 6 in I.(s </> vpad 1 0 s)
let rad n color = let a1 = A.fg color in let a2 = A.(st blink ++ a1) in I.((string a2 "Rad" |> hpad n 0) <-> (string a1 "(⌐■_■)" |> hpad (n + 7) 0)) let colors = A.[red; green; yellow; blue; magenta; cyan]
colors |> List.mapi I.(fun i c -> rad i c |> pad ~t:i ~l:(2 * i)) |> I.zcat
Note Usage of
blink might be regulated by law in some
Images can be pretty-printed into:
I.strf "(%d)" 42
Attributes can be applied to the entire format string, or by decorating
user-defined printers that are supplied with
let pp = Format.pp_print_int
I.strf ~attr:A.(fg lightwhite) "(%a)" (I.pp_attr A.(fg green) pp) 42
The core module has no real IO. Examples above are simple
expressions, displayed by the pretty-printer that is installed by the
toplevel support. Self-contained programs need a separate IO module:
sierp 8 |> Notty_unix.output_image
(Note the difference in cropping behavior.)
Computations can be adapted to the current terminal size. A line can stretch end-to-end:
Notty_unix.output_image_size @@ fun (w, _) -> let i1 = I.string A.(fg green) "very" and i2 = I.string A.(fg yellow) "melon" in I.(i1 <|> void (w - width i1 - width i2) 1 <|> i2)
The largest triangle that horizontally fits into the terminal:
Notty_unix.output_image_size @@ fun (w, _) -> let steps = int_of_float ((log (float w)) /. log 2.) in sierp steps |> I.vpad 0 1
let img (double, n) = let s = sierp n in if double then I.(s </> vpad 1 0 s) else s in let rec update t state = Term.image t (img state); loop t state and loop t (double, n as state) = match Term.event t with | `Key (`Enter,_) -> () | `Key (`Arrow `Left,_) -> update t (double, max 1 (n - 1)) | `Key (`Arrow `Right,_) -> update t (double, min 8 (n + 1)) | `Key (`ASCII ' ', _) -> update t (not double, n) | `Resize _ -> update t state | _ -> loop t state in let t = Term.create () in update t (false, 1); Term.release t
The program uses a fullscreen terminal and loops reading the input. LEFT and RIGHT control the iteration count, and SPACE toggles double-drawing. Resizing the window causes a redraw. When the loop exits on ENTER, the terminal is cleaned up.
This section is only relevant if using
Notty becomes your bottleneck.
TL;DR Shared sub-expressions do not share work, so operators stick with you.
The main performance parameter is image complexity. This roughly
corresponds to the number of image composition and
cropping operators in the fully expanded
ignoring all sharing.
cplx of an image
cplx i = 1.
cplx (op i1 i2) = 1 + cplx i1 + cplx i2.
cplx (cr i1) = 1 + cplx i1 - k, where
kis the combined complexity of all the maximal sub-terms that do not contribute to the output.
For example (assuming an image
let img1 = I.((i <|> i) <-> (i <|> i)) let img2 = I.(let x = i <|> i in x <-> x) let img3 = I.(((i <|> i) <|> i) <|> i)
Complexity of each of these is
4 * cplx i + 3. This might be surprising
width i = 1,
cplx (hcrop 1 0 img1) = 3 + 2 * cplx i, and
cplx (hcrop 2 0 img3) = 2 + 2 * cplx i.
Notty strives to be accommodating to all usage scenarios, these are
the things to keep in mind if the rendering becomes slow:
let wrap1 width img = let rec go img = img :: if I.width img > width then go (I.hcrop width 0 img) else  in go img |> I.vcat |> I.hsnap ~align:`Left width
cropis applied only
linestimes, the image complexity of each line depends on the number of preceding lines. An O(n) version does not iterate
let wrap2 width img = let rec go off = I.hcrop off 0 img :: if I.width img - off > width then go (off + width) else  in go 0 |> I.vcat |> I.hsnap ~align:`Left width
w * himplicitly crops it to its leftmost
wcolumns and topmost
hwill have an impact on the rendering performance, the complexity of the (cropped) image tends to be more important.