Skip to content
led-ticker

Tool: validate

led-ticker validate reads a config.toml and reports configuration mistakes without opening a panel. It catches TOML parse errors, structural problems, widget-construction failures (via _build_widget(validate_only=True)), and soft layout warnings — before you deploy to the Pi. The full list is on the Validation rules page; the implementation lives at src/led_ticker/validate.py.

It exists so that the most common config mistakes — a text_align = "scroll" paired with a fit = "stretch", a hi-res font without font_size, a content_height that overflows the panel — surface on your laptop in milliseconds, instead of on the Pi after a deploy. It’s also the contract the creating-a-config skill uses to verify generated configs: the skill calls validate --json and refuses to ship anything that fails.

Run the validator directly with the console script or wrap it through the Makefile shortcut:

Terminal window
led-ticker validate config/config.toml
# or
make validate CONFIG=config/config.toml

The make target is just a convenience that runs uv run led-ticker validate — useful when you don’t want to remember the uv run prefix.

Warnings are gated behind a Phase-2 check — they only surface when no errors are present, so you’ll never see both classes in the same run. The two examples below show the human-readable output for an errors-only run and a warnings-only run; the --json section mirrors the same split.

A config that pairs text_align = "scroll" with fit = "stretch" on a gif/image widget:

Terminal window
led-ticker validate config/config.toml
Validating config/config.toml...
✗ ERROR section[0].widget[1]: text_align='scroll' with fit='stretch': no transparent regions for text to walk behind
Fix: Use text_align='scroll_over' to paint text on top of the image, or change fit to 'pillarbox' / 'letterbox' / 'crop'.
1 issue(s): 1 error(s), 0 warning(s)

A config with no hard errors but a transition_duration that looks like milliseconds:

Terminal window
led-ticker validate config/config.toml
Validating config/config.toml...
⚠ WARNING transitions.between_sections: transition_duration 800.0 looks like milliseconds (> 5 s is unusual)
Fix: Divide by 1000 → 0.800 s
1 issue(s): 0 error(s), 1 warning(s)

The same split applies to --json output (used by the creating-a-config skill):

Terminal window
led-ticker validate config/config.toml --json

Errors-only run:

{
"valid": false,
"path": "config/config.toml",
"errors": [
{
"rule": 3,
"location": "section[0].widget[1]",
"message": "text_align='scroll' with fit='stretch': no transparent regions for text to walk behind",
"fix": "Use text_align='scroll_over' to paint text on top of the image, or change fit to 'pillarbox' / 'letterbox' / 'crop'."
}
],
"warnings": []
}

Warnings-only run:

{
"valid": true,
"path": "config/config.toml",
"errors": [],
"warnings": [
{
"rule": 21,
"location": "transitions.between_sections",
"message": "transition_duration 800.0 looks like milliseconds (> 5 s is unusual)",
"fix": "Divide by 1000 → 0.800 s"
}
]
}

The CLI exits 0 when the config is valid (no errors; warnings allowed), 1 when there are errors, and 2 when the file does not exist.

Strict mode — promote warnings to errors

Section titled “Strict mode — promote warnings to errors”

Pass --strict to treat every warning as an error and also enable asset-path existence checks (Rule 40):

Terminal window
led-ticker validate --strict config/config.toml

In strict mode:

  • All warnings are promoted to errors — valid is false if any warnings were found.
  • gif and image widget path fields must exist on disk (relative to the config file).

Exit codes are the same: 0 clean, 1 errors (or promoted warnings), 2 file not found.

CI use case: gate deploys on a warning-clean config:

Terminal window
led-ticker validate --strict config/config.toml || exit 1

Run validate without --strict during local drafting (assets may not be present locally); switch to --strict on the Pi or in your CI pipeline once all assets are in place.

The JSON shape is stable and meant to be parsed:

{
"valid": true,
"path": "config/config.toml",
"errors": [],
"warnings": []
}

Each entry in errors and warnings follows the ValidationIssue dataclass defined in src/led_ticker/validate.py:

FieldTypeMeaning
ruleint | nullA numeric id used internally for test assertions and tooling. Stable across releases; not surfaced in the human-readable output. null for unclassified errors (e.g. raw TOML parse).
locationstringPath into the config, e.g. section[0].widget[1] or transitions.between_sections.
messagestringWhat the validator found.
fixstringA one-line suggestion that maps the problem to a concrete config change.
severity"error" or "warning"Errors block the ticker at runtime; warnings are advisory and the ticker will still start.

The valid field is true exactly when errors is empty — warnings do not flip it.

Quick reference. Each row maps to a longer entry on Validation rules.

CheckSeverityQuick fix
text_align="scroll" paired with fit="stretch"erroruse pillarbox/letterbox/crop
text_x_offset set with a scrolling text_alignerrordrop the offset, or use a static text_align
hold_time < 0.05errorraise to ≥ 0.05 (it’s seconds, not ms)
animation on a widget that doesn’t support iterrorremove it; use font_color = "rainbow" instead
animation = "typewriter" on gif / image with conflictserrordrop bottom_text, scroll mode, or empty text
Per-row font line-height exceeds the band on two_row / two-rowerrorsmaller font_size, or raise content_height
start_hold on slideshow section or negative valueerrorremove start_hold or set it to ≥ 0
separator_* set on a non-ticker sectionerrorremove the field, or switch to ticker
bottom_text_loops on two_row without bottom_text_wraperrorset bottom_text_wrap = true, or drop the field
text_loops on a two_row widget (rule 29)errorrename to bottom_text_loops
scroll_step_ms <= 0 on a section (rule 31)errorset to a positive integer (e.g. 35)
content_height × scale > panel_h_real (rule 1)errorlower content_height to panel_h ÷ scale
scroll_speed_ms at section level or scroll_step_ms on widgeterrorswap fields to the correct scope (rule 34)
Unknown widget field name (typo in TOML)errorrun --list-fields TYPE to see valid names
Unknown transition name (typo in any transition field)errordid-you-mean hint; see Transitions for valid names
Asset path does not exist on disk (--strict only)errorcheck path is relative to config file
two_row widget at scale = 4warninguse scale = 2 (128 logical px wide)
transition_duration > 5 s or < 0.05 swarningdivide by 1000 (it’s seconds, not milliseconds)
Held top_text wider than the logical canvaswarningshorten text, smaller font, or lower scale
font name is not bundled and not in config/fonts/warningdrop the font file on the deploy target
hold_time set alongside bottom_text_loops on two_rowwarningdrop one (or accept max() semantics)
default = "..." written inside a section block (rule 35)warningrename to transition = "..."

Discovering widget fields — --list-fields

Section titled “Discovering widget fields — --list-fields”

If you’re not sure what fields a widget type accepts, run:

Terminal window
led-ticker validate --list-fields message
led-ticker validate --list-fields two_row
led-ticker validate --list-fields gif

This prints the widget’s full field surface in two groups:

  • Widget-level fields — attrs fields the widget constructor accepts directly, with types and defaults.
  • Dispatch-level fields — shared fields _build_widget handles before construction (e.g. font, font_size, animation, border).

When you use an unrecognized field name in a widget config block, the validator now catches it with a did-you-mean suggestion (rule 38). The fix line in the error output points at --list-fields so you can immediately see the correct name.

The validator only catches static, config-time problems

Section titled “The validator only catches static, config-time problems”

Anything that depends on runtime state — a stale WEATHERAPI_KEY, an unreachable RSS feed, an image_path that exists in your repo but not in the Docker bind mount, a network split-brain that makes a live data fetch time out — is out of scope. A green validate is necessary but not sufficient; the only way to fully verify a config is to run it against the panel (or the render-demo gif pipeline).

The ticker will start, scroll, and swap when only warnings are present, but the warning usually points at a rendering quirk you don’t actually want — a two_row widget cramped into a 64px logical canvas, or a transition_duration that looks like milliseconds when the engine wants seconds. Treat them as a checklist before you call a config done.

Pass --strict to promote all warnings to errors and enforce a warning-clean config in CI. See the Strict mode section above.