# `RasterExRatatui.Input.Devices`
[🔗](https://github.com/mcass19/raster_ex_ratatui/blob/v0.4.0/lib/raster_ex_ratatui/input/devices.ex#L1)

Finds and reads the evdev input devices a panel comes with, and finds them again when they go away.

A keyboard on a Nerves device is `/dev/input/eventN` for some N that changes with what else is plugged in, may not exist at boot, and goes away when unplugged; a touch panel is another such device. This module owns both: which device to read, starting an `input_event` reader on it with the device grabbed (so keystrokes do not also reach the kernel console), translating what it delivers into `ExRatatui.Event` structs (`RasterExRatatui.Input.Evdev` for keys, `RasterExRatatui.Input.Touch` for fingers), and looking again, every `retry_ms:`, whenever there is nothing to read. It has no process of its own: the caller keeps the struct, hands it every message it receives, and gets events back, from a surface's `c:RasterExRatatui.Surface.handle_info/2` or from any process that traps exits.

    defmodule Kiosk.Surface do
      use RasterExRatatui.Surface, app: Kiosk.Dashboard, format: RasterExRatatui.PixelFormat.RGB565

      alias RasterExRatatui.Input.Devices

      @impl true
      def init(_opts) do
        {:ok, devices} = Devices.start(Devices.new(keyboard: true))
        {:ok, [size: {480, 320}], devices}
      end

      @impl true
      def push(patches, devices), do: write(patches) && devices

      @impl true
      def handle_info(msg, devices) do
        case Devices.handle_info(msg, devices) do
          {:events, keys, devices} -> {:events, keys, devices}
          {:noreply, devices} -> {:noreply, devices}
          :unknown -> {:noreply, devices}
        end
      end

      @impl true
      def terminate(_reason, devices), do: Devices.stop(devices)
    end

## `input_event` is the consumer's dependency

The reader is [`input_event`](https://hex.pm/packages/input_event), a C port that only builds on Linux, so this library does not depend on it: the consumer adds `{:input_event, "~> 1.4"}` to its own deps. `start/1` checks that the module is there and returns `{:error, :input_event_missing}` otherwise, so a host build without it still runs, without input. `input:` swaps the module for a stub in tests.

## Options

`new/1` takes:

  * `:keyboard` — `true` (default) to read the first device that reports letter keys, a `"/dev/input/eventN"` path to read that one, or `false`
  * `:touch` — `true` to read the first touch panel found (`touch_path/1`), a path, or `false` (default). Its taps and drags come back as `ExRatatui.Event.Mouse` events on the cell under the finger, which takes the two options below
  * `:size` — the physical panel in pixels, for the touch axes (required with `:touch`)
  * `:cell_at` — `({px, py} -> {col, row} | :outside)`, normally `RasterExRatatui.Raster.cell_at/2` of the raster the panel shows (required with `:touch`)
  * `:swap_xy`, `:invert_x`, `:invert_y` — for a touch controller that does not follow the panel's orientation, see `RasterExRatatui.Input.Touch`
  * `:retry_ms` — how long to wait before looking for a missing device again (default `2_000`)
  * `:input` — the module standing in for `InputEvent` (default `InputEvent`)
  * `:layout`, `:emit_release` — passed to `RasterExRatatui.Input.Evdev.new/1`

## Messages

`start/1` sends the calling process a first `{RasterExRatatui.Input.Devices, :scan}`; `handle_info/2` handles it and every later one, the `{:input_event, path, events}` and `{:input_event, path, :disconnect}` messages of the readers it started, and their `{:EXIT, pid, reason}` (a reader stops when its device goes away, and the search starts over). Everything else is `:unknown`, including the `:normal` exits of the short-lived readers that `InputEvent.enumerate/0` starts from the caller while scanning.

# `device`

```elixir
@type device() :: {path :: String.t(), %{report_info: [{atom(), [term()]}]}}
```

# `t`

```elixir
@type t() :: %RasterExRatatui.Input.Devices{
  evdev: RasterExRatatui.Input.Evdev.t(),
  evdev_opts: keyword(),
  input: module(),
  keyboard: boolean() | String.t(),
  keyboard_path: String.t() | nil,
  keyboard_reader: pid() | nil,
  retry_ms: non_neg_integer(),
  timer: reference() | nil,
  touch: boolean() | String.t(),
  touch_opts: keyword(),
  touch_path: String.t() | nil,
  touch_reader: pid() | nil,
  touch_state: RasterExRatatui.Input.Touch.t() | nil
}
```

# `handle_info`

```elixir
@spec handle_info(term(), t()) ::
  {:events, [ExRatatui.Event.t()], t()} | {:noreply, t()} | :unknown
```

Handles a message from the calling process's mailbox.

Returns `{:events, events, devices}` with the `ExRatatui.Event` structs a reader's message translated to, `{:noreply, devices}` for the other messages of this module, or `:unknown`.

# `keyboard`

```elixir
@spec keyboard(t()) :: String.t() | nil
```

The path of the keyboard being read, or `nil` while there is none.

# `keyboard_path`

```elixir
@spec keyboard_path([device()]) :: String.t() | nil
```

The path of the first device in an `InputEvent.enumerate/0` list that reports letter keys, or `nil`. Mice, touch panels, and power buttons report `:ev_key` too, but not `:key_a`.

## Examples

    iex> RasterExRatatui.Input.Devices.keyboard_path([
    ...>   {"/dev/input/event0", %{report_info: [ev_key: [:btn_touch], ev_abs: [abs_x: %{}]]}},
    ...>   {"/dev/input/event1", %{report_info: [ev_key: [:key_esc, :key_a, :key_b], ev_led: [:led_capsl]]}}
    ...> ])
    "/dev/input/event1"

    iex> RasterExRatatui.Input.Devices.keyboard_path([])
    nil

# `new`

```elixir
@spec new(keyword()) :: t()
```

Builds the state from the options in the moduledoc. Raises `ArgumentError` on a bad option, or on `:touch` without `:size` and `:cell_at`.

## Examples

    iex> devices = RasterExRatatui.Input.Devices.new(keyboard: "/dev/input/event3", retry_ms: 500)
    iex> {devices.keyboard, devices.retry_ms, RasterExRatatui.Input.Devices.keyboard(devices)}
    {"/dev/input/event3", 500, nil}

# `start`

```elixir
@spec start(t()) :: {:ok, t()} | {:error, :input_event_missing}
```

Starts looking for the devices: the first scan arrives as a message to the calling process, which must trap exits (the readers are linked).

Returns `{:ok, devices}`, or `{:error, :input_event_missing}` when a device is wanted and the `input:` module is not available.

# `stop`

```elixir
@spec stop(t()) :: t()
```

Stops the readers that are running and any pending scan. Returns the devices, idle; `start/1` starts them again.

# `touch`

```elixir
@spec touch(t()) :: String.t() | nil
```

The path of the touch panel being read, or `nil` while there is none.

# `touch_axes`

```elixir
@spec touch_axes(%{report_info: [{atom(), [term()]}]}) ::
  RasterExRatatui.Input.Touch.axes() | nil
```

The ranges of a touch panel's axes, `%{x: {min, max}, y: {min, max}}`, from a device's `report_info` (the multitouch axes when present, else the single-touch ones), or `nil` when the device has no such axes.

## Examples

    iex> RasterExRatatui.Input.Devices.touch_axes(%{
    ...>   report_info: [
    ...>     ev_key: [:btn_touch],
    ...>     ev_abs: [
    ...>       abs_x: %{min: 0, max: 719},
    ...>       abs_y: %{min: 0, max: 1279},
    ...>       abs_mt_position_x: %{min: 0, max: 719},
    ...>       abs_mt_position_y: %{min: 0, max: 1279}
    ...>     ]
    ...>   ]
    ...> })
    %{x: {0, 719}, y: {0, 1279}}

    iex> RasterExRatatui.Input.Devices.touch_axes(%{report_info: [ev_key: [:key_a]]})
    nil

# `touch_path`

```elixir
@spec touch_path([device()]) :: String.t() | nil
```

The path of the first device in an `InputEvent.enumerate/0` list that is a touch panel: one reporting the multitouch `:abs_mt_position_x` axis, or else `:abs_x` together with `:btn_touch`. `nil` when there is none.

## Examples

    iex> RasterExRatatui.Input.Devices.touch_path([
    ...>   {"/dev/input/event1", %{report_info: [ev_key: [:key_a]]}},
    ...>   {"/dev/input/event2", %{report_info: [ev_key: [:btn_left], ev_abs: [abs_x: %{}, abs_y: %{}]]}},
    ...>   {"/dev/input/event3", %{report_info: [ev_key: [:btn_touch], ev_abs: [abs_x: %{}, abs_y: %{}, abs_mt_position_x: %{}]]}}
    ...> ])
    "/dev/input/event3"

    iex> RasterExRatatui.Input.Devices.touch_path([
    ...>   {"/dev/input/event4", %{report_info: [ev_key: [:btn_touch], ev_abs: [abs_x: %{}, abs_y: %{}]]}}
    ...> ])
    "/dev/input/event4"

    iex> RasterExRatatui.Input.Devices.touch_path([])
    nil

---

*Consult [api-reference.md](api-reference.md) for complete listing*
