Metadata-Version: 2.4
Name: hyprvalidate
Version: 0.3.0
Summary: Convert and validate Hyprland Lua configs against Hyprland's real API schema
Author: Paritsingla7
License-Expression: MIT
Project-URL: Homepage, https://paritsingla7.github.io/hyprvalidate/
Project-URL: Source, https://github.com/Paritsingla7/hyprvalidate
Project-URL: Issues, https://github.com/Paritsingla7/hyprvalidate/issues
Project-URL: Changelog, https://github.com/Paritsingla7/hyprvalidate/blob/main/CHANGELOG.md
Keywords: hyprland,hyprlang,lua,config,converter,validator,wayland
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Desktop Environment :: Window Managers
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: luaparser>=4.1.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

<h1 align="center">hyprvalidate</h1>

<p align="center">
  Migrate old Hyprland configs to Lua.<br>
  Validate Lua configs against Hyprland's own API schema.
</p>

<p align="center">
  <a href="https://paritsingla7.github.io/hyprvalidate/"><strong>Try it online</strong></a> ·
  <a href="#install"><strong>Install</strong></a> ·
  <a href="docs/COMPARISON.md"><strong>Why it's different</strong></a>
</p>

<p align="center">
  <a href="https://github.com/Paritsingla7/hyprvalidate/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/Paritsingla7/hyprvalidate/actions/workflows/ci.yml/badge.svg"></a>
  <img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue.svg">
  <img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-blue.svg">
  <img alt="Hyprland 0.55+" src="https://img.shields.io/badge/hyprland-0.55%2B-1a9fb2.svg">
  <img alt="tests" src="https://img.shields.io/badge/tests-170%20passing-brightgreen.svg">
</p>

---

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.

```console
$ 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):

```bash
pipx install git+https://github.com/Paritsingla7/hyprvalidate.git
```

<details>
<summary>Other methods, and Arch/PEP-668 notes</summary>

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:**

```bash
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.
</details>

**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

```bash
# 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:

```lua
-- 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

```bash
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"`):

```console
$ 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:

```console
$ 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`](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.

```yaml
- run: pipx install hyprvalidate
- run: curl -fsSLo schema.json https://raw.githubusercontent.com/Paritsingla7/hyprvalidate/v0.3.0/schema.json
- 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](https://github.com/Phillezi/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`](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](CONTRIBUTING.md).

```bash
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](https://github.com/hyprwm/Hyprland)** — ships the
  machine-readable API stub this whole project reads; no schema, no project.
- **[hypr2lua](https://github.com/Phillezi/hypr2lua)** — reached the
  schema-driven idea first (see "Why this exists" above).
- **[hyprconf2lua](https://github.com/Prateek-squadron/hyprconf2lua)** —
  its MIT-licensed hyprlang lexer is adapted here (attributed in
  `hyprvalidate/hyprlang/lexer.py`) rather than rewritten from scratch.
- **hyprlang2lua** and **[hypr-migrate](https://github.com/loeclos/hypr-migrate)**
  — output worth studying; reading all four is what made the problem visible.
- **[luaparser](https://pypi.org/project/luaparser/)** (MIT) — the Lua grammar.

## License

[MIT](LICENSE).
