defmodule KeyboardLayout do
@moduledoc """
Describes a keyboard layout.
The layout can be created dynamically, or it can be predefined in the `Config`
for the application.
Example of a layout defined in `Config`:
import Config
config :keyboard_layout,
layout: [
leds: [
%{id: :l1, x: 0, y: 0},
%{id: :l2, x: 2, y: 1.5},
%{id: :l3, x: 3, y: 3}
],
keys: [
%{id: :k1, x: 0, y: 0, opts: [led: :l1]},
%{id: :k2, x: 2, y: 1.5, opts: [width: 1.5, height: 2, led: :l2]},
%{id: :k3, x: 5, y: 0}
]
]
"""
alias __MODULE__.{Key, LED}
@typedoc """
A keyboard layout consisting of [keys](`t:KeyboardLayout.Key.t/0`) and optional
[LEDs](`t:KeyboardLayout.LED.t/0`).
"""
@type t :: %__MODULE__{
keys: [Key.t()],
leds: [LED.t()],
leds_by_keys: %{Key.id() => LED.t()},
keys_by_leds: %{LED.id() => Key.t()}
}
defstruct [:keys, :leds, :leds_by_keys, :keys_by_leds]
@doc """
Creates a new [KeyboardLayout](`t:KeyboardLayout.t/0`) from a list of keys and
LEDs. LEDs are optional.
Example:
iex> keys = [KeyboardLayout.Key.new(:k1, 0, 0, led: :l1)]
[%KeyboardLayout.Key{height: 1, id: :k1, led: :l1, width: 1, x: 0, y: 0}]
iex> leds = [KeyboardLayout.LED.new(:l1, 0, 0)]
[%KeyboardLayout.LED{id: :l1, x: 0, y: 0}]
iex> KeyboardLayout.new(keys, leds)
%KeyboardLayout{
keys: [
%KeyboardLayout.Key{height: 1, id: :k1, led: :l1, width: 1, x: 0, y: 0}
],
keys_by_leds: %{
l1: %KeyboardLayout.Key{height: 1, id: :k1, led: :l1, width: 1, x: 0, y: 0}
},
leds: [
%KeyboardLayout.LED{id: :l1, x: 0, y: 0}
],
leds_by_keys: %{
k1: %KeyboardLayout.LED{id: :l1, x: 0, y: 0}
}
}
"""
@spec new(keys :: [Key.t()], leds :: [LED.t()]) :: t
def new(keys, leds \\ []) do
leds_map = Map.new(leds, &{&1.id, &1})
leds_by_keys =
keys
|> Enum.filter(& &1.led)
|> Map.new(&{&1.id, Map.fetch!(leds_map, &1.led)})
keys_by_leds =
keys
|> Enum.filter(& &1.led)
|> Map.new(&{&1.led, &1})
%__MODULE__{
keys: keys,
leds: leds,
leds_by_keys: leds_by_keys,
keys_by_leds: keys_by_leds
}
end
@doc """
Returns a list of [keys](`t:KeyboardLayout.Key.t/0`) from the provided [layout](`t:KeyboardLayout.t/0`)
"""
@spec keys(layout :: t) :: [Key.t()]
def keys(layout), do: layout.keys
@doc """
Returns a list of [leds](`t:KeyboardLayout.LED.t/0`) from the provided [layout](`t:KeyboardLayout.t/0`)
"""
@spec leds(layout :: t) :: [LED.t()]
def leds(layout), do: layout.leds
@doc """
Returns the corresponding [LED](`t:KeyboardLayout.LED.t/0`) from the provided [layout](`t:KeyboardLayout.t/0`)
and [key id](`t:KeyboardLayout.Key.id/0`).
Returns `nil` if the LED does not belong to a key.
"""
@spec led_for_key(layout :: t, Key.id()) :: LED.t() | nil
def led_for_key(%__MODULE__{} = layout, key_id) when is_atom(key_id),
do: Map.get(layout.leds_by_keys, key_id)
@doc """
Returns the corresponding [key](`t:KeyboardLayout.Key.t/0`) from the provided [layout](`t:KeyboardLayout.t/0`)
and [LED id](`t:KeyboardLayout.LED.id/0`).
Returns `nil` if the key has no LED.
"""
@spec key_for_led(layout :: t, LED.id()) :: Key.t() | nil
def key_for_led(%__MODULE__{} = layout, led_id) when is_atom(led_id),
do: Map.get(layout.keys_by_leds, led_id)
@doc """
Returns the [layout](`t:KeyboardLayout.t/0`) defined in the `Config` of the application
"""
@spec load_from_config() :: t
def load_from_config do
env_layout =
case Application.get_env(:keyboard_layout, :layout) do
nil -> raise "A layout must be defined for the application to function"
layout -> layout
end
keys = build_keys(Keyword.get(env_layout, :keys, []))
leds = build_leds(Keyword.get(env_layout, :leds, []))
new(keys, leds)
end
@spec build_leds([map]) :: [LED.t()]
defp build_leds(led_list) do
led_list
|> Enum.map(fn %{id: id, x: x, y: y} ->
LED.new(id, x, y)
end)
end
@spec build_keys([map]) :: [Key.t()]
defp build_keys(key_list) do
key_list
|> Enum.map(fn
%{id: id, x: x, y: y, opts: opts} ->
Key.new(id, x, y, opts)
%{id: id, x: x, y: y} ->
Key.new(id, x, y)
end)
end
end