hyprvalidate
Migrate old Hyprland configs to Lua.
Validate Lua configs against Hyprland's own API schema.
Try it online · Install · Why it's different
Hyprland 0.55 replaced the old hyprland.conf (hyprlang) format with a Lua
config. If you have a config from before that, it needs migrating — and if you
already wrote a Lua one, nothing currently tells you whether it's actually
valid until Hyprland refuses to load it.
hyprvalidate does both, by reading the API description Hyprland already ships
(/usr/share/hypr/stubs/hl.meta.lua — autogenerated from the compositor's own
source on every build) rather than a table someone typed out by hand.
$ hyprvalidate convert ~/.config/hypr/hyprland.conf --split ~/.config/hypr
wrote 10 file(s) to /home/you/.config/hypr/ (entry point: hyprland.lua)
appearance.lua
autostart.lua
devices.lua
env.lua
hyprland.lua
input.lua
keybinds.lua
monitors.lua
plugins.lua
windowrules.lua
$ hyprvalidate check ~/.config/hypr
10 file(s) checked, no issues found.
Install
pipx (recommended — no clone, isolated environment):
pipx install git+https://github.com/Paritsingla7/hyprvalidate.git
Other methods, and Arch/PEP-668 notes
If pipx isn't installed, on Arch use sudo pacman -S python-pipx —
pip install pipx is blocked by default because the system Python is
externally managed (PEP 668).
From a clone:
git clone https://github.com/Paritsingla7/hyprvalidate.git
cd hyprvalidate
python3 -m venv .venv && .venv/bin/pip install .
Prefer a venv over --break-system-packages on Arch and other
externally-managed Pythons.
Arch (PKGBUILD): a ready packaging/PKGBUILD is in the repo.
Requires: Python 3.10+, and luac (pacman -S lua) for the syntax gate.
Hyprland 0.55+ is needed to read the live schema — or pass
--stub schema.json to run without Hyprland installed at all.
Usage
Convert an old config
# straight to stdout
hyprvalidate convert hyprland.conf
# one file
hyprvalidate convert hyprland.conf -o hyprland.lua
# a modular directory (recommended)
hyprvalidate convert hyprland.conf --split ~/.config/hypr
--split writes one file per config area with a hyprland.lua entry point
that require()s the rest — the layout Hyprland's own docs recommend
("you can (and should!!) split this configuration into multiple files"),
rather than a single 400-line dump.
Symbol names and value types are looked up in the schema. Anything the converter can't resolve confidently becomes a comment, never a silent guess:
-- TODO(hyprvalidate convert): unrecognized old dispatcher 'frobnicate' -
-- no confident rename, convert manually (hyprland.conf line 84)
The converter runs the full validator on its own output before reporting success. If a conversion doesn't come out clean, you find out immediately rather than when Hyprland reloads.
Validate a Lua config
hyprvalidate check ~/.config/hypr/hyprland.lua
hyprvalidate check ~/.config/hypr # a whole directory
Catches unknown dispatchers, invalid config keys, wrong value types, bad call arity, and invalid fields inside a spec table:
$ hyprvalidate check hyprland.lua
hyprland.lua:14: [unknown_spec_field] 'resolution' is not a field of HL.MonitorSpec
hyprland.lua:47: [type_mismatch] 'animations.enabled' expects boolean, got string ('yes')
2 issue(s) found.
Exit codes: 0 clean · 1 schema findings · 2 invalid Lua, unparseable
input, or a missing schema. Scriptable in CI.
Validating dotfiles in CI (no Hyprland needed)
--stub accepts a schema.json snapshot as well as the live stub, so you can
check a dotfiles repo on a runner that has no compositor installed:
- run: pipx install git+https://github.com/Paritsingla7/hyprvalidate.git
- run: hyprvalidate check hypr/ --stub schema.json
Why this exists
Four community converters already existed when this started. Testing all four against the same real config found that three produce output that isn't valid Lua at all, and that all four hand-transcribe Hyprland's dispatcher and config-key names into their own tables — tables which have drifted from the real API in different directions (measured: 53, 11, and 2 dispatcher names, against a real count of 51).
Meanwhile Hyprland ships the correct answer, regenerated on every build. One of
those four tools — hypr2lua — already
had the right idea and tried to read it. Its loader hits a one-line bug
(strings.HasPrefix(line, "--") discards all 1736 annotation lines in the
file, leaving 0 entries extracted), so the schema it threads through its
pipeline is always empty. The idea was right; the read failed silently.
That's the whole thesis here: extract the real schema properly, look everything up against it, and never hardcode a name you could have asked for.
Full measured comparison, with a script to reproduce every number, and proper
credit to the prior work: docs/COMPARISON.md.
Limitations
Stated up front, because a tool whose whole point is honesty about uncertainty shouldn't hide its own gaps.
plugin { }blocks aren't converted. Plugin config is registered at runtime by each plugin and is genuinely absent from the schema, so there's nothing to validate against. Flagged as a TODO for manual conversion.source =isn't inlined. Sourced files are reported, not followed — convert them separately.animation/bezierlist directives aren't mapped yet (they're not in the schema's flat config-key space).- Window-rule fields aren't deeply checked.
HL.WindowRuleSpeconly types 3 universal fields in the stub; per-rule-type fields are dispatched dynamically and aren't enumerable, so checking them would produce false positives. Deliberately skipped rather than guessed. $variablesare inlined, not preserved as a shared Lua table — so you lose the single-point-of-change that$terminalgave you.- Untyped functions aren't arity-checked. Many stub functions are typed
fun(...)with no named parameters; there's nothing to check against, so they aren't. - No
--fixmode yet. The validator reports; it doesn't repair.
How it works
hyprland.conf ──lexer──▶ hyprlang AST ──mapper──▶ Lua AST ──▶ .lua file(s)
▲ │
hl.meta.lua ──extractor─┤ │
(the schema) │ ▼
└──────── checker ◀── luac -p
Everything routes through one extracted schema. The mapper asks it what a symbol is called and what type a value should be; the checker asks it whether what's on disk is real. Neither has a hardcoded API table.
| Component | What it does |
|---|---|
schema/extractor.py |
hl.meta.lua → queryable schema (79 classes, 353 config keys, 51 dispatchers) |
hyprlang/ |
lexer + parser for the old .conf format |
luaast/ |
Lua reader, writer, and the luac -p gate |
converter/ |
block dispatch, type coercion, rename tables, TODO emission, mapper |
checker.py |
the validator: symbols, config keys, types, arity, spec fields |
cli.py |
convert / check |
Contributing
Bug reports about wrong conversions are especially useful — attach the hyprlang snippet and what you expected. See CONTRIBUTING.md.
git clone https://github.com/Paritsingla7/hyprvalidate.git && cd hyprvalidate
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest tests/ -q
The suite runs with or without Hyprland installed (it falls back to the
committed schema.json).
Acknowledgements
- Hyprland — ships the machine-readable API stub this whole project reads; no schema, no project.
- hypr2lua — reached the schema-driven idea first (see "Why this exists" above).
- hyprconf2lua —
its MIT-licensed hyprlang lexer is adapted here (attributed in
hyprvalidate/hyprlang/lexer.py) rather than rewritten from scratch. - hyprlang2lua and hypr-migrate — output worth studying; reading all four is what made the problem visible.
- luaparser (MIT) — the Lua grammar.
License
MIT.
Metadata
Release files for hyprvalidate 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hyprvalidate-0.2.0.tar.gz | 57.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hyprvalidate-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 98.3 kB
Release files / hyprvalidate-0.2.0.tar.gz
| Download URL | hyprvalidate-0.2.0.tar.gz |
|---|---|
| Size | 57.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
93d20c72d33d92120a78a9aa345f278373d305534f96b30407664a9f728957e3
|
|
BLAKE2b-256 checksum How to use checksums |
445d9665a8493a09a35ae3719849d754d143dcc92017dded3c021e5b4720c004
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.
Transparency logRelease files / hyprvalidate-0.2.0-py3-none-any.whl
| Download URL | hyprvalidate-0.2.0-py3-none-any.whl |
|---|---|
| Size | 40.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cae3ccf6c53b4aafd60fc2a6c7ab835a2976a7954dfe1086dcebbbd1c719e9ea
|
|
BLAKE2b-256 checksum How to use checksums |
66719504b92f4400882aa93ead923f7b000159df91db01ea91521208596bb728
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.
Transparency log