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/mouse/mouse_server.ex
defmodule Raxol.Terminal.Mouse.MouseServer do
@moduledoc """
Provides unified mouse handling functionality for the terminal emulator.
This module handles mouse events, tracking, and state management.
"""
use Raxol.Core.Behaviours.BaseManager
require Logger
# Types
@type mouse_id :: non_neg_integer()
@type mouse_button :: :left | :middle | :right | :wheel_up | :wheel_down
@type mouse_event ::
:press | :release | :move | :drag | :click | :double_click
@type mouse_modifier :: :shift | :ctrl | :alt | :meta
@type mouse_config :: %{
optional(:tracking) => boolean(),
optional(:reporting) => boolean(),
optional(:sgr_mode) => boolean(),
optional(:urxvt_mode) => boolean(),
optional(:pixel_mode) => boolean()
}
# Client API
# BaseManager provides start_link/1 automatically with name: __MODULE__ as default
@doc """
Creates a new mouse context with the given configuration.
"""
@spec create_mouse(map()) :: {:ok, mouse_id()} | {:error, term()}
def create_mouse(config \\ %{}) do
GenServer.call(__MODULE__, {:create_mouse, config})
end
@doc """
Gets the list of all mouse contexts.
"""
@spec get_mice() :: list(mouse_id())
def get_mice do
GenServer.call(__MODULE__, :get_mice)
end
@doc """
Gets the active mouse context ID.
"""
@spec get_active_mouse() :: {:ok, mouse_id()} | {:error, :no_active_mouse}
def get_active_mouse do
GenServer.call(__MODULE__, :get_active_mouse)
end
@doc """
Sets the active mouse context.
"""
@spec set_active_mouse(mouse_id()) :: :ok | {:error, term()}
def set_active_mouse(mouse_id) do
GenServer.call(__MODULE__, {:set_active_mouse, mouse_id})
end
@doc """
Gets the state of a specific mouse context.
"""
@spec get_mouse_state(mouse_id()) :: {:ok, map()} | {:error, term()}
def get_mouse_state(mouse_id) do
GenServer.call(__MODULE__, {:get_mouse_state, mouse_id})
end
@doc """
Updates the configuration of a specific mouse context.
"""
@spec update_mouse_config(mouse_id(), mouse_config()) ::
:ok | {:error, term()}
def update_mouse_config(mouse_id, config) do
GenServer.call(__MODULE__, {:update_mouse_config, mouse_id, config})
end
@doc """
Processes a mouse event.
"""
@spec process_mouse_event(
mouse_id(),
mouse_event(),
mouse_button(),
{integer(), integer()},
list(mouse_modifier())
) :: :ok | {:error, term()}
def process_mouse_event(mouse_id, event, button, position, modifiers) do
GenServer.call(
__MODULE__,
{:process_mouse_event, mouse_id, event, button, position, modifiers}
)
end
@doc """
Processes a mouse event from an event map.
The event map should contain: button, action, modifiers, x, y
"""
@spec process_mouse_event(mouse_id(), map()) :: :ok | {:error, term()}
def process_mouse_event(mouse_id, %{} = event) do
button = Map.get(event, :button, :left)
action = Map.get(event, :action, :press)
modifiers = Map.get(event, :modifiers, [])
x = Map.get(event, :x, 0)
y = Map.get(event, :y, 0)
position = {x, y}
process_mouse_event(mouse_id, action, button, position, modifiers)
end
@doc """
Gets the current mouse position.
"""
@spec get_mouse_position(mouse_id()) ::
{:ok, {integer(), integer()}} | {:error, term()}
def get_mouse_position(mouse_id) do
GenServer.call(__MODULE__, {:get_mouse_position, mouse_id})
end
@doc """
Gets the current mouse button state.
"""
@spec get_mouse_button_state(mouse_id()) ::
{:ok, list(mouse_button())} | {:error, term()}
def get_mouse_button_state(mouse_id) do
GenServer.call(__MODULE__, {:get_mouse_button_state, mouse_id})
end
@doc """
Closes a mouse context.
"""
@spec close_mouse(mouse_id()) :: :ok | {:error, term()}
def close_mouse(mouse_id) do
GenServer.call(__MODULE__, {:close_mouse, mouse_id})
end
@doc """
Updates the mouse manager configuration.
"""
@spec update_config(map()) :: :ok
def update_config(config) do
GenServer.call(__MODULE__, {:update_config, config})
end
@doc """
Cleans up resources.
"""
@spec cleanup() :: :ok
def cleanup do
GenServer.call(__MODULE__, :cleanup)
end
# Server Callbacks
@impl true
def init_manager(opts) do
# Convert opts to map if it's a keyword list
opts_map = if is_list(opts), do: Map.new(opts), else: opts
state = %{
mice: %{},
active_mouse: nil,
next_id: 1,
config: Map.merge(default_config(), opts_map)
}
{:ok, state}
end
@impl true
def handle_manager_call({:create_mouse, config}, _from, state) do
mouse_id = state.next_id
mouse_state = %{
id: mouse_id,
config: Map.merge(default_mouse_config(), config),
position: {0, 0},
button_state: %{},
modifiers: [],
created_at: System.system_time(:millisecond),
last_update: System.system_time(:millisecond)
}
new_state = %{
state
| mice: Map.put(state.mice, mouse_id, mouse_state),
next_id: mouse_id + 1
}
# If this is the first mouse context, make it active
new_state =
set_active_if_first_mouse(state.active_mouse == nil, new_state, mouse_id)
{:reply, {:ok, mouse_id}, new_state}
end
@impl true
def handle_manager_call(:get_mice, _from, state) do
{:reply, Map.keys(state.mice), state}
end
@impl true
def handle_manager_call(:get_active_mouse, _from, state) do
case state.active_mouse do
nil -> {:reply, {:error, :no_active_mouse}, state}
mouse_id -> {:reply, {:ok, mouse_id}, state}
end
end
@impl true
def handle_manager_call({:set_active_mouse, mouse_id}, _from, state) do
case Map.get(state.mice, mouse_id) do
nil ->
{:reply, {:error, :mouse_not_found}, state}
_mouse ->
new_state = %{state | active_mouse: mouse_id}
{:reply, :ok, new_state}
end
end
@impl true
def handle_manager_call({:get_mouse_state, mouse_id}, _from, state) do
case Map.get(state.mice, mouse_id) do
nil -> {:reply, {:error, :mouse_not_found}, state}
mouse_state -> {:reply, {:ok, mouse_state}, state}
end
end
@impl true
def handle_manager_call(
{:update_mouse_config, mouse_id, config},
_from,
state
) do
case Map.get(state.mice, mouse_id) do
nil ->
{:reply, {:error, :mouse_not_found}, state}
mouse_state ->
new_config = Map.merge(mouse_state.config, config)
new_mouse_state = %{mouse_state | config: new_config}
new_state = %{
state
| mice: Map.put(state.mice, mouse_id, new_mouse_state)
}
{:reply, :ok, new_state}
end
end
@impl true
def handle_manager_call(
{:process_mouse_event, mouse_id, event, button, position, modifiers},
_from,
state
) do
case Map.get(state.mice, mouse_id) do
nil ->
{:reply, {:error, :mouse_not_found}, state}
mouse_state ->
new_mouse_state =
process_event(mouse_state, event, button, position, modifiers)
new_state = %{
state
| mice: Map.put(state.mice, mouse_id, new_mouse_state)
}
{:reply, :ok, new_state}
end
end
@impl true
def handle_manager_call({:get_mouse_position, mouse_id}, _from, state) do
case Map.get(state.mice, mouse_id) do
nil -> {:reply, {:error, :mouse_not_found}, state}
mouse_state -> {:reply, {:ok, mouse_state.position}, state}
end
end
@impl true
def handle_manager_call({:get_mouse_button_state, mouse_id}, _from, state) do
case Map.get(state.mice, mouse_id) do
nil -> {:reply, {:error, :mouse_not_found}, state}
mouse_state -> {:reply, {:ok, mouse_state.button_state}, state}
end
end
@impl true
def handle_manager_call({:close_mouse, mouse_id}, _from, state) do
case Map.get(state.mice, mouse_id) do
nil ->
{:reply, {:error, :mouse_not_found}, state}
_mouse ->
# Remove mouse context
new_mice = Map.delete(state.mice, mouse_id)
new_active_mouse =
update_active_mouse(state.active_mouse, mouse_id, new_mice)
new_state = %{state | mice: new_mice, active_mouse: new_active_mouse}
{:reply, :ok, new_state}
end
end
@impl true
def handle_manager_call({:update_config, config}, _from, state) do
new_config = Map.merge(state.config, config)
new_state = %{state | config: new_config}
{:reply, :ok, new_state}
end
@impl true
def handle_manager_call(:cleanup, _from, state) do
# Clean up all mouse contexts
{:reply, :ok, %{state | mice: %{}, active_mouse: nil}}
end
# Private Functions
defp set_active_if_first_mouse(false, new_state, _mouse_id), do: new_state
defp set_active_if_first_mouse(true, new_state, mouse_id) do
%{new_state | active_mouse: mouse_id}
end
defp update_active_mouse_conditional(false, active_mouse, _new_mice),
do: active_mouse
defp update_active_mouse_conditional(true, _active_mouse, new_mice) do
case Map.keys(new_mice) do
[] -> nil
[first_mouse | _] -> first_mouse
end
end
defp handle_press_if_not_pressed(
true,
mouse_state,
_button,
_position,
_modifiers
) do
mouse_state
end
defp handle_press_if_not_pressed(
false,
mouse_state,
button,
position,
modifiers
) do
update_mouse_state(
mouse_state,
position,
modifiers,
Map.put(mouse_state.button_state, button, :pressed)
)
end
defp handle_release_if_pressed(
false,
mouse_state,
_button,
_position,
_modifiers
) do
mouse_state
end
defp handle_release_if_pressed(true, mouse_state, button, position, modifiers) do
update_mouse_state(
mouse_state,
position,
modifiers,
Map.delete(mouse_state.button_state, button)
)
end
defp handle_move_if_position_changed(
false,
mouse_state,
_position,
_modifiers
) do
mouse_state
end
defp handle_move_if_position_changed(true, mouse_state, position, modifiers) do
update_mouse_state(
mouse_state,
position,
modifiers,
mouse_state.button_state
)
end
defp handle_drag_if_position_changed(
false,
mouse_state,
_position,
_modifiers
) do
mouse_state
end
defp handle_drag_if_position_changed(true, mouse_state, position, modifiers) do
update_mouse_state(
mouse_state,
position,
modifiers,
mouse_state.button_state
)
end
defp update_active_mouse(active_mouse, closed_mouse_id, new_mice) do
update_active_mouse_conditional(
active_mouse == closed_mouse_id,
active_mouse,
new_mice
)
end
defp process_event(mouse_state, event, button, position, modifiers) do
case event do
:press -> handle_press_event(mouse_state, button, position, modifiers)
:release -> handle_release_event(mouse_state, button, position, modifiers)
:move -> handle_move_event(mouse_state, position, modifiers)
:drag -> handle_drag_event(mouse_state, position, modifiers)
_ -> handle_other_event(mouse_state, position, modifiers)
end
end
defp handle_press_event(mouse_state, button, position, modifiers) do
handle_press_if_not_pressed(
Map.has_key?(mouse_state.button_state, button),
mouse_state,
button,
position,
modifiers
)
end
defp handle_release_event(mouse_state, button, position, modifiers) do
handle_release_if_pressed(
Map.has_key?(mouse_state.button_state, button),
mouse_state,
button,
position,
modifiers
)
end
defp handle_move_event(mouse_state, position, modifiers) do
handle_move_if_position_changed(
position != mouse_state.position,
mouse_state,
position,
modifiers
)
end
defp handle_drag_event(mouse_state, position, modifiers) do
handle_drag_if_position_changed(
position != mouse_state.position,
mouse_state,
position,
modifiers
)
end
defp handle_other_event(mouse_state, position, modifiers) do
update_mouse_state(
mouse_state,
position,
modifiers,
mouse_state.button_state
)
end
defp update_mouse_state(mouse_state, position, modifiers, button_state) do
%{
mouse_state
| position: position,
button_state: button_state,
modifiers: modifiers,
last_update: System.system_time(:millisecond)
}
end
defp default_config do
%{
max_mice: 1,
default_tracking: true,
default_reporting: true,
default_sgr_mode: true,
default_urxvt_mode: false,
default_pixel_mode: false
}
end
defp default_mouse_config do
%{
tracking: :all,
reporting: true,
sgr_mode: true,
urxvt_mode: false,
pixel_mode: false
}
end
end