Vtex.Input (Vtex v0.1.0)

Copy Markdown View Source

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 writing CSI 6 n) becomes {:cursor_position, row, col} (1-based). This is the in-band way to read the cursor — and, by parking it at CSI 999 ; 999 H first, 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_in and :focus_out as the window gains and loses focus.

Summary

Functions

Interpret a list of tokens into a list of semantic events.

Types

event()

@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()}

modifier()

@type modifier() :: :shift | :alt | :ctrl | :meta

Functions

interpret(tokens)

@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]