Packages
Terminal emulation and driver infrastructure for Raxol. ANSI parsing, screen buffers, command processing, cursor management, input handling, session management, and termbox2 NIF integration.
Current section
Files
Jump to
Current section
Files
lib/raxol/terminal/ansi/input_parser.ex
defmodule Raxol.Terminal.ANSI.InputParser do
@moduledoc """
Parses raw ANSI terminal input bytes into Raxol Event structs.
Handles:
- Arrow keys, Enter, Backspace, Tab, Escape
- Function keys F1-F12 (SS3 and CSI variants)
- Navigation keys (Home, End, Insert, Delete, PageUp, PageDown)
- Modifier combos (Shift+Tab, Ctrl+Arrow, Alt+key, etc.)
- Ctrl+A through Ctrl+Z
- Mouse SGR and X10/normal mode events
- Focus in/out events
- Bracketed paste
- Printable ASCII and UTF-8 characters
"""
alias Raxol.Core.Events.Event
import Bitwise
@doc """
Parses a binary of raw terminal input into a list of Event structs.
Returns a list because a single read may contain multiple events
(e.g., pasted text or buffered input).
"""
@spec parse(binary()) :: [Event.t()]
def parse(data) when is_binary(data), do: parse_loop(data, [])
defp parse_loop(<<>>, acc), do: Enum.reverse(acc)
defp parse_loop(data, acc) do
case parse_one(data) do
{nil, <<>>} ->
Enum.reverse(acc)
{nil, _rest} ->
# Unrecognized byte -- skip it and continue
<<_, rest::binary>> = data
parse_loop(rest, acc)
{event, rest} ->
parse_loop(rest, [event | acc])
end
end
# --- parse_one/1: consume one event from the front, return {event, rest} ---
# --- CSI sequences: ESC [ ... ---
# Bracketed paste: ESC [ 200 ~
defp parse_one(<<27, 91, 50, 48, 48, 126, rest::binary>>) do
case :binary.split(rest, <<27, 91, 50, 48, 49, 126>>) do
[pasted, remaining] ->
{%Event{type: :paste, data: %{text: pasted}}, remaining}
[_no_end] ->
{%Event{type: :paste, data: %{text: rest}}, <<>>}
end
end
# Mouse SGR mode: ESC [ < params M/m
defp parse_one(<<27, 91, 60, rest::binary>>) do
parse_sgr_mouse_one(rest)
end
# Mouse X10/normal mode: ESC [ M <3 bytes>
defp parse_one(<<27, 91, 77, button, x, y, rest::binary>>) do
{parse_x10_mouse_event(button, x, y), rest}
end
# Arrow keys
defp parse_one(<<27, 91, 65, rest::binary>>), do: {key_event(:up), rest}
defp parse_one(<<27, 91, 66, rest::binary>>), do: {key_event(:down), rest}
defp parse_one(<<27, 91, 67, rest::binary>>), do: {key_event(:right), rest}
defp parse_one(<<27, 91, 68, rest::binary>>), do: {key_event(:left), rest}
# Navigation keys (CSI letter variants)
defp parse_one(<<27, 91, 72, rest::binary>>), do: {key_event(:home), rest}
defp parse_one(<<27, 91, 70, rest::binary>>), do: {key_event(:end), rest}
# Shift+Tab (backtab)
defp parse_one(<<27, 91, 90, rest::binary>>),
do: {key_event(:tab, shift: true), rest}
# Focus events
defp parse_one(<<27, 91, 73, rest::binary>>),
do: {%Event{type: :focus, data: %{focused: true}}, rest}
defp parse_one(<<27, 91, 79, rest::binary>>),
do: {%Event{type: :focus, data: %{focused: false}}, rest}
# Modified keys: ESC [ 1 ; <mod> <letter>
defp parse_one(<<27, 91, 49, 59, mod, letter, rest::binary>>)
when letter in [65, 66, 67, 68, 70, 72, 80, 81, 82, 83] do
{shift, alt, ctrl} = decode_modifier(mod - ?0)
key = csi_letter_to_key(letter)
{key_event(key, shift: shift, alt: alt, ctrl: ctrl), rest}
end
# CSI tilde/letter sequences: ESC [ <params...> <final>
defp parse_one(<<27, 91, rest::binary>>) do
parse_csi_tilde_one(rest)
end
# --- SS3 sequences: ESC O ... ---
# F1-F4 SS3 variants
defp parse_one(<<27, 79, 80, rest::binary>>), do: {key_event(:f1), rest}
defp parse_one(<<27, 79, 81, rest::binary>>), do: {key_event(:f2), rest}
defp parse_one(<<27, 79, 82, rest::binary>>), do: {key_event(:f3), rest}
defp parse_one(<<27, 79, 83, rest::binary>>), do: {key_event(:f4), rest}
# SS3 Home/End (some terminals)
defp parse_one(<<27, 79, 72, rest::binary>>), do: {key_event(:home), rest}
defp parse_one(<<27, 79, 70, rest::binary>>), do: {key_event(:end), rest}
# --- Alt+key: ESC <char> (must come after ESC[ and ESC O) ---
defp parse_one(<<27, char, rest::binary>>) when char >= 32 and char <= 126 do
{key_event(:char, char: <<char>>, alt: true), rest}
end
# --- Bare escape ---
defp parse_one(<<27>>), do: {key_event(:escape), <<>>}
# --- Control characters ---
# Enter (CR)
defp parse_one(<<13, rest::binary>>), do: {key_event(:enter), rest}
# Linefeed (LF) - also treat as enter
defp parse_one(<<10, rest::binary>>), do: {key_event(:enter), rest}
# Backspace
defp parse_one(<<127, rest::binary>>), do: {key_event(:backspace), rest}
# Tab
defp parse_one(<<9, rest::binary>>), do: {key_event(:tab), rest}
# Ctrl+Space / Null
defp parse_one(<<0, rest::binary>>),
do: {key_event(:char, char: " ", ctrl: true), rest}
# Ctrl+A through Ctrl+Z (bytes 1-26, excluding special cases above)
defp parse_one(<<byte, rest::binary>>) when byte >= 1 and byte <= 26 do
char = <<byte + 96>>
{key_event(:char, char: char, ctrl: true), rest}
end
# --- Printable ASCII ---
defp parse_one(<<char, rest::binary>>) when char >= 32 and char <= 126 do
{key_event(:char, char: <<char>>), rest}
end
# --- Multi-byte UTF-8 ---
# 2-byte: 110xxxxx 10xxxxxx
defp parse_one(<<a, b, rest::binary>>)
when a >= 0xC0 and a <= 0xDF and b >= 0x80 and b <= 0xBF do
{key_event(:char, char: <<a, b>>), rest}
end
# 3-byte: 1110xxxx 10xxxxxx 10xxxxxx
defp parse_one(<<a, b, c, rest::binary>>)
when a >= 0xE0 and a <= 0xEF and b >= 0x80 and b <= 0xBF and
c >= 0x80 and c <= 0xBF do
{key_event(:char, char: <<a, b, c>>), rest}
end
# 4-byte: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
defp parse_one(<<a, b, c, d, rest::binary>>)
when a >= 0xF0 and a <= 0xF7 and b >= 0x80 and b <= 0xBF and
c >= 0x80 and c <= 0xBF and d >= 0x80 and d <= 0xBF do
{key_event(:char, char: <<a, b, c, d>>), rest}
end
# --- Unrecognized ---
defp parse_one(<<>>), do: {nil, <<>>}
defp parse_one(_data), do: {nil, :skip}
# --- CSI tilde sequence parser (returns {event, rest}) ---
defp parse_csi_tilde_one(data) do
case parse_csi_params(data) do
{[n], ?~, rest} ->
{key_event(tilde_key(n)), rest}
{[n, mod], ?~, rest} ->
{shift, alt, ctrl} = decode_modifier(mod)
{key_event(tilde_key(n), shift: shift, alt: alt, ctrl: ctrl), rest}
{[1, mod], letter, rest} when letter in [80, 81, 82, 83] ->
{shift, alt, ctrl} = decode_modifier(mod)
key = csi_letter_to_key(letter)
{key_event(key, shift: shift, alt: alt, ctrl: ctrl), rest}
_ ->
{nil, data}
end
end
# Parse semicolon-separated numeric params ending with a final byte.
# Returns {params, final_byte, rest}.
defp parse_csi_params(data) do
parse_csi_params(data, [], [])
end
defp parse_csi_params(<<byte, rest::binary>>, current_digits, params)
when byte >= ?0 and byte <= ?9 do
parse_csi_params(rest, [byte | current_digits], params)
end
defp parse_csi_params(<<?;, rest::binary>>, current_digits, params) do
num = digits_to_integer(current_digits)
parse_csi_params(rest, [], params ++ [num])
end
defp parse_csi_params(<<final_byte, rest::binary>>, current_digits, params)
when final_byte >= 64 and final_byte <= 126 do
num = digits_to_integer(current_digits)
{params ++ [num], final_byte, rest}
end
defp parse_csi_params(<<>>, current_digits, params) do
num = digits_to_integer(current_digits)
{params ++ [num], nil, <<>>}
end
defp digits_to_integer([]), do: 0
defp digits_to_integer(digits) do
digits |> Enum.reverse() |> List.to_string() |> String.to_integer()
end
# Tilde key mapping
defp tilde_key(1), do: :home
defp tilde_key(2), do: :insert
defp tilde_key(3), do: :delete
defp tilde_key(4), do: :end
defp tilde_key(5), do: :page_up
defp tilde_key(6), do: :page_down
defp tilde_key(11), do: :f1
defp tilde_key(12), do: :f2
defp tilde_key(13), do: :f3
defp tilde_key(14), do: :f4
defp tilde_key(15), do: :f5
defp tilde_key(17), do: :f6
defp tilde_key(18), do: :f7
defp tilde_key(19), do: :f8
defp tilde_key(20), do: :f9
defp tilde_key(21), do: :f10
defp tilde_key(23), do: :f11
defp tilde_key(24), do: :f12
defp tilde_key(_), do: :unknown
# CSI letter to key mapping (catch-all kept intentionally for future CSI codes)
@dialyzer {:nowarn_function, csi_letter_to_key: 1}
defp csi_letter_to_key(65), do: :up
defp csi_letter_to_key(66), do: :down
defp csi_letter_to_key(67), do: :right
defp csi_letter_to_key(68), do: :left
defp csi_letter_to_key(70), do: :end
defp csi_letter_to_key(72), do: :home
defp csi_letter_to_key(80), do: :f1
defp csi_letter_to_key(81), do: :f2
defp csi_letter_to_key(82), do: :f3
defp csi_letter_to_key(83), do: :f4
defp csi_letter_to_key(_), do: :unknown
# Decode xterm modifier parameter (mod-1 is bitmask: bit0=shift, bit1=alt, bit2=ctrl)
defp decode_modifier(mod) when is_integer(mod) do
bits = mod - 1
shift = (bits &&& 1) != 0
alt = (bits &&& 2) != 0
ctrl = (bits &&& 4) != 0
{shift, alt, ctrl}
end
# --- SGR mouse parsing (returns {event, rest}) ---
defp parse_sgr_mouse_one(data) do
case Regex.run(~r/^(\d+);(\d+);(\d+)([mM])/, data, return: :index) do
[{0, full_len} | captures] ->
[
{bs, bl},
{xs, xl},
{ys, yl},
{ks, _kl}
] = captures
button_code =
binary_part(data, bs, bl) |> String.to_integer()
x = binary_part(data, xs, xl) |> String.to_integer()
y = binary_part(data, ys, yl) |> String.to_integer()
kind = binary_part(data, ks, 1)
rest = binary_part(data, full_len, byte_size(data) - full_len)
{button, motion} = decode_sgr_button(button_code)
action =
case {kind, motion} do
{_, true} -> :move
{"M", _} -> :press
{"m", _} -> :release
end
{shift, alt, ctrl} = decode_sgr_modifiers(button_code)
event = %Event{
type: :mouse,
data: %{
button: button,
x: x,
y: y,
action: action,
shift: shift,
alt: alt,
ctrl: ctrl
}
}
{event, rest}
_ ->
{nil, data}
end
end
defp decode_sgr_button(code) do
motion = (code &&& 32) != 0
base = code &&& 0x03
button =
cond do
(code &&& 64) != 0 and base == 0 -> :wheel_up
(code &&& 64) != 0 and base == 1 -> :wheel_down
(code &&& 128) != 0 -> :extra
base == 0 -> :left
base == 1 -> :middle
base == 2 -> :right
base == 3 -> :release
true -> :unknown
end
{button, motion}
end
defp decode_sgr_modifiers(code) do
shift = (code &&& 4) != 0
alt = (code &&& 8) != 0
ctrl = (code &&& 16) != 0
{shift, alt, ctrl}
end
# --- X10/normal mouse event ---
defp parse_x10_mouse_event(button_byte, x_byte, y_byte) do
button_code = button_byte - 32
x = x_byte - 32
y = y_byte - 32
{button, motion} = decode_sgr_button(button_code)
action =
case motion do
true -> :move
false -> :press
end
%Event{
type: :mouse,
data: %{
button: button,
x: x,
y: y,
action: action
}
}
end
# --- Event constructors ---
defp key_event(key, opts \\ []) do
data =
%{key: key}
|> maybe_put(:char, Keyword.get(opts, :char))
|> maybe_put_bool(:shift, Keyword.get(opts, :shift, false))
|> maybe_put_bool(:alt, Keyword.get(opts, :alt, false))
|> maybe_put_bool(:ctrl, Keyword.get(opts, :ctrl, false))
%Event{type: :key, data: data}
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp maybe_put_bool(map, _key, false), do: map
defp maybe_put_bool(map, key, true), do: Map.put(map, key, true)
end