# `RasterExRatatui.Framebuffer.Surface`
[🔗](https://github.com/mcass19/raster_ex_ratatui/blob/v0.4.0/lib/raster_ex_ratatui/framebuffer/surface.ex#L1)

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`](https://hex.pm/packages/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)
    end

A panel that is not a framebuffer, or a keyboard that is not evdev, starts from `RasterExRatatui.Surface` instead; [Building a Surface](surfaces.md) is the guide.

# `state`

```elixir
@type state() :: %{
  :fb =&gt; RasterExRatatui.Framebuffer.t(),
  :devices =&gt; RasterExRatatui.Input.Devices.t() | nil,
  optional(atom()) =&gt; term()
}
```

The surface's state: the open framebuffer and the input devices, plus whatever an override adds.

# `handle_info`

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

The default `c:RasterExRatatui.Surface.handle_info/2`: `RasterExRatatui.Input.Devices.handle_info/2`, with anything it does not know ignored.

# `init`

```elixir
@spec init(keyword()) :: {:ok, keyword(), state()} | {:stop, {:framebuffer, term()}}
```

The default `c: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).

# `push`

```elixir
@spec push([RasterExRatatui.Patch.t()] | {:frame, binary()}, state()) :: state()
```

The default `c:RasterExRatatui.Surface.push/2`: `RasterExRatatui.Framebuffer.write/2`.

# `terminate`

```elixir
@spec terminate(term(), state()) :: :ok | {:error, term()}
```

The default `c:RasterExRatatui.Surface.terminate/2`: stops the input readers and closes the framebuffer.

---

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