Skip to content

Authoring in YAML

xcentaurix edited this page Oct 12, 2026 · 1 revision

Authoring in YAML

Skin sources are written as YAML — a skin.yml (a full document) or a *.ymlinc (an include fragment, the YAML equivalent of .xmlinc) — and converted to and from plain XML+ by yml2xml/xml2yml (the single-file tools ymlcompile drives via yml2xmldomain/xml2ymldomain).

Usage

yml2xml <source .yml/.ymlinc file> <destination .xml/.xmlinc file>   # YAML -> XML+, feed the result to xmlinc
xml2yml <source .xml/.xmlinc file> <destination .yml/.ymlinc file>   # XML+ -> YAML, the reverse

Mapping

  • An element <tag attr="val">...children...</tag> becomes {tag: {attr: "val", ..., children: [...]}}.
  • A standalone <!-- comment --> before a child element becomes a standalone # comment right before the matching list item (and back).
  • A document with more than one top-level element — a real possibility for a .xmlinc fragment (spliced into a parent by literal text substitution, so it isn't required to have a single root the way a real XML document is) — becomes a top-level - tag: {...} dash list instead of the usual single tag: {...} mapping, the same shape a children: list already uses:
    - eLabel: {backgroundColor: "separator", position: "0,e-56", size: "e,2"}
    - eLabel: {backgroundColor: "barBG", position: "0,e-55", size: "e,55"}
    A single-root document (by far the common case) keeps the plain tag: {...} form unchanged.

The <convert> template special case

A <convert type="..."> block holding a TemplatedMultiContent/TemplatedMultiContentEx template is a Python dict literal, not markup — RT_*/BT_* flag algebra, MultiContentEntry* call names, and numeric font indices. Rather than mirroring that syntax 1:1, it's translated into a domain-level cell: describing what's actually shown in each row:

cell:
  itemHeight: 150
  fonts:
    - "Regular;32"
    - "Regular;20"
  border: {width: 1, color: 0x595959, font: "Regular;20", flags: RT_VALIGN_CENTER}
  fields:
    - text:
        position: "5,5"              # pos=/size= kept as the same "x,y" strings every other widget's position=/size= use
        size: "55,25"
        font: "Regular;20"          # resolved from the fonts index, same font="..." syntax every other widget uses
        flags: RT_HALIGN_LEFT | RT_VALIGN_CENTER
        value: 0                    # renamed from text= - still the raw tuple index, see note below
    - icon:
        position: "0,5"
        size: "35,35"
        flags: BT_SCALE
        value: 23                   # renamed from png=
    - progress:
        position: "90,5"
        size: "90,14"
        value: -22                  # renamed from percent=
        foreColor: "#bababa"
  • Three field kinds cover every real template in this codebase: text (from MultiContentEntryText), icon (from MultiContentEntryPixmapAlphaBlend, or MultiContentEntryPixmapAlphaTest — marked with a variant: "AlphaTest" key, omitted for the far more common Blend case), progress (from MultiContentEntryProgress) — each only when the call has exactly the plain kwarg set observed in practice. Anything else (Rectangle, LinearGradient*, ProgressPixmap, or a call with an unusual extra kwarg like cornerRadius) falls back to a raw field carrying the untouched {call, args?, kwargs?} shape described below — never lossy, it just doesn't get the readability upgrade. A text field's font: is itself optional — some plugins omit font= and let enigma2 default it, and that's preserved as an absent key rather than forced to a guessed value.

  • cell.fonts is the source template's "fonts" list, kept verbatim, same order, same positions — not deduplicated or renumbered, and never dropped, even for an entry no font=N anywhere in the template currently points to. font=N is a positional reference something outside this one <convert> block may rely on, so round-tripping must never renumber or delete a slot just because nothing here currently uses it. A plain two-arg gFont(family, size) entry renders as the same "Family;Size" string every font: reference elsewhere uses (as above); anything else (e.g. parseFont(...)) falls back to the verbose {call, args} form, still inside fonts:.

  • cell.vars covers TemplatedMultiContentEx's local-variable feature — some plugins declare a "var": (name := expr, ...) tuple of walrus-bound values ahead of "template" and reference them throughout its pos=/size= expressions (grid math shared across several rows/columns, computed once). Preserved as an ordered list of the exact binding text, since order matters (a later binding can reference an earlier one by name) and the right-hand side is an arbitrary Python expression, not typed data. Each entry accepts either name := expr or the more natural-looking name = expr — always compiled back out as :=, the only valid syntax inside the "var": (...) tuple literal itself:

    cell:
      vars:
        - "ih = 70"
        - "hspace = 10"
        - "x1 = hspace//2+thumb_w+hspace"
      fields:
        - icon:
            position: "hspace//2,(ih-thumb_h)//2"   # position:/size: freely mix var names, literals, and grid math
            size: "thumb_w,thumb_h"
            ...
  • MultiContentTemplateColor(n) still nests inside a color-ish key exactly as before: color: {call: MultiContentTemplateColor, args: [24]}.

  • Not in scope: resolving value: 0 to a semantic name (value: startHM) — that needs a per-plugin tuple-index mapping (e.g. a screen's own Index.py) this tool has no generic way to discover, so value: stays the raw index/literal, same information as text=/png=/percent= always held.

  • Every entry factory is covered the same generic way — Text, Pixmap, PixmapAlphaTest, PixmapAlphaBlend, Progress, ProgressPixmap, Rectangle, LinearGradient, LinearGradientAlphaBlend (the complete list in Components/MultiContent.py) — the call name is just emitted as-is.

  • TemplatedMultiContentEx's e/c grid variables work in pos=/size= arithmetic (and inside a domain field's position:/size:): position: "e-350,15".

  • Kwarg values are quoted strings by default (real Python string literals) — except flags, call, and direction, the three keys that hold code (RT_*/BT_*/GRADIENT_* constants, a callable name) rather than data. This is decided by key name, not by how the value was written: YAML's own parser can't tell a quoted string from a bare one apart once parsed, so quoting style alone can't carry the distinction through a round trip.

Formatting conventions

yml2xml, xml2yml, and xmlpretty all independently enforce the same conventions on a <convert> block's Python-source body, so a file generated by one and reformatted by another never disagrees with itself:

  • pos=(x,y)/size=(w,h) tuples, gFont(family,size) calls, and flags OR-chains (A|B) stay tight — no space after the comma or around | — even though every other comma (between kwargs, between list items) does get a space. This matches how these are overwhelmingly hand-written across the real plugins in this codebase.
  • Strings always use ", never '.
  • "key": value object pairs (itemHeight, fonts, ...) always have a space after the colon.
  • A raw-preserved convert body (one parseConvertTemplate doesn't recognize the shape of, e.g. a plugin-specific type with extra top-level keys) still gets its outer indentation normalized — the opening {/closing } line up with the <convert> tag itself, one level deeper for its own keys — without touching a single character of content it doesn't understand.

Clone this wiki locally