# `Drafter.Terminal.ANSI`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/terminal/ansi.ex#L1)

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.
  * `{:key_down, key, modifiers}`, `{:key_up, key, modifiers}` and
    `{:key_release_support, boolean}` — reports in the kitty keyboard protocol,
    which a terminal sends only after a driver has turned it on; the support event
    is `true` only when the terminal will report releases. See
    `Drafter.Terminal.KittyKeyboard`.
  * `{:cell_size, {width, height}}` — the terminal's answer, in pixels, to
    `Drafter.Terminal.Reports.cell_size_query/0`.
  * `{:pixel_mouse, boolean}` — the terminal's answer to
    `Drafter.Terminal.Reports.pixel_mouse_query/0`: whether mouse reports now come
    in pixels.

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

## Pixel mouse reports

A terminal in SGR-Pixels mode (DECSET 1016) sends SGR reports whose coordinates are
pixels, not cells. They look the same as cell reports, so the parser reads SGR
reports as pixels only while pixel reading is on: from the start of a parse given
`pixel_mouse: true` and a `:cell_size`, or from a `{:pixel_mouse, true}` report
onward once a cell size is known. A `{:cell_size, size}` report in the buffer
replaces the cell size for the rest of it, and a `{:pixel_mouse, false}` report
turns pixel reading off.

A pixel report keeps the payload above, with `x` and `y` the zero-based cell under
the pointer, and adds `px: {x, y}`, the zero-based pixel counted from the terminal's
top-left corner. A cell report never carries `:px`, so its absence means the
position is known only to the cell.

    iex> Drafter.Terminal.ANSI.parse_sequence("\e[<0;25;37M", pixel_mouse: true, cell_size: {8, 16})
    {[{:mouse, %{type: :mouse_down, button: :left, x: 3, y: 2, px: {24, 36}, modifiers: []}}], ""}

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

A complete CSI sequence (`ESC [`, parameter and intermediate bytes `0x20`..`0x3F`,
a final byte `0x40`..`0x7E`) that no key, mouse or report parser reads is consumed
the same way. `ESC [` with no final byte yet is incomplete; on flush it is the
escape key followed by `[`.

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"

# `event`

```elixir
@type event() ::
  {:key, key()}
  | {:key, key(), modifiers()}
  | {:char, char()}
  | {:mouse, mouse_payload()}
  | {:bracketed_paste, binary()}
  | {:key_down, KittyKeyboard.key(), modifiers()}
  | {:key_up, KittyKeyboard.key(), modifiers()}
  | {:key_release_support, boolean()}
  | {:cell_size, {pos_integer(), pos_integer()}}
  | {:pixel_mouse, boolean()}
```

Everything `parse_sequence/1` and `flush_sequence/1` can produce.

# `key`

```elixir
@type key() :: atom()
```

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

# `modifiers`

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

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

# `mouse_payload`

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

What a `{:mouse, payload}` event carries.

`:button` is absent on `:move`, `:direction` is present only on `:scroll`, and `:px`
only on a pixel report.

# `bg_color`

```elixir
@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"

# `bold`

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

Turn on bold.

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

# `clear_line`

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

Clear the whole line the cursor is on.

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

# `clear_screen`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Turn on dim.

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

# `disable_mouse`

```elixir
@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"

# `disable_pixel_mouse`

```elixir
@spec disable_pixel_mouse() :: String.t()
```

Sequence that returns SGR mouse reports to cells (DECRST 1016).

    iex> Drafter.Terminal.ANSI.disable_pixel_mouse()
    "\e[?1016l"

# `enable_mouse`

```elixir
@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"

# `enable_pixel_mouse`

```elixir
@spec enable_pixel_mouse() :: String.t()
```

Sequence that makes SGR mouse reports carry pixels instead of cells (DECSET 1016).

Takes effect only while SGR reporting from `enable_mouse/1` is on. Reports are read
as pixels only once the parser is told so; see "Pixel mouse reports".

    iex> Drafter.Terminal.ANSI.enable_pixel_mouse()
    "\e[?1016h"

# `enter_alt_screen`

```elixir
@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`

```elixir
@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`

```elixir
@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"

# `flush_sequence`

```elixir
@spec flush_sequence(
  binary(),
  keyword()
) :: {[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>>}

Takes the same options as `parse_sequence/2`.

# `hide_cursor`

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

Hide the cursor.

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

# `incomplete_sequence?`

```elixir
@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`

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

Turn on italic.

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

# `parse_sequence`

```elixir
@spec parse_sequence(
  binary(),
  keyword()
) :: {[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>>}

Reports in the kitty keyboard protocol are read whatever the options, producing
the events `Drafter.Terminal.KittyKeyboard` describes. With `key_release: true`,
which a driver passes once it has turned that protocol on, every key press in a
legacy encoding is also followed by its `{:key_down, key, modifiers}`:

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

## Options

  * `:key_release` - `boolean()`, default `false`.
  * `:pixel_mouse` - `boolean()`, default `false`. `true` reads SGR mouse reports
    as pixels when a `:cell_size` is also given; see "Pixel mouse reports" in the
    module documentation.
  * `:cell_size` - `{width, height}` in pixels, or `nil` (the default).

# `printable_key`

```elixir
@spec printable_key(32..126) :: key()
```

The key atom for a printable ASCII codepoint, as `{:key, atom}` events carry it.

    iex> Drafter.Terminal.ANSI.printable_key(?a)
    :a

    iex> Drafter.Terminal.ANSI.printable_key(?\s)
    :" "

# `reset`

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

Clear every attribute and both colors.

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

# `reverse`

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

Turn on reverse video, swapping foreground and background.

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

# `show_cursor`

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

Show the cursor.

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

# `sync_end`

```elixir
@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`

```elixir
@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`

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

Turn on underline.

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
