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