RasterExRatatui.Input.Evdev (RasterExRatatui v0.4.0)

Copy Markdown View Source

Translates Linux evdev keyboard events into ExRatatui.Event.Key structs.

A terminal hands an app finished key events: "A" with ["shift"], "back_tab", "c" with ["ctrl"]. A USB keyboard read through evdev (with the input_event library, for instance) only reports physical keys going down, repeating, and coming up, so something has to remember which modifiers are held and which character a key makes. This module is that something: pure, no process, no dependency on input_event.

It consumes the {type, code, value} tuples input_event delivers in {:input_event, path, events} messages and keeps only :ev_key ones:

{:ok, _pid} = InputEvent.start_link(path: "/dev/input/event0", grab: true)
keyboard = Evdev.new()

# in the process receiving the messages (a surface's handle_info/2):
def handle_info({:input_event, _path, events}, %{keyboard: keyboard} = state) do
  {keyboard, keys} = Evdev.translate_all(keyboard, events)
  {:events, keys, %{state | keyboard: keyboard}}
end

Keys come out the way crossterm reports them in a terminal, so apps behave the same on both: character keys carry the character they type ("A" when shifted or under caps lock, "!" for shift+1) with "shift", "ctrl", "alt", "super" modifiers in that order; special keys use the codes listed in ExRatatui.Event.Key; shift+tab is "back_tab". Modifier keys alone produce no events.

Layouts

The built-in character table is a US layout. Pass layout: a map of key_code => {unshifted, shifted} to override or add keys (a German layout swaps :key_y and :key_z, for example).

Examples

iex> alias RasterExRatatui.Input.Evdev
iex> {keyboard, []} = Evdev.translate(Evdev.new(), {:ev_key, :key_leftshift, 1})
iex> {_keyboard, [key]} = Evdev.translate(keyboard, {:ev_key, :key_1, 1})
iex> key
%ExRatatui.Event.Key{code: "!", kind: "press", modifiers: ["shift"]}

Summary

Functions

A keyboard with no keys held and caps lock off.

Translates one evdev event. Events other than :ev_key (:ev_syn, :ev_msc, …) and keys with no mapping produce no keys.

Translates a list of evdev events in order, as delivered in one {:input_event, path, events} message.

Types

t()

@type t() :: %RasterExRatatui.Input.Evdev{
  caps_lock: boolean(),
  chars: %{required(atom()) => {String.t(), String.t()}},
  emit_release: boolean(),
  held: MapSet.t(atom())
}

Functions

new(opts \\ [])

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

A keyboard with no keys held and caps lock off.

Options

  • :layout — :us (default) or a map of key_code => {unshifted, shifted} merged over the US table
  • :emit_release — also emit kind: "release" events (default false, like a terminal without keyboard enhancement)

Examples

iex> RasterExRatatui.Input.Evdev.new(layout: %{key_z: {"y", "Y"}}).chars.key_z
{"y", "Y"}

translate(keyboard, arg2)

@spec translate(t(), {atom(), atom(), integer()}) :: {t(), [ExRatatui.Event.Key.t()]}

Translates one evdev event. Events other than :ev_key (:ev_syn, :ev_msc, …) and keys with no mapping produce no keys.

Examples

iex> alias RasterExRatatui.Input.Evdev
iex> {_keyboard, keys} = Evdev.translate(Evdev.new(), {:ev_key, :key_enter, 1})
iex> keys
[%ExRatatui.Event.Key{code: "enter", kind: "press", modifiers: []}]

iex> alias RasterExRatatui.Input.Evdev
iex> Evdev.translate(Evdev.new(), {:ev_syn, :syn_report, 0}) |> elem(1)
[]

translate_all(keyboard, events)

@spec translate_all(t(), [{atom(), atom(), integer()}] | :disconnect) ::
  {t(), [ExRatatui.Event.Key.t()]}

Translates a list of evdev events in order, as delivered in one {:input_event, path, events} message.

input_event sends :disconnect in place of the list when the device goes away (unplugged, or grabbed by another reader). That releases every held modifier and produces no keys; the consumer is the one to look for a keyboard again.

Examples

iex> alias RasterExRatatui.Input.Evdev
iex> events = [{:ev_key, :key_leftctrl, 1}, {:ev_msc, :msc_scan, 29}, {:ev_key, :key_c, 1}, {:ev_syn, :syn_report, 0}]
iex> {_keyboard, keys} = Evdev.translate_all(Evdev.new(), events)
iex> keys
[%ExRatatui.Event.Key{code: "c", kind: "press", modifiers: ["ctrl"]}]
iex> {keyboard, []} = Evdev.translate_all(Evdev.new(), [{:ev_key, :key_leftshift, 1}])
iex> {keyboard, []} = Evdev.translate_all(keyboard, :disconnect)
iex> MapSet.size(keyboard.held)
0