package notty

  1. Overview
  2. Docs

Declaring terminals.

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 Notty_unix and Notty_lwt.

Consult the basics, examples and limitations.

Interface

type uchar = int

A lone Unicode scalar value.

type attr

Visual characteristics of displayed text.

type image

Rectangles of styled characters.

module A : sig ... end

A is for attribute.

module I : sig ... end

I is for image.

module Infix : sig ... end

Operators, repeated.

Low-level interface

You can ignore it, unless you are porting Notty to a new platform not supported by the existing IO backends.

module Cap : sig ... end

Terminal capabilities.

module Render : sig ... end

Convert images to string.

module Unescape : sig ... end

Parse and decode escape sequences in character streams.

Basics

Print a red "Wow!" above its right-shifted copy:

let wow = I.string A.(fg lightred) "Wow!" in
I.(wow <-> (void 2 0 <|> wow)) |> Notty_unix.output_image_endline

The meaning of images

An 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.

Consult I for more details.

Display attributes

attr values describe the styling characteristics of fragments of text.

They combine a foreground and a background color with a set of styles. Either color can be unset, which corresponds to the terminal's default foreground (resp. background) color.

Attributes are used to construct primitive images.

Consult A for more details.

Control characters

These are taken to be characters in the ranges 0x00-0x1f (C0) and 0x80-0x9f (C1), and 0x7f (BACKSPACE). This is the Unicode 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 U+000a (NEWLINE).

Limitations

Notty does not use Terminfo. If your terminal is particularly idiosyncratic, things might fail to work. Get in touch with the author to expand support.

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.

Unicode vs. Text geometry

Notty uses Uucp.Break.tty_width_hint to guess the width of text fragments when computing geometry, and it suffers from the same shortcomings:

  • Geometry in general works for alphabets and east Asian scripts, mostly works for abjad scripts, and is a matter of luck for abugidas.
  • East Asian scripts work better when in NFC.
  • Emoji tend to be consistent with the actual rendering, and the actual rendering tends to be wrong.

When in doubt, see Uucp.Break.tty_width_hint.

Unicode also has a special interaction with horizontal cropping:

  • Strings within images are cropped at grapheme cluster boundaries. This means that scalar value sequences that are rendered combined, or overlaid, stay unbroken.
  • When a crop splits a wide character in two, the remaining half is replaced by U+0020 (SPACE). Hence, character-cell-accurate cropping is possible even in the presence of characters that horizontally occupy more than one cell.

Examples

There are further examples in the /examples directory in the source tree.

We assume the module is open:

open Notty

As the core module has no IO, we borrow a helper from Notty_unix:

let output_image_endline = Notty_unix.output_image_endline

Hello

Output "Rad!" with default foreground and background:

I.string A.empty "Rad!" |> output_image_endline

Hello, with colors

Output "Rad!" in rad letters:

I.string A.(fg lightred) "Rad!" |> output_image_endline

Padding and spacing

let a1 = A.(fg lightwhite ++ bg red)
and a2 = A.(fg red)

Output "Rad" and " stuff!" in different colors:

I.(string a1 "Rad" <|> string a2 " stuff!")
  |> output_image_endline

Output them with the second word hanging on a line below:

I.(string a1 "Rad" <|> (void 0 1 <-> string a2 "stuff!"))
  |> output_image_endline

More geometry

Sierpinski triangle:

let square = "\xe2\x97\xbe"

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)

Print a triangle:

sierp 8 |> output_image_endline

(Note the cropping behavior.)

Print a triangle overlaid over its shifted copy:

let s = sierp 6 in I.(s </> hpad 1 0 s) |> output_image_endline

Blinkenlights:

let rad n color =
  let a1 = A.fg color in
  let a2 = A.(st blink ++ a1) in
  I.((string a1 "Rad" |> hpad n 0) <->
     (string a2 "stuff!" |> hpad (n + 6) 0))
in
A.[ red; green; yellow; blue; magenta; cyan ]
  |> List.mapi I.(fun i c -> rad i c |> pad ~t:i ~l:(2 * i))
  |> I.zcat
  |> output_image_endline

Note Usage of blink might be regulated by law in some jurisdictions.

Pretty-printing

Pretty-printing into an image:

I.strf ~attr:A.(fg green) "(%d)" 42 |> output_image_endline

Decorated pretty-printers:

let pp = Format.pp_print_int |> I.pp_attr A.(fg green) in
I.strf "(%a)" pp 43 |> output_image_endline

Taking terminal size into account

Space a line end-to-end horizontally:

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)

Print a triangle that 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

Simple interaction

Interactive Sierpinski:

open Notty_unix

let img (double, n) =
  let s = sierp n in
  if double then I.(s </> hpad 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 (`Uchar 0x20,_)   -> 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.

Performance model

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 image term, ignoring all sharing.

Outline numbers:

  • Highly complex images can be rendered and pushed out to a full-screen terminal more than 1000 times per second.
  • With more realistic images, this number is closer to 30,000.
  • Input processing is somewhere around 50MB/s.

Image complexity cplx of an image i is:

  • For a primitive i, cplx i = 1.
  • For a composition operator op, cplx (op i1 i2) = 1 + cplx i1 + cplx i2.
  • For a crop cr, cplx (cr i1) = 1 + cplx i1 - k, where k is the combined complexity of all the maximal sub-terms that do not contribute to the output.

For example (assuming an image i):

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 for img2.

If width i = 1, cplx (hcrop 1 0 img1) = 3 + 2 * cplx i, and cplx (hcrop 2 0 img3) = 2 + 2 * cplx i.

While Notty strives to be accommodating to all usage scenarios, these are the things to keep in mind if the rendering becomes slow:

  1. Image composition is cheap.

    Combining images performs a negligible amount of computation.

    Constructing primitive images that contain scalar values outside of the ASCII range does a little more work upfront and is worth holding onto.

  2. Rendering depends on image complexity.

    As a consequence, this real-world example of wrapping renders in time O(n2) in the number of lines:

    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

    Although crop is applied only lines times, the image complexity of each line depends on the number of preceding lines.

    An O(n) version does not iterate crop:

    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
  3. Rendering depends on the output dimensions, but not on the image dimensions.

    Rendering an image to w * h implicitly crops it to its leftmost w columns and topmost h rows. While w and h will have an impact on the rendering performance, the complexity of the (cropped) image tends to be more important.

OCaml

Innovation. Community. Security.