Templates: Jinja → XML → image

A screen template (the file a screens: entry points template: at) is a Jinja template. Rendering it produces a string of XML, which is then parsed and walked to build the image — so everything in a template must produce well-formed XML once Jinja’s {{ }}/{% %} are evaluated. This has two practical consequences: attribute values coming from Jinja expressions need to end up quoted (x="{{ x_pos }}", not x={{ x_pos }}), and any literal &/</> in text content needs XML escaping (&amp; for a literal &, as in the example below).

Variables and functions available to every template

Name

Description

width, height

The panel size from the config’s screens: section.

timestamp

The current render time, as a timezone-aware datetime in the config’s settings.timezone.

tomorrow

timestamp + timedelta(days=1) — a convenience for “what’s tomorrow’s date”, e.g. when a screen is designed to be read the evening before.

timezone

timestamp.tzinfo, for constructing further datetimes.

source(name)

Calls .fetch() on the named entry from the config’s sources: section and returns whatever that data source produces (a list of calendar events, a weather forecast model, …). Fetches are not cached across calls — calling source("weather") twice in one render hits the API twice.

The tag vocabulary

Every element in the rendered XML is a “view”: either a layout container (<box>, <column>, <row>) or a piece of content (<text>, <image>, <line>, <rectangle>, and a few source-specific ones like <weather.icon>). The full reference, with every attribute each one accepts, is in Widget reference. The root element of every template must be <screen>.

A worked example

This is configs/home/family_calendar.xml.jinja, trimmed slightly:

{% macro homework() -%}
{% with ical = source("school_work") -%}
    {% for event in ical.walk('vevent') -%}
        {% set start = event.get('dtstart').dt -%}
        {% if start > timestamp.date() %}
            <box height="85">
                <image y="0" x="0" path="home/cal_task.png" />
                <text y="12" x="80" size="35" font="home/Roboto-Bold.ttf">{{ start.strftime('%-d/%-m') }}</text>
                <text y="12" x="300" width="440" size="35" font="home/Roboto.ttf">{{ event.get('summary') }}</text>
            </box>
        {% endif %}
    {% endfor %}
{% endwith %}
{%- endmacro %}
<screen>
    <rectangle x="0" y="0" width="{{ width }}" height="70" fill="true" color="#5c5c5c" />
    <text x="{{ width // 2 | int }}" y="10" size="40" align="center" font="home/Roboto-Bold.ttf" color="white">
        {{ timestamp.strftime('%A, %B %d') }}
    </text>
    <column x="700" y="100">
        <box>
            <text size="50" font="home/Roboto-Bold.ttf">LÄXOR &amp; PROV</text>
            <line y="65" x2="700" y2="65" width="7" />
            <column y="95">
                {{ homework() }}
            </column>
        </box>
    </column>
</screen>

A few things worth noting from this example, since they’re easy to trip over:

  • source("school_work") is called inside a Jinja {% for %} loop, iterating events straight from the icalendar calendar object the calendar.ical source returns (see Calendar: calendar.ical, calendar.google).

  • A Jinja macro (homework()) that itself contains XML tags is a normal way to factor out a repeated chunk of layout — the macro’s output is just more XML text, spliced into the surrounding document.

  • Nesting a <box> of <text>/<line> items inside a <column> composes layout containers just like composing HTML — see Layout: box, column, row, screen for exactly how each one sizes and positions its children.

  • width="{{ width }}" on the <rectangle> pulls the panel width from the template variable, not a hardcoded pixel value, so the same template adapts if the config’s screens.width changes.

Debugging a template

Two tools make it much faster to figure out why a screen doesn’t look right:

  • uv run todays_paper <config> -s <screen> -d -o preview.png — the -d flag logs the XML Jinja produced before it’s walked into an image, which is the quickest way to spot a missing attribute, a mis-nested tag, or a macro that didn’t expand the way you expected.

  • -t "2024-12-25 08:00" — render as if it were a specific date/time, useful for testing day-of-week/time-of-day branches in a template (like the homework-deadline check above) without waiting for that date to actually arrive.