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
raxol_terminal lib raxol terminal ansi input_parser.ex
Raw

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