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/event_handler.ex
defmodule Raxol.Terminal.EventHandler do
@moduledoc """
Handles various terminal events including mouse, keyboard, and focus events.
This module is responsible for processing and responding to user interactions.
"""
alias Raxol.Terminal.Emulator
require Raxol.Core.Runtime.Log
@doc """
Processes a mouse event.
Returns {:ok, updated_emulator} or {:error, reason}.
"""
@spec handle_mouse_event(Emulator.t(), map()) ::
{:ok, Emulator.t()} | {:error, String.t()}
def handle_mouse_event(emulator, %{type: :mouse_press} = event) do
handle_mouse_press(emulator, event.button, event.x, event.y)
end
def handle_mouse_event(emulator, %{type: :mouse_release} = event) do
handle_mouse_release(emulator, event.button, event.x, event.y)
end
def handle_mouse_event(emulator, %{type: :mouse_move} = event) do
handle_mouse_move(emulator, event.x, event.y)
end
def handle_mouse_event(_emulator, invalid_event) do
{:error, "Invalid mouse event: #{inspect(invalid_event)}"}
end
@doc """
Processes a keyboard event.
Returns {:ok, updated_emulator} or {:error, reason}.
"""
@spec handle_keyboard_event(Emulator.t(), map()) ::
{:ok, Emulator.t()} | {:error, String.t()}
def handle_keyboard_event(emulator, %{type: :key_press} = event) do
handle_key_press(emulator, event.key, event.modifiers)
end
def handle_keyboard_event(emulator, %{type: :key_release} = event) do
handle_key_release(emulator, event.key, event.modifiers)
end
def handle_keyboard_event(_emulator, invalid_event) do
{:error, "Invalid keyboard event: #{inspect(invalid_event)}"}
end
@doc """
Processes a focus event.
Returns {:ok, updated_emulator} or {:error, reason}.
"""
@spec handle_focus_event(Emulator.t(), atom()) ::
{:ok, Emulator.t()} | {:error, String.t()}
def handle_focus_event(emulator, :focus_gain) do
handle_focus_gain(emulator)
end
def handle_focus_event(emulator, :focus_loss) do
handle_focus_loss(emulator)
end
def handle_focus_event(_emulator, invalid_event) do
{:error, "Invalid focus event: #{inspect(invalid_event)}"}
end
# Private Functions
defp handle_mouse_press(emulator, button, x, y) do
# Always handle selection for left button regardless of mouse reporting mode
emulator = handle_button_selection(emulator, button, x, y)
case Emulator.get_mode_manager(emulator).mouse_report_mode do
:none ->
{:ok, emulator}
:x10 ->
process_mouse_button_event(emulator, :press, button, x, y)
:cell_motion ->
process_mouse_any_event(emulator, :press, button, x, y)
:sgr ->
process_mouse_any_event(emulator, :press, button, x, y)
_ ->
{:ok, emulator}
end
end
defp handle_mouse_release(emulator, button, x, y) do
# Handle selection end for left button
emulator = handle_button_release(emulator, button)
case Emulator.get_mode_manager(emulator).mouse_report_mode do
:none ->
{:ok, emulator}
:x10 ->
# X10 mode only reports press events, not releases
{:ok, emulator}
:cell_motion ->
process_mouse_any_event(emulator, :release, button, x, y)
:sgr ->
process_mouse_any_event(emulator, :release, button, x, y)
_ ->
{:ok, emulator}
end
end
defp handle_mouse_move(emulator, x, y) do
# Handle selection drag if active
emulator = handle_selection_drag_if_active(emulator, x, y)
case Emulator.get_mode_manager(emulator).mouse_report_mode do
:none ->
{:ok, emulator}
:x10 ->
# X10 mode doesn't report move events
{:ok, emulator}
:cell_motion ->
process_mouse_move_event(emulator, x, y)
:sgr ->
process_mouse_move_event(emulator, x, y)
_ ->
{:ok, emulator}
end
end
defp handle_key_press(emulator, key, modifiers) do
case Emulator.get_mode_manager(emulator).keyboard_mode do
:normal ->
process_normal_key_press(emulator, key, modifiers)
:application ->
process_application_key_press(emulator, key, modifiers)
end
end
defp handle_key_release(emulator, key, modifiers) do
case Emulator.get_mode_manager(emulator).keyboard_mode do
:normal ->
process_normal_key_release(emulator, key, modifiers)
:application ->
process_application_key_release(emulator, key, modifiers)
end
end
defp handle_focus_gain(emulator) do
# Restore cursor visibility and other focus-related states
emulator = Emulator.set_cursor_visibility(emulator, true)
{:ok, emulator}
end
defp handle_focus_loss(emulator) do
# Save cursor visibility and other focus-related states
emulator = Emulator.set_cursor_visibility(emulator, false)
{:ok, emulator}
end
# Mouse Event Processing
defp process_mouse_button_event(emulator, type, button, x, y) do
# Convert mouse coordinates to terminal coordinates
{term_x, term_y} = convert_to_terminal_coordinates(emulator, x, y)
# Generate appropriate command based on event type
command = generate_mouse_command(type, button, term_x, term_y)
# Process the command - Emulator.process_input always returns {emulator, output}
{updated_emulator, _} = Emulator.process_input(emulator, command)
{:ok, updated_emulator}
end
defp process_mouse_any_event(emulator, type, button, x, y) do
# Similar to button events but with different command generation
{term_x, term_y} = convert_to_terminal_coordinates(emulator, x, y)
command = generate_mouse_any_command(type, button, term_x, term_y)
{updated_emulator, _} = Emulator.process_input(emulator, command)
{:ok, updated_emulator}
end
defp process_mouse_move_event(emulator, x, y) do
{term_x, term_y} = convert_to_terminal_coordinates(emulator, x, y)
command = generate_mouse_move_command(term_x, term_y)
{updated_emulator, _} = Emulator.process_input(emulator, command)
{:ok, updated_emulator}
end
# Keyboard Event Processing
defp process_normal_key_press(emulator, key, modifiers) do
command = generate_normal_key_command(key, modifiers)
{updated_emulator, _} = Emulator.process_input(emulator, command)
{:ok, updated_emulator}
end
defp process_application_key_press(emulator, key, modifiers) do
command = generate_application_key_command(key, modifiers)
{updated_emulator, _} = Emulator.process_input(emulator, command)
{:ok, updated_emulator}
end
defp process_normal_key_release(emulator, _key, _modifiers) do
{:ok, emulator}
end
defp process_application_key_release(emulator, _key, _modifiers) do
{:ok, emulator}
end
# Helper Functions
defp convert_to_terminal_coordinates(_emulator, x, y) do
# Convert pixel coordinates to cell coordinates
# For now, assume direct mapping - this could be enhanced based on font metrics
{x, y}
end
defp generate_mouse_command(type, button, x, y) do
# Generate standard mouse command sequence
# Format: ESC [ M <button> <x+32> <y+32>
button_code = encode_mouse_button(button, type)
<<27, "[M", button_code, x + 32, y + 32>>
end
defp generate_mouse_any_command(type, button, x, y) do
# For "any event" mode, use SGR format which is more reliable
# Format: ESC [ < <button> ; <x> ; <y> <M or m>
button_code = encode_sgr_button(button, type)
suffix = if type == :press, do: "M", else: "m"
"\e[<#{button_code};#{x};#{y}#{suffix}"
end
defp generate_mouse_move_command(x, y) do
# Mouse move events typically use button code 32 (motion)
<<27, "[M", 32, x + 32, y + 32>>
end
# Mouse button encoding for standard format (X10 mode only reports press events)
defp encode_mouse_button(button, :press) do
case button do
:left -> 0
:middle -> 1
:right -> 2
_ -> 0
end
end
# Mouse button encoding for SGR format
defp encode_sgr_button(button, _type) do
case button do
:left -> 0
:middle -> 1
:right -> 2
_ -> 0
end
end
defp generate_normal_key_command(_key, _modifiers) do
""
end
# Mouse selection handling functions
defp handle_mouse_selection_start(emulator, x, y) do
alias Raxol.Terminal.Selection.Manager
# Initialize selection manager if not present
selection_manager = emulator.selection_manager || Manager.new()
# Convert screen coordinates to terminal coordinates
{term_x, term_y} = convert_to_terminal_coordinates(emulator, x, y)
# Start new selection
updated_selection_manager =
Manager.start_selection(selection_manager, {term_x, term_y})
%{emulator | selection_manager: updated_selection_manager}
end
defp handle_mouse_selection_drag(emulator, x, y) do
alias Raxol.Terminal.Selection.Manager
case {emulator.selection_manager,
emulator.selection_manager && emulator.selection_manager.active} do
{nil, _} ->
emulator
{_manager, false} ->
emulator
{manager, true} ->
# Convert screen coordinates to terminal coordinates
{term_x, term_y} = convert_to_terminal_coordinates(emulator, x, y)
# Update selection end position
updated_selection_manager =
Manager.update_selection(manager, {term_x, term_y})
%{emulator | selection_manager: updated_selection_manager}
end
end
defp handle_mouse_selection_end(emulator) do
# Selection remains active until explicitly cleared
# This allows for copy operations after selection is made
emulator
end
defp generate_application_key_command(_key, _modifiers) do
""
end
# Helper functions for button handling
defp handle_button_selection(emulator, :left, x, y) do
handle_mouse_selection_start(emulator, x, y)
end
defp handle_button_selection(emulator, _button, _x, _y) do
emulator
end
defp handle_button_release(emulator, :left) do
handle_mouse_selection_end(emulator)
end
defp handle_button_release(emulator, _button) do
emulator
end
defp handle_selection_drag_if_active(emulator, x, y) do
case {emulator.selection_manager,
emulator.selection_manager && emulator.selection_manager.active} do
{nil, _} -> emulator
{_manager, false} -> emulator
{_manager, true} -> handle_mouse_selection_drag(emulator, x, y)
end
end
end