A complete surface for a Linux framebuffer and its evdev keyboard, in one use.
defmodule MyDevice.Surface do
use RasterExRatatui.Framebuffer.Surface, app: MyDevice.Dashboard, rotate: 90
end
# in the application's supervision tree
children = [MyDevice.Surface]That is the whole consumer for a Raspberry Pi with a display: at start the surface waits for /dev/fb0 to appear, reads its size and depth from sysfs, picks the pixel format (RGB565 at 16 bits per pixel, XRGB8888 at 32) and a font scale that keeps about a hundred columns on the long side, detaches the kernel console from the framebuffer, starts the app, and reads the first USB keyboard it finds (and the touch panel, with touch: true), looking again whenever there is none. Pixels go out with RasterExRatatui.Framebuffer.write/2; keys and taps come in through RasterExRatatui.Input.Devices. When the app exits, a new one starts on the same surface (on_app_exit: :restart), because a panel has nothing else to show.
Options
Given to use or to the child spec ({MyDevice.Surface, rotate: 90}), and read from the application's config the usual way. All of RasterExRatatui.Surface's options apply too; these are the ones this module adds or defaults.
| Option | Default | What it does |
|---|---|---|
:app | required | the ExRatatui.App to run |
:framebuffer | "fb0" | the framebuffer to draw on |
:framebuffer_timeout | 30_000 | how long to wait for it to appear, in milliseconds (display drivers load during boot) |
:scale | :auto | integer font scale, or :auto for RasterExRatatui.Framebuffer.auto_scale/2 |
:columns | 100 | the columns :auto keeps on the long side |
:rotate | 0 | 90, 180, or 270 for a panel mounted on its side (see RasterExRatatui.Raster) |
:console | "vtcon1" | the framebuffer console to detach, or false to leave it |
:keyboard | true | read the first keyboard found, a "/dev/input/eventN" path, or false |
:touch | false | the touch panel, the same way: taps and drags reach the app as ExRatatui.Event.Mouse events on the cell under the finger, rotation included |
:swap_xy, :invert_x, :invert_y | false | for a touch controller that does not follow the panel's orientation (RasterExRatatui.Input.Touch) |
:on_app_exit | :restart | :stop to let the supervisor decide instead; with :restart, :max_restarts crashes (default 3) within :max_seconds (default 5) still stop the surface |
:font, :format_opts, :app_opts, :min_interval, :push_mode, :shutdown_timeout, :name | as in RasterExRatatui.Surface | |
:retry_ms, :layout, :emit_release, :input | as in RasterExRatatui.Input.Devices | |
:root | "/" | where /sys and /dev are (tests point it at a fake tree) |
Starting blocks until the framebuffer is there, up to :framebuffer_timeout, so the surface belongs last in its supervisor: children after it would wait that long on a slow or missing display.
The keyboard needs input_event in the consumer's deps, a C port for Linux. Without it the surface logs a warning once and runs without input, so the same project still compiles and runs its tests on a host.
Under the hood, and overriding
use gives the module init/1, push/2, handle_info/2, and terminate/2, each delegating to the function of the same name here, and all overridable. Override one and call the default from it to add to what it does; the state is a map with :fb (the open RasterExRatatui.Framebuffer) and :devices (the RasterExRatatui.Input.Devices, or nil), and anything else added to it travels along.
defmodule MyDevice.Surface do
use RasterExRatatui.Framebuffer.Surface, app: MyDevice.Dashboard
@impl true
def init(opts) do
{:ok, config, state} = RasterExRatatui.Framebuffer.Surface.init(opts)
{:ok, config, Map.put(state, :backlight, MyDevice.Backlight.on!())}
end
@impl true
def handle_info({:button, :back}, state),
do: {:events, [%ExRatatui.Event.Key{code: "esc", kind: "press"}], state}
def handle_info(msg, state), do: RasterExRatatui.Framebuffer.Surface.handle_info(msg, state)
endA panel that is not a framebuffer, or a keyboard that is not evdev, starts from RasterExRatatui.Surface instead; Building a Surface is the guide.
Summary
Types
The surface's state: the open framebuffer and the input devices, plus whatever an override adds.
Functions
The default RasterExRatatui.Surface.handle_info/2: RasterExRatatui.Input.Devices.handle_info/2, with anything it does not know ignored.
The default RasterExRatatui.Surface.init/1: waits for the framebuffer, picks the format and scale, detaches the console, starts the input devices.
The default RasterExRatatui.Surface.terminate/2: stops the input readers and closes the framebuffer.
Types
@type state() :: %{ :fb => RasterExRatatui.Framebuffer.t(), :devices => RasterExRatatui.Input.Devices.t() | nil, optional(atom()) => term() }
The surface's state: the open framebuffer and the input devices, plus whatever an override adds.
Functions
@spec handle_info(term(), state()) :: {:noreply, state()} | {:events, [ExRatatui.Event.t()], state()}
The default RasterExRatatui.Surface.handle_info/2: RasterExRatatui.Input.Devices.handle_info/2, with anything it does not know ignored.
The default RasterExRatatui.Surface.init/1: waits for the framebuffer, picks the format and scale, detaches the console, starts the input devices.
Returns {:ok, [size:, format:, scale:, rotate:, on_app_exit:], %{fb:, devices:}}, or {:stop, {:framebuffer, reason}} when the framebuffer never appears or has a depth the library cannot pack (that one fails at once).
@spec push([RasterExRatatui.Patch.t()] | {:frame, binary()}, state()) :: state()
The default RasterExRatatui.Surface.push/2: RasterExRatatui.Framebuffer.write/2.
The default RasterExRatatui.Surface.terminate/2: stops the input readers and closes the framebuffer.