Maps raw tokens from Vtex.Input.Tokenizer to semantic input events.
This is a pure, stateless interpretation layer, typically called on the tokens
returned by Vtex.Input.Stream.feed/2. It understands the common key sequences sent
by terminals (arrow keys, editing keys, function keys) regardless of whether
they arrive as CSI or SS3, and it expands runs of text into per-character
events, decoding UTF-8 so multi-byte characters stay whole.
Event types
:enter
:backspace
:escape
:tab
:arrow_up | :arrow_down | :arrow_left | :arrow_right
:insert | :delete | :home | :end | :page_up | :page_down
{:function, 1..12}
{:alt, byte()}
{:key, base_event, [:shift | :alt | :ctrl | :meta]}
{:mouse, Vtex.Mouse.event()}
:paste_start | :paste_end
:focus_in | :focus_out
{:cursor_position, row :: pos_integer(), col :: pos_integer()}
{:char, char()}
{:sgr, [Vtex.SGR.attribute()]}
{:unknown, Vtex.Input.Tokenizer.token()}Arrow and editing keys are sent differently depending on the terminal's cursor
key mode: as CSI (ESC [ A) in normal mode, or as SS3 (ESC O A) in
application mode. Both forms are handled.
Modified keys
Holding Shift, Ctrl or Alt while pressing an arrow, navigation or
function key produces a CSI sequence with a modifier parameter — Shift+Up is
CSI 1 ; 2 A, Ctrl+Home is CSI 1 ; 5 H, Shift+F5 is CSI 15 ; 2 ~. The
modifier is encoded as 1 + bitmask, where the bitmask is 1=Shift, 2=Alt,
4=Ctrl, 8=Meta.
These surface as {:key, base, mods}, where base is the same event the
unmodified key would produce (:arrow_up, :home, {:function, 5}) and
mods is a list drawn from :shift, :alt, :ctrl, :meta in that order.
Unmodified keys keep their plain form, so :arrow_up and {:key, :arrow_up, [:shift]} are distinct.
Alt / Meta keys
A terminal sends an Alt/Meta-modified key as an ESC prefix followed by
the key (xterm's metaSendsEscape), so Alt+a arrives as ESC a. These
surface as {:alt, byte} events, where byte is the key that followed ESC.
This collides with the standalone Escape key, which is a bare ESC with
nothing after it. Because the parser is stateless and timeout-free it cannot
tell "the user pressed Escape" from "an ESC-prefixed sequence is still
arriving": a lone trailing ESC is held in the Vtex.Input.Stream buffer until the
next byte decides it, and ESC immediately followed by a key reads as :alt.
Disambiguating a real Escape press requires an inactivity timeout in the
caller (flush as :escape if no byte follows within a few milliseconds);
Vtex deliberately leaves that policy to you.
Bracketed paste
When bracketed paste is enabled (Vtex.Mouse-style, via Vtex.Paste.enable/0)
the terminal wraps pasted text between ESC [ 200 ~ and ESC [ 201 ~, which
surface as :paste_start and :paste_end. The pasted bytes in between arrive
as ordinary events — accumulate them until :paste_end to reconstruct the
text, treating it as literal data (e.g. an embedded :enter is a newline in
the pasted content, not a "submit"). Apply your own size limit while
accumulating: the parser is stateless and does not buffer the paste for you,
so a never-terminated paste cannot exhaust memory.
Reports and focus
A couple of sequences arrive as replies to a query you send, or when a mode is enabled:
- A Cursor Position Report (
CSI <row> ; <col> R, the reply to writingCSI 6 n) becomes{:cursor_position, row, col}(1-based). This is the in-band way to read the cursor — and, by parking it atCSI 999 ; 999 Hfirst, to probe the terminal size when the transport can't tell you (prefer SSH window-change / Telnet NAWS when it can). - Focus reporting (enabled with
Vtex.Focus.enable/0) delivers:focus_inand:focus_outas the window gains and loses focus.
Summary
Functions
Interpret a list of tokens into a list of semantic events.
Types
@type event() :: :enter | :backspace | :escape | :tab | :arrow_up | :arrow_down | :arrow_left | :arrow_right | :insert | :delete | :home | :end | :page_up | :page_down | {:function, 1..12} | {:alt, byte()} | {:key, event(), [modifier()]} | {:mouse, Vtex.Mouse.event()} | :paste_start | :paste_end | :focus_in | :focus_out | {:cursor_position, pos_integer(), pos_integer()} | {:char, char()} | {:sgr, [Vtex.SGR.attribute()]} | {:unknown, Vtex.Input.Tokenizer.token()}
@type modifier() :: :shift | :alt | :ctrl | :meta
Functions
@spec interpret([Vtex.Input.Tokenizer.token()]) :: [event()]
Interpret a list of tokens into a list of semantic events.
Examples
iex> Vtex.Input.interpret([{:text, "hi\r"}])
[{:char, ?h}, {:char, ?i}, :enter]
iex> Vtex.Input.interpret([{:text, "é"}])
[{:char, ?é}]
iex> Vtex.Input.interpret([{:csi, "", "", ?A}, {:ss3, ?B}])
[:arrow_up, :arrow_down]
iex> Vtex.Input.interpret([{:csi, "5", "", ?~}])
[:page_up]
iex> Vtex.Input.interpret([{:esc, ?x}])
[{:alt, ?x}]
iex> Vtex.Input.interpret([{:csi, "1;2", "", ?A}, {:csi, "15;5", "", ?~}])
[{:key, :arrow_up, [:shift]}, {:key, {:function, 5}, [:ctrl]}]
iex> Vtex.Input.interpret([{:csi, "0;10;5", "<", ?M}])
[{:mouse, %{action: :press, button: :left, x: 10, y: 5, mods: []}}]
iex> Vtex.Input.interpret([{:csi, "200", "", ?~}, {:text, "hi"}, {:csi, "201", "", ?~}])
[:paste_start, {:char, ?h}, {:char, ?i}, :paste_end]
iex> Vtex.Input.interpret([{:csi, "24;80", "", ?R}, {:csi, "", "", ?O}])
[{:cursor_position, 24, 80}, :focus_out]