lib/input_event.ex

defmodule InputEvent do
  use GenServer
  alias InputEvent.{Info, Report}

  @input_event_report 1
  @input_event_version 2
  @input_event_name 3
  @input_event_id 4
  @input_event_report_info 5
  @input_event_ready 6

  @moduledoc """
  Elixir interface to Linux input event devices
  """

  @doc """
  Start a GenServer that reports events from the specified input event device
  """
  @spec start_link(Path.t()) :: :ignore | {:error, any()} | {:ok, pid()}
  def start_link(path) do
    GenServer.start_link(__MODULE__, [path, self()])
  end

  @doc """
  Return information about this input event device
  """
  @spec info(GenServer.server()) :: Info.t()
  def info(server) do
    GenServer.call(server, :info)
  end

  @doc """
  Stop the InputEvent GenServer.
  """
  @spec stop(GenServer.server()) :: :ok
  def stop(server) do
    GenServer.stop(server)
  end

  @doc """
  Scan the system for input devices and return information on each one.
  """
  @spec enumerate() :: [{String.t(), Info.t()}]
  defdelegate enumerate(), to: InputEvent.Enumerate

  @impl GenServer
  def init([path, caller]) do
    executable = :code.priv_dir(:input_event) ++ '/input_event'

    port =
      Port.open({:spawn_executable, executable}, [
        {:args, [path]},
        {:packet, 2},
        :use_stdio,
        :binary,
        :exit_status
      ])

    state = %{port: port, path: path, info: %Info{}, callback: caller, ready: false, deferred: []}

    {:ok, state}
  end

  @impl GenServer
  def handle_call(:info, _from, %{ready: true} = state) do
    {:reply, state.info, state}
  end

  def handle_call(:info, from, state) do
    {:noreply, %{state | deferred: [from | state.deferred]}}
  end

  @impl GenServer
  def handle_info({_port, {:data, data}}, state) do
    new_state = process_notification(state, data)
    {:noreply, new_state}
  end

  def handle_info({_port, {:exit_status, _rc}}, state) do
    send(state.callback, {:input_event, state.path, :disconnect})
    {:stop, :port_crashed, state}
  end

  def handle_info(other, state) do
    IO.puts("Not expecting: #{inspect(other)}")
    send(state.callback, {:input_event, state.path, :error})
    {:stop, :error, state}
  end

  defp process_notification(state, <<@input_event_report, _sub, raw_events::binary>>) do
    Enum.each(Report.decode(raw_events), fn events ->
      send(state.callback, {:input_event, state.path, events})
    end)

    state
  end

  defp process_notification(state, <<@input_event_version, _sub, version::binary>>) do
    new_info = %{state.info | input_event_version: version}
    %{state | info: new_info}
  end

  defp process_notification(state, <<@input_event_name, _sub, name::binary>>) do
    new_info = %{state.info | name: name}
    %{state | info: new_info}
  end

  defp process_notification(
         state,
         <<@input_event_id, _sub, bus::native-16, vendor::native-16, product::native-16,
           version::native-16>>
       ) do
    new_info = %{state.info | bus: bus, vendor: vendor, product: product, version: version}
    %{state | info: new_info}
  end

  defp process_notification(state, <<@input_event_report_info, type, raw_report_info::binary>>) do
    old_report_info = state.info.report_info
    report_info = Info.decode_report_info(type, raw_report_info)
    new_info = %{state.info | report_info: [report_info | old_report_info]}
    %{state | info: new_info}
  end

  defp process_notification(state, <<@input_event_ready, _sub>>) do
    Enum.each(state.deferred, fn client -> GenServer.reply(client, state.info) end)
    %{state | ready: true, deferred: []}
  end
end