Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/terminal/ansi/mouse_tracking.ex
defmodule Raxol.Terminal.ANSI.MouseTracking do
@moduledoc """
Handles mouse tracking and focus tracking for the terminal.
Supports various mouse tracking modes and focus tracking events.
"""
import Bitwise
import Raxol.Guards
alias Raxol.Terminal.ANSI.Monitor
@type mouse_button :: :left | :middle | :right | :wheel_up | :wheel_down
@type mouse_action :: :press | :release | :move | :drag
@type mouse_event :: {mouse_button(), mouse_action(), integer(), integer()}
@type focus_event :: :focus_in | :focus_out
@mouse_modes %{
normal: 1000,
highlight: 1001,
button: 1002,
any: 1003,
focus: 1004
}
@mouse_buttons %{
0 => :left,
1 => :middle,
2 => :right,
64 => :wheel_up,
65 => :wheel_down,
66 => :wheel_up
}
@mouse_actions %{
0 => :press,
3 => :release,
# Left button press
32 => :press,
# Left button release
35 => :release,
# Mouse move
64 => :move,
# Mouse drag
67 => :drag,
240 => :wheel_up,
243 => :wheel_down
}
@doc """
Enables mouse tracking with the specified mode.
"""
@spec enable_mouse_tracking(atom()) :: String.t()
def enable_mouse_tracking(mode) do
case Map.get(@mouse_modes, mode) do
nil ->
Monitor.record_error(
"",
"Invalid mouse tracking mode: #{inspect(mode)}",
%{mode: mode}
)
""
code ->
"\e[?#{code}h"
end
end
@doc """
Disables mouse tracking with the specified mode.
"""
@spec disable_mouse_tracking(atom()) :: String.t()
def disable_mouse_tracking(mode) do
case Map.get(@mouse_modes, mode) do
nil ->
Monitor.record_error(
"",
"Invalid mouse tracking mode: #{inspect(mode)}",
%{mode: mode}
)
""
code ->
"\e[?#{code}l"
end
end
@doc """
Parses a mouse tracking sequence into a mouse event.
"""
@spec parse_mouse_sequence(String.t()) :: mouse_event() | nil
def parse_mouse_sequence(sequence) do
try do
case sequence do
<<27, 77, button, x, y>> ->
# Decode coordinates: they are encoded as x+32, y+32
decoded_x = x - 32
decoded_y = y - 32
parse_mouse_event(button, decoded_x, decoded_y)
<<"\e[<", rest::binary>> ->
parse_sgr_mouse_event(rest)
_ ->
nil
end
rescue
e ->
Monitor.record_error("", "Mouse sequence parse error: #{inspect(e)}", %{
sequence: sequence,
stacktrace: Exception.format_stacktrace(__STACKTRACE__)
})
nil
end
end
@doc """
Parses a focus tracking sequence into a focus event.
"""
@spec parse_focus_sequence(String.t()) :: focus_event() | nil
def parse_focus_sequence(sequence) do
try do
case sequence do
"\e[I" -> :focus_in
"\e[O" -> :focus_out
_ -> nil
end
rescue
e ->
Monitor.record_error("", "Focus sequence parse error: #{inspect(e)}", %{
sequence: sequence,
stacktrace: Exception.format_stacktrace(__STACKTRACE__)
})
nil
end
end
@doc """
Formats a mouse event into a tracking sequence.
"""
@spec format_mouse_event(mouse_event()) :: String.t()
def format_mouse_event({button, action, x, y}) do
button_code = get_mouse_button_code(button, action)
"\e[M#{button_code}#{x + 32}#{y + 32}"
end
@doc """
Formats a focus event into a tracking sequence.
"""
@spec format_focus_event(focus_event()) :: String.t()
def format_focus_event(:focus_in), do: "\e[I"
def format_focus_event(:focus_out), do: "\e[O"
defp parse_mouse_event(button_code, x, y),
do: parse_mouse_event_fallback(button_code, x, y)
defp parse_mouse_event_fallback(button_code, x, y) do
import Bitwise
# For X10 mouse tracking, the entire byte represents the action
# We need to map the button_code directly to the action
case button_code do
# Space - left button press
32 ->
{:left, :press, x, y}
# # - left button release
35 ->
{:left, :release, x, y}
# @ - mouse move
64 ->
{:left, :move, x, y}
# C - mouse drag
67 ->
{:left, :drag, x, y}
_ ->
# Fallback to the old logic for other protocols
button = Map.get(@mouse_buttons, button_code &&& 0x03)
action = Map.get(@mouse_actions, button_code)
if button && action, do: {button, action, x, y}, else: nil
end
end
defp parse_sgr_mouse_event(rest) do
rest_str =
if binary?(rest),
do: :erlang.binary_to_list(rest) |> to_string(),
else: rest
case Regex.run(~r/^([0-9]+);([0-9]+);([0-9]+)([mM])/, rest_str) do
[_, button, x, y, kind] ->
button = String.to_integer(button)
x = String.to_integer(x)
y = String.to_integer(y)
event = parse_mouse_event(button, x, y)
if kind == "m" and event, do: put_elem(event, 1, :release), else: event
_ ->
nil
end
end
defp get_mouse_button_code(button, action) do
case {button, action} do
{:left, :press} ->
0
{:left, :release} ->
3
{:left, :move} ->
32
{:left, :drag} ->
35
{:middle, :press} ->
1
{:right, :press} ->
2
{:wheel_up, :press} ->
64
{:wheel_down, :press} ->
65
_ ->
button_code = get_button_code(button)
action_code = get_action_code(action)
button_code + action_code
end
end
defp get_button_code(button) do
Enum.find_value(@mouse_buttons, 0, fn {code, b} ->
if b == button, do: code
end)
end
defp get_action_code(action) do
Enum.find_value(@mouse_actions, 0, fn {code, a} ->
if a == action, do: code
end)
end
end