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.
Quick start
Section titled “Quick start”Run the validator directly with the console script or wrap it through the Makefile shortcut:
led-ticker validate config/config.toml# ormake validate CONFIG=config/config.tomlThe 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.
Errors-only run
Section titled “Errors-only run”A config that pairs text_align = "scroll" with fit = "stretch" on a gif/image widget:
led-ticker validate config/config.tomlValidating 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)Warnings-only run
Section titled “Warnings-only run”A config with no hard errors but a transition_duration that looks like milliseconds:
led-ticker validate config/config.tomlValidating 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)Machine-readable JSON
Section titled “Machine-readable JSON”The same split applies to --json output (used by the creating-a-config skill):
led-ticker validate config/config.toml --jsonErrors-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):
led-ticker validate --strict config/config.tomlIn strict mode:
- All warnings are promoted to errors —
validisfalseif any warnings were found. gifandimagewidgetpathfields 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:
led-ticker validate --strict config/config.toml || exit 1Run 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.
Output format
Section titled “Output format”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:
| Field | Type | Meaning |
|---|---|---|
rule | int | null | A 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). |
location | string | Path into the config, e.g. section[0].widget[1] or transitions.between_sections. |
message | string | What the validator found. |
fix | string | A 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.
What gets caught
Section titled “What gets caught”Quick reference. Each row maps to a longer entry on Validation rules.
| Check | Severity | Quick fix |
|---|---|---|
text_align="scroll" paired with fit="stretch" | error | use pillarbox/letterbox/crop |
text_x_offset set with a scrolling text_align | error | drop the offset, or use a static text_align |
hold_time < 0.05 | error | raise to ≥ 0.05 (it’s seconds, not ms) |
animation on a widget that doesn’t support it | error | remove it; use font_color = "rainbow" instead |
animation = "typewriter" on gif / image with conflicts | error | drop bottom_text, scroll mode, or empty text |
Per-row font line-height exceeds the band on two_row / two-row | error | smaller font_size, or raise content_height |
start_hold on slideshow section or negative value | error | remove start_hold or set it to ≥ 0 |
separator_* set on a non-ticker section | error | remove the field, or switch to ticker |
bottom_text_loops on two_row without bottom_text_wrap | error | set bottom_text_wrap = true, or drop the field |
text_loops on a two_row widget (rule 29) | error | rename to bottom_text_loops |
scroll_step_ms <= 0 on a section (rule 31) | error | set to a positive integer (e.g. 35) |
content_height × scale > panel_h_real (rule 1) | error | lower content_height to panel_h ÷ scale |
scroll_speed_ms at section level or scroll_step_ms on widget | error | swap fields to the correct scope (rule 34) |
| Unknown widget field name (typo in TOML) | error | run --list-fields TYPE to see valid names |
| Unknown transition name (typo in any transition field) | error | did-you-mean hint; see Transitions for valid names |
Asset path does not exist on disk (--strict only) | error | check path is relative to config file |
two_row widget at scale = 4 | warning | use scale = 2 (128 logical px wide) |
transition_duration > 5 s or < 0.05 s | warning | divide by 1000 (it’s seconds, not milliseconds) |
Held top_text wider than the logical canvas | warning | shorten text, smaller font, or lower scale |
font name is not bundled and not in config/fonts/ | warning | drop the font file on the deploy target |
hold_time set alongside bottom_text_loops on two_row | warning | drop one (or accept max() semantics) |
default = "..." written inside a section block (rule 35) | warning | rename 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:
led-ticker validate --list-fields messageled-ticker validate --list-fields two_rowled-ticker validate --list-fields gifThis 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_widgethandles 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).
Warnings are advisory, not blocking
Section titled “Warnings are advisory, not blocking”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.