defmodule Raxol.Terminal.Input do @moduledoc """ Handles input processing for the terminal. """ defstruct [ :buffer, :state, :last_click, :last_drag, :last_release, :completion_callback, :completion_options, :completion_index ] @type completion_callback :: (String.t() -> [String.t()]) @type t :: %__MODULE__{ buffer: list(), state: atom(), last_click: {integer(), integer(), atom()} | nil, last_drag: {integer(), integer(), atom()} | nil, last_release: {integer(), integer(), atom()} | nil, completion_callback: completion_callback() | nil, completion_options: [String.t()], completion_index: non_neg_integer() } @doc """ Creates a new input handler. """ def new do %__MODULE__{ buffer: [], state: :normal, last_click: nil, last_drag: nil, last_release: nil, completion_callback: nil, completion_options: [], completion_index: 0 } end @doc """ Handles a mouse click event. """ def handle_click(input, x, y, button) do %{input | last_click: {x, y, button}} end @doc """ Handles a mouse drag event. """ def handle_drag(input, x, y, button) do %{input | last_drag: {x, y, button}} end @doc """ Handles a mouse release event. """ def handle_release(input, x, y, button) do %{input | last_release: {x, y, button}} end @doc """ Performs tab completion on the input buffer. Uses the completion_callback to find matches and cycles through them. """ def tab_complete(%__MODULE__{completion_callback: nil} = input) do # No completion callback set, return unchanged input end def tab_complete(%__MODULE__{completion_callback: callback} = input) do # Convert buffer to string for completion buffer_string = buffer_to_string(input.buffer) # Ensure completion_options is a list completion_options = input.completion_options || [] case completion_options == [] do true -> # First tab - get initial completions options = callback.(buffer_string) case options do [] -> # No matches, return unchanged input [single_match] -> # Single match, complete immediately and clear options %{ input | buffer: string_to_buffer(single_match), completion_options: [], completion_index: 0 } multiple_matches -> # Multiple matches, set up for cycling first_match = Enum.at(multiple_matches, 0) %{ input | buffer: string_to_buffer(first_match), completion_options: multiple_matches, completion_index: 0 } end false -> # Subsequent tabs - cycle through existing options next_index = rem(input.completion_index + 1, length(completion_options)) next_match = Enum.at(completion_options, next_index) %{ input | buffer: string_to_buffer(next_match), completion_index: next_index } end end @doc """ Clears completion state. Should be called when input changes other than tab completion. """ def clear_completion(%__MODULE__{} = input) do %{input | completion_options: [], completion_index: 0} end @doc """ Example completion callback that provides Elixir keywords. """ def example_completion_callback(prefix) do elixir_keywords = [ "def", "defp", "defmodule", "defstruct", "defprotocol", "defimpl", "do", "end", "if", "unless", "when", "case", "cond", "with", "try", "catch", "rescue", "after", "receive", "for", "import", "alias", "require", "use", "quote", "unquote", "spawn", "send", "fn", "->", "true", "false", "nil" ] elixir_keywords |> Enum.filter(&String.starts_with?(&1, prefix)) |> Enum.sort() end # Helper functions defp buffer_to_string(buffer) when is_list(buffer) do buffer |> Enum.join("") end defp string_to_buffer(string) when is_binary(string) do String.graphemes(string) end end