Skip to content

About

Convert hyprland.conf to Lua and validate Hyprland Lua configs against Hyprland's real API schema, not a hand-typed table

Topics

Resources

Code of conduct

Contributing

Stars

6 stars

Watchers

0 watching

Forks

Repository files navigation

hyprvalidate

Migrate old Hyprland configs to Lua.
Validate Lua configs against Hyprland's own API schema.

Try it online · Install · Why it's different

CI License: MIT Python 3.10+ Hyprland 0.55+ tests


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, 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.

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. 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.json

Checking whether an upcoming Hyprland update will affect you

check 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.

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 / bezier list directives aren't mapped yet (they're not in the schema's flat config-key space).
  • Window-rule fields aren't deeply checked. HL.WindowRuleSpec only 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.
  • $variables are inlined, not preserved as a shared Lua table — so you lose the single-point-of-change that $terminal gave 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 --fix mode 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.

About

Convert hyprland.conf to Lua and validate Hyprland Lua configs against Hyprland's real API schema, not a hand-typed table

Topics

Resources

Code of conduct

Contributing

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages