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 betweenESC [ 200~andESC [ 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}wherebuttonis:left,:middle,:right,:scrollor: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
@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.
@type key() :: atom()
A named key such as :up or :f5, or a printable ASCII character as an atom.
@type modifiers() :: [:ctrl | :alt | :shift]
Held modifiers, in the order :ctrl, :alt, :shift, and never empty in an event.
@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
@spec bg_color(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: String.t()
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"
@spec bold() :: String.t()
Turn on bold.
iex> Drafter.Terminal.ANSI.bold()
"\e[1m"
@spec clear_line() :: String.t()
Clear the whole line the cursor is on.
iex> Drafter.Terminal.ANSI.clear_line()
"\e[2K"
@spec clear_screen() :: String.t()
Clear the entire screen, leaving the cursor where it is.
iex> Drafter.Terminal.ANSI.clear_screen()
"\e[2J"
@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"
@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"
@spec dim() :: String.t()
Turn on dim.
iex> Drafter.Terminal.ANSI.dim()
"\e[2m"
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"
Sequence that turns on mouse reporting in SGR encoding.
Options:
:hover— whentrue(the default) any-motion tracking is enabled, so:moveevents arrive with no button held. Whenfalseonly 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"
@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"
@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"
@spec fg_color(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: String.t()
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"
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>>}
@spec hide_cursor() :: String.t()
Hide the cursor.
iex> Drafter.Terminal.ANSI.hide_cursor()
"\e[?25l"
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")
falseAn 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
@spec italic() :: String.t()
Turn on italic.
iex> Drafter.Terminal.ANSI.italic()
"\e[3m"
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>>}
@spec reset() :: String.t()
Clear every attribute and both colors.
iex> Drafter.Terminal.ANSI.reset()
"\e[0m"
@spec reverse() :: String.t()
Turn on reverse video, swapping foreground and background.
iex> Drafter.Terminal.ANSI.reverse()
"\e[7m"
@spec show_cursor() :: String.t()
Show the cursor.
iex> Drafter.Terminal.ANSI.show_cursor()
"\e[?25h"
@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"
@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"
@spec underline() :: String.t()
Turn on underline.
iex> Drafter.Terminal.ANSI.underline()
"\e[4m"