Drafter.Terminal.ANSI (drafter v0.3.2)

Copy Markdown View Source

Decodes terminal input bytes into events and builds terminal output sequences.

Input

parse_sequence/1 turns a byte buffer into a list of events plus the bytes that cannot yet be decided. flush_sequence/1 does the same but resolves the trailing ambiguity instead of retaining it. incomplete_sequence?/1 reports whether a buffer ends mid-sequence.

iex> Drafter.Terminal.ANSI.parse_sequence("hi\e[A\e[")
{[{:key, :h}, {:key, :i}, {:key, :up}], "\e["}

Event shapes

Every event this module produces takes one of these forms:

  • {:key, key} — a named key (:up, :enter, :f5, :backspace, :escape) or, for printable ASCII 32..126, the character itself as an atom (:a, :Z, :"1", :" ").
  • {:key, key, modifiers} — the same, with a non-empty modifier list. Modifiers are a subset of [:ctrl, :alt, :shift] and appear in that order.
  • {:char, codepoint} — a printable codepoint outside ASCII 32..126, as an integer.
  • {:mouse, payload} — see below.
  • {:bracketed_paste, text} — the content between ESC [ 200~ and ESC [ 201~, undecoded and with the delimiters stripped.

Control characters \x01..\x1a decode as {:key, letter, [:ctrl]}, except \x09 which is {:key, :tab} and \x0a/\x0d which are both {:key, :enter}.

Mouse payloads

x and y are zero-based column and row. modifiers is a subset of [:ctrl, :alt, :shift].

  • %{type: :mouse_down | :mouse_up | :drag, button: button, x: x, y: y, modifiers: mods} where button is :left, :middle, :right, :scroll or :unknown

  • %{type: :move, x: x, y: y, modifiers: mods}
  • %{type: :scroll, direction: :up | :down | :left | :right, x: x, y: y, modifiers: mods}

SGR (ESC [ < …M/m), legacy numeric and X10 (ESC [ M plus three bytes) encodings are all accepted. A final H in the SGR form is read as if it were M.

Discarded input

Terminals reply to queries with string-typed control sequences opened by ESC P, ESC ], ESC ^, ESC _ or ESC X and closed by a string terminator (ESC \, or BEL for OSC). These are consumed and produce no event. A buffer ending inside one is treated as incomplete.

The release report for a scroll button — buttons 64..67 with a final m — is consumed the same way, since the press already carried the scroll.

iex> Drafter.Terminal.ANSI.parse_sequence("\e[<64;1;1m")
{[], ""}

Output

The remaining functions return the escape sequences for cursor placement, screen clearing, alternate screen, synchronized updates, mouse reporting and SGR styling. They only build strings; writing them is the caller's job.

iex> Drafter.Terminal.ANSI.cursor_to(1, 1) <> Drafter.Terminal.ANSI.clear_screen()
"\e[1;1H\e[2J"

Summary

Types

Everything parse_sequence/1 and flush_sequence/1 can produce.

A named key such as :up or :f5, or a printable ASCII character as an atom.

Held modifiers, in the order :ctrl, :alt, :shift, and never empty in an event.

What a {:mouse, payload} event carries.

Functions

Set the background to the 24-bit color r, g, b, each 0..255.

Turn on bold.

Clear the whole line the cursor is on.

Clear the entire screen, leaving the cursor where it is.

Clear from the cursor to the end of the screen.

Place the cursor at column x, row y, both counted from 1.

Turn on dim.

Sequence that turns off mouse reporting.

Sequence that turns on mouse reporting in SGR encoding.

Switch to the alternate screen buffer, keeping the scrollback of the main one.

Return to the main screen buffer, restoring what was on it.

Set the foreground to the 24-bit color r, g, b, each 0..255.

Parse an input buffer, resolving any trailing ambiguity instead of retaining it.

Hide the cursor.

Whether the buffer ends in a control sequence that has not fully arrived.

Turn on italic.

Parse an input buffer into events plus the bytes that could not yet be decided.

Clear every attribute and both colors.

Turn on reverse video, swapping foreground and background.

Show the cursor.

Close the synchronized update opened by sync_start/0 and present the frame.

Open a synchronized update, so the terminal shows nothing until sync_end/0.

Turn on underline.

Types

event()

@type event() ::
  {:key, key()}
  | {:key, key(), modifiers()}
  | {:char, char()}
  | {:mouse, mouse_payload()}
  | {:bracketed_paste, binary()}

Everything parse_sequence/1 and flush_sequence/1 can produce.

key()

@type key() :: atom()

A named key such as :up or :f5, or a printable ASCII character as an atom.

modifiers()

@type modifiers() :: [:ctrl | :alt | :shift]

Held modifiers, in the order :ctrl, :alt, :shift, and never empty in an event.

mouse_payload()

@type mouse_payload() :: %{
  :type => :mouse_down | :mouse_up | :drag | :move | :scroll,
  :x => non_neg_integer(),
  :y => non_neg_integer(),
  :modifiers => modifiers(),
  optional(:button) => :left | :middle | :right | :scroll | :unknown,
  optional(:direction) => :up | :down | :left | :right
}

What a {:mouse, payload} event carries.

:button is absent on :move, and :direction is present only on :scroll.

Functions

bg_color(r, g, b)

Set the background to the 24-bit color r, g, b, each 0..255.

iex> Drafter.Terminal.ANSI.bg_color(0, 0, 128)
"\e[48;2;0;0;128m"

bold()

@spec bold() :: String.t()

Turn on bold.

iex> Drafter.Terminal.ANSI.bold()
"\e[1m"

clear_line()

@spec clear_line() :: String.t()

Clear the whole line the cursor is on.

iex> Drafter.Terminal.ANSI.clear_line()
"\e[2K"

clear_screen()

@spec clear_screen() :: String.t()

Clear the entire screen, leaving the cursor where it is.

iex> Drafter.Terminal.ANSI.clear_screen()
"\e[2J"

clear_to_end()

@spec clear_to_end() :: String.t()

Clear from the cursor to the end of the screen.

iex> Drafter.Terminal.ANSI.clear_to_end()
"\e[0J"

cursor_to(x, y)

@spec cursor_to(non_neg_integer(), non_neg_integer()) :: String.t()

Place the cursor at column x, row y, both counted from 1.

The arguments are in column-then-row order, the reverse of the order they take in the sequence itself.

iex> Drafter.Terminal.ANSI.cursor_to(3, 5)
"\e[5;3H"

dim()

@spec dim() :: String.t()

Turn on dim.

iex> Drafter.Terminal.ANSI.dim()
"\e[2m"

disable_mouse(opts \\ [])

@spec disable_mouse(keyword()) :: String.t()

Sequence that turns off mouse reporting.

Pass the same :hover value that was given to enable_mouse/1, so the mode that was set is the mode that is cleared. Defaults to true.

iex> Drafter.Terminal.ANSI.disable_mouse()
"\e[?1006l\e[?1003l"

iex> Drafter.Terminal.ANSI.disable_mouse(hover: false)
"\e[?1006l\e[?1002l"

enable_mouse(opts \\ [])

@spec enable_mouse(keyword()) :: String.t()

Sequence that turns on mouse reporting in SGR encoding.

Options:

  • :hover — when true (the default) any-motion tracking is enabled, so :move events arrive with no button held. When false only button presses, releases and drags are reported.

Examples

iex> Drafter.Terminal.ANSI.enable_mouse()
"\e[?1003h\e[?1006h"

iex> Drafter.Terminal.ANSI.enable_mouse(hover: false)
"\e[?1002h\e[?1006h"

enter_alt_screen()

@spec enter_alt_screen() :: String.t()

Switch to the alternate screen buffer, keeping the scrollback of the main one.

iex> Drafter.Terminal.ANSI.enter_alt_screen()
"\e[?1049h"

exit_alt_screen()

@spec exit_alt_screen() :: String.t()

Return to the main screen buffer, restoring what was on it.

iex> Drafter.Terminal.ANSI.exit_alt_screen()
"\e[?1049l"

fg_color(r, g, b)

Set the foreground to the 24-bit color r, g, b, each 0..255.

iex> Drafter.Terminal.ANSI.fg_color(255, 0, 0)
"\e[38;2;255;0;0m"

flush_sequence(buffer)

@spec flush_sequence(binary()) :: {[event()], binary()}

Parse an input buffer, resolving any trailing ambiguity instead of retaining it.

Call this when no further bytes are expected — after an escape timeout, or when the input stream closes. A lone ESC becomes the escape key, and an unterminated bracketed paste is delivered with the content received so far.

iex> Drafter.Terminal.ANSI.flush_sequence("\e")
{[{:key, :escape}], ""}

iex> Drafter.Terminal.ANSI.flush_sequence("\e[200~half")
{[{:bracketed_paste, "half"}], ""}

A codepoint split across a read is the one thing still retained, since its remaining bytes carry no ambiguity to resolve:

iex> Drafter.Terminal.ANSI.flush_sequence(<<0xC3>>)
{[], <<0xC3>>}

hide_cursor()

@spec hide_cursor() :: String.t()

Hide the cursor.

iex> Drafter.Terminal.ANSI.hide_cursor()
"\e[?25l"

incomplete_sequence?(arg1)

@spec incomplete_sequence?(binary()) :: boolean()

Whether the buffer ends in a control sequence that has not fully arrived.

A lone ESC counts as incomplete: it is indistinguishable from the start of a sequence still in flight, so the caller must resolve it on a timeout via flush_sequence/1.

iex> Drafter.Terminal.ANSI.incomplete_sequence?("\e")
true

iex> Drafter.Terminal.ANSI.incomplete_sequence?("\e[1;5")
true

iex> Drafter.Terminal.ANSI.incomplete_sequence?("\e[A")
false

iex> Drafter.Terminal.ANSI.incomplete_sequence?("ab")
false

An unterminated bracketed paste is not reported here: ESC [ 200~ is a complete CSI sequence. parse_sequence/1 holds the paste back on its own.

iex> Drafter.Terminal.ANSI.incomplete_sequence?("\e[200~half")
false

italic()

@spec italic() :: String.t()

Turn on italic.

iex> Drafter.Terminal.ANSI.italic()
"\e[3m"

parse_sequence(buffer)

@spec parse_sequence(binary()) :: {[event()], binary()}

Parse an input buffer into events plus the bytes that could not yet be decided.

A trailing sequence that is only partially received — an unterminated bracketed paste, a CSI awaiting its final byte, a split UTF-8 codepoint — is returned in the remaining buffer. Callers must carry that remainder into the next call.

Printable ASCII arrives as {:key, atom}, and anything else printable as {:char, codepoint}:

iex> Drafter.Terminal.ANSI.parse_sequence("A ")
{[{:key, :A}, {:key, :" "}], ""}

iex> Drafter.Terminal.ANSI.parse_sequence("é")
{[{:char, 233}], ""}

Named keys, control characters and modified keys:

iex> Drafter.Terminal.ANSI.parse_sequence("\e[A")
{[{:key, :up}], ""}

iex> Drafter.Terminal.ANSI.parse_sequence("\x03")
{[{:key, :c, [:ctrl]}], ""}

iex> Drafter.Terminal.ANSI.parse_sequence("\e[1;5C")
{[{:key, :right, [:ctrl]}], ""}

Mouse reports, in any of the three encodings, become a {:mouse, payload} pair with zero-based coordinates:

iex> Drafter.Terminal.ANSI.parse_sequence("\e[<0;10;5M")
{[{:mouse, %{type: :mouse_down, button: :left, x: 9, y: 4, modifiers: []}}], ""}

iex> Drafter.Terminal.ANSI.parse_sequence("\e[<64;1;1M")
{[{:mouse, %{type: :scroll, direction: :up, x: 0, y: 0, modifiers: []}}], ""}

iex> Drafter.Terminal.ANSI.parse_sequence("\e[<35;1;1M")
{[{:mouse, %{type: :move, x: 0, y: 0, modifiers: []}}], ""}

A bracketed paste is delivered whole, with its delimiters stripped:

iex> Drafter.Terminal.ANSI.parse_sequence("\e[200~two words\e[201~")
{[{:bracketed_paste, "two words"}], ""}

Replies to terminal queries are consumed without producing an event:

iex> Drafter.Terminal.ANSI.parse_sequence("\eP1$r0m\e\\x")
{[{:key, :x}], ""}

Whatever cannot yet be decided is handed back for the next call — an unterminated paste, a CSI without its final byte, and a split UTF-8 codepoint alike:

iex> Drafter.Terminal.ANSI.parse_sequence("\e[200~half")
{[], "\e[200~half"}

iex> Drafter.Terminal.ANSI.parse_sequence("x\e[")
{[{:key, :x}], "\e["}

iex> Drafter.Terminal.ANSI.parse_sequence(<<0xC3>>)
{[], <<0xC3>>}

reset()

@spec reset() :: String.t()

Clear every attribute and both colors.

iex> Drafter.Terminal.ANSI.reset()
"\e[0m"

reverse()

@spec reverse() :: String.t()

Turn on reverse video, swapping foreground and background.

iex> Drafter.Terminal.ANSI.reverse()
"\e[7m"

show_cursor()

@spec show_cursor() :: String.t()

Show the cursor.

iex> Drafter.Terminal.ANSI.show_cursor()
"\e[?25h"

sync_end()

@spec sync_end() :: String.t()

Close the synchronized update opened by sync_start/0 and present the frame.

iex> Drafter.Terminal.ANSI.sync_end()
"\e[?2026l"

sync_start()

@spec sync_start() :: String.t()

Open a synchronized update, so the terminal shows nothing until sync_end/0.

Terminals that do not implement mode 2026 ignore it and draw as bytes arrive.

iex> Drafter.Terminal.ANSI.sync_start()
"\e[?2026h"

underline()

@spec underline() :: String.t()

Turn on underline.

iex> Drafter.Terminal.ANSI.underline()
"\e[4m"