Skip to content

Screens, layouts and widgets

Dashboards are declarative. A source describes what it wants shown; Pharos decides how to draw it on this device, at this resolution, with this input method.

Screens

A screen has an id, a title, a layout, widgets, and optionally: a duration, a refresh policy, a transition, a validity interval, a priority and a cache policy.

Screens may be persistent, temporary, scheduled, provider-owned or locally configured. Playlists rotate them:

{
  "playlist": [
    {"screenId": "overview", "durationSeconds": 30},
    {"screenId": "weather",  "durationSeconds": 20},
    {"screenId": "network",  "durationSeconds": 30}
  ]
}

Rotation pauses on user interaction and resumes after a configurable timeout — otherwise the screen changes under someone's hand while they are reading it.

Layouts

grid · rows · columns · hero + grid · list · full-screen · split

Widget catalog

MVP: text, metric, status, image, clock, list.

Planned: progress, gauge, countdown, QR code, chart, media, web, camera, alert feed.

{
  "type": "metric",
  "id": "network.internet.latency",
  "label": "Internet latency",
  "value": 18,
  "unit": "ms"
}

Note what that example does not contain: a colour, a font size, a position, or a threshold for "bad". The source says what the number is; the node decides how it looks on a 4K television across a room.

Unknown widgets never break a screen

A widget type this version does not know renders a placeholder and logs a sanitized warning. Everything else on the screen renders normally.

This is the single most important rule in this file. Sources will run ahead of nodes — that is the normal state of a fleet, not a fault — and a display that goes blank because one tile was too new is a display nobody trusts.

The renderer registry

interface WidgetRenderer<T : Widget> {
    val type: String

    @Composable
    fun Render(widget: T, context: RenderContext)
}

Registration is source-level: add a renderer, register its type. Dynamic APK plugin loading is deliberately not supported — see architecture.md.

Renderers stay fast. A renderer does layout and drawing; it does not fetch, parse, decode or compute. Anything else belongs upstream in the runtime, because a slow renderer is a stutter on every frame of a screen that may be up for days.

Focus and input

Every renderer that can be interacted with participates in focus. The requirements are release-quality, not polish:

  • an obviously focused state — visible across a room, not a subtle tint
  • deterministic directional movement
  • a correct default focus when a screen appears
  • focus containment inside modals
  • focus restoration when returning to a screen
  • no touch-only controls, anywhere

Focus indication never relies on colour alone; it changes shape, border or scale as well. Colour-only meaning fails for a colour-blind viewer and fails again on a badly calibrated television.

Input arrives as logical actions — Up, Down, Left, Right, Select, Back, Menu, PlayPause, and the rest — never as raw key codes from one particular remote. A Fire remote, a game controller, a Bluetooth keyboard and a touchscreen all produce the same actions, which is why supporting the next device is a mapping change rather than a UI change.

Accessibility and theming

Large readable text, strong focus indicators, no colour-only meaning, screen-reader semantics where applicable, optional reduced motion, and a UI that scales.

All UI strings go through Android resources — English first. Protocol values are stable machine identifiers and are never localized; a status of "degraded" means the same thing in every locale, and translating it would break every consumer that switches on it.

Themes: dark (default), light, system. High contrast and a configurable accent come later.