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.pipx (recommended — no clone, isolated environment):
pipx install git+https://github.com/Paritsingla7/hyprvalidate.gitOther 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.
# 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.
hyprvalidate check ~/.config/hypr/hyprland.lua
hyprvalidate check ~/.config/hypr # a whole directoryCatches unknown dispatchers, invalid config keys, wrong value types, bad call
arity, invalid fields inside a spec table, dispatcher factories referenced
without being called (hl.dsp.window.close instead of
hl.dsp.window.close()), duplicate key binds, and unquoted string values
that would silently evaluate to nil (accel_profile = flat instead of
accel_profile = "flat"):
$ 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.
--fix applies the fixes that have exactly one correct answer — adding
quotes, adding () to a bare dispatcher reference — in place, then
re-checks:
$ hyprvalidate check hyprland.lua --fix
hyprland.lua: fixed 2 issue(s)
[uncalled_dispatcher] hl.bind: argument 2 ('hl.dsp.window.close') is a dispatcher factory reference, not a call - did you mean 'hl.dsp.window.close(...)'?
[possible_missing_quotes] 'input.accel_profile' is set to the bare identifier 'flat', which isn't defined anywhere in this file and would evaluate to nil - did you mean the string "flat"?
fixed 2 issue(s).Findings with no single correct fix — an unknown dispatcher/config key
(which real name was meant?), a type or arity mismatch, two binds fighting
over the same key combo — are still reported, never guessed at. Same as
convert: a fix that would produce invalid Lua, or reveal a fresh problem
(e.g. a dispatcher that turns out to need arguments once it's actually
called), is surfaced, not silently written over.
--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. A ready
GitHub Actions workflow is at
examples/ci/validate-hyprland-lua.yml;
copy it into .github/workflows/ and point CONFIG_PATH at your config. It's
two jobs, split by trigger:
- on a pull request — read-only
check, fails the PR on findings. Works the same for a same-repo branch or a fork; no write access needed, no surprise commits landing mid-review. - on a push —
check --fix, and if that changed anything, commits and pushes the fix straight back onto the branch. Safe to automate because the two things it fixes (missing quotes, uncalled dispatcher factories) have exactly one correct answer; anything else it finds still fails the job instead of being pushed.
- run: pipx install hyprvalidate
- run: curl -fsSLo schema.json https://raw.githubusercontent.com/Paritsingla7/hyprvalidate/v0.4.0/schema.json
- run: hyprvalidate check hypr/ --stub schema.jsoncheck validates against one schema — "is my config valid right now."
diff-impact answers a different question: "of everything that changed
between two Hyprland versions, which parts does my config actually use?"
Hyprland's Lua config API is still young and has already shipped silent
breaking renames (e.g. hl.permission{}'s allow field became mode one
day after Lua config first shipped) — diff-impact cross-references a
config against a real schema diff instead of waiting to find out the hard
way:
$ hyprvalidate diff-impact hyprland.lua --from schemas/v0.55.0.json --to schemas/v0.55.1.json
hyprland.lua: [possible_rename] 'allow' on HL.PermissionSpec no longer exists - possibly renamed to 'mode' (heuristic guess, not confirmed)
1 change(s) in the target schema affect these file(s) - not confirmed breakage, just what's worth checking before updating.schemas/ in this repo has one real schema per tagged Hyprland release
(v0.55.0 onward) to diff between. Every line is phrased as "this changed in
the schema," never "this will break" — the generated stub has itself lagged
real Hyprland behavior before, so a diff is evidence worth checking, not
proof of breakage. Exits 0 even when it finds something, since this isn't
a confirmed problem — pass --fail-on-impact to gate a build on it instead.
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.
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.
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 |
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/ -qThe suite runs with or without Hyprland installed (it falls back to the
committed schema.json).
- 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.
MIT.