README
¶
hyprlang2lua
Convert legacy Hyprland hyprland.conf (hyprlang) files
to the Lua configuration format introduced in Hyprland 0.55 (May 2026).
The converter is built around a hand-written lexer + recursive-descent parser
and a per-directive code generator. The output is idiomatic Lua that matches
the shape of the example config shipped at /usr/share/hypr/hyprland.lua,
mapping each hyprlang construct to the hl.* API exposed by the Lua stubs
at /usr/share/hypr/stubs/hl.meta.lua.
Install
Arch Linux (AUR)
paru -S hyprlang2lua # or: yay -S hyprlang2lua
The PKGBUILD source lives at packaging/aur/PKGBUILD; release notes for
maintainers are in packaging/aur/MAINTAINING.md.
Nix (flake)
nix run github:EIonTusk/hyprlang2lua -- input.conf > output.lua
nix profile install github:EIonTusk/hyprlang2lua # persistent install
nix develop opens a dev shell with the Go toolchain, gopls, and lua
(used by the optional luac -p gate in the golden tests).
Go (source)
go install github.com/EIonTusk/hyprlang2lua/cmd/hyprlang2lua@latest
…or build locally:
go build -o hyprlang2lua ./cmd/hyprlang2lua
Go 1.26+. The library (internal/converter) is standard-library only; the
CLI adds a single dependency, github.com/spf13/pflag, for POSIX-style flag
parsing.
A WebAssembly build is included under web/ for an in-browser converter — see
Browser build below.
Usage
# single file → stdout
hyprlang2lua ~/.config/hypr/hyprland.conf > ~/.config/hypr/hyprland.lua
# from stdin
cat hyprland.conf | hyprlang2lua > hyprland.lua
# write next to each *.conf in a tree
hyprlang2lua --dir ~/.config/hypr
# show coverage stats on stderr
hyprlang2lua --report hyprland.conf > hyprland.lua
# CI mode: exit non-zero if anything was flagged for manual review
hyprlang2lua --check hyprland.conf > /dev/null
Flags:
| flag | effect |
|---|---|
-d, --dir DIR |
walk a directory, writing *.lua next to every *.conf |
--in-place |
with --dir, overwrite existing *.lua siblings (off by default) |
-o, --out FILE |
write to FILE (single-file mode; default stdout) |
-r, --report |
print translated / passthrough / flagged / coverage% to stderr |
-c, --check |
exit code 3 if any directive was flagged for manual review |
-m, --merge |
merge every hl.X(...) call into a single one if the API supports it (default on; pass --merge=false to disable). In practice this folds every per-section hl.config({...}) into one call — section-separating comments are preserved inside the merged table. Other hl.* APIs (bind, window_rule, monitor, env, device, …) take one spec per call by design and pass through unchanged. --merge-config is kept as a deprecated alias. |
-s, --strip-comments |
drop comments from the output (-- TODO: manual review markers from flagged directives are kept) |
With no positional argument, the CLI reads from stdin — unless stdin is a TTY, in which case it prints usage instead of hanging on a read.
Exit codes: 0 success, 1 I/O or conversion error, 2 usage/flag error,
3 --check failed (at least one flagged directive).
Supported directives
Phase 1 (translated automatically)
key = valueat the top level and inside any of the recognized sections:general,decoration,input,animations,gestures,misc,binds,cursor,debug,dwindle,master,group,render,xwayland,opengl,ecosystem,experimental,layout,scrolling,quirks. Nested sections (decoration { blur { } }) emit nested Lua tables.$var = value→local var = value. References ($varon the right side of any directive) resolve to the local; mixed text builds a concat chain (mainMod .. " + SHIFT + 1").- The
bindfamily —bind,bindm,binde,bindr,bindl,bindn,bindo,bindt,bindi,bindp,bindc,bindd, and any combined-flag form likebindel/bindle. Each flag suffix becomes the corresponding field onHL.BindOptions. exec,exec-once,execr-once,exec-shutdown. Bundled into onehl.on("hyprland.start", function() ... end)(orconfig.reloaded/hyprland.shutdown) block per kind.monitor,windowrule,windowrulev2,workspace,layerrule,env,envd,animation,bezier,gesture,permission.device:<name> { ... }, mapped tohl.device({ name = "<name>", ... }).# comment→-- comment, in roughly the same source position.
Phase 2 (translated where possible, flagged where ambiguous)
source = path→require("path")plus a comment reminding the user that the sourced.confmust itself be converted. Reason:require()integrates with Lua'spackage.pathand preserves the user's modular structure;dofile()would force relative paths, and inline-expansion would bloat output and discard organization.submap = name/submap = reset— these define stateful blocks of binds in hyprlang. The Lua API expects a callback (hl.define_submap("name", function() hl.bind(...) end)), which requires reordering the source. The converter emits a TODO at thesubmap =line so users can wrap the following block by hand.plugin { name { ... } }— plugin sections are passed through as a Lua comment block with a TODO, since each plugin exposes its own keys underhl.plugin.<name>and we can't safely guess the API.envdis converted tohl.env(...); the systemd/D-Bus propagation thatenvdprovided needs to be replicated outside Lua.
Anything not in either list is preserved with a -- TODO: manual review
comment, contributes to flagged in the report, and trips --check.
Architecture
internal/converter/ pure Go — lexer, parser, AST, Lua codegen.
no os, no net, no filesystem. wasm-compatible.
single entry point: Convert(src) -> (lua, Report, err)
cmd/hyprlang2lua/ thin CLI wrapper over the converter package.
The core is deliberately I/O-free so the same code backs both the CLI and
the WebAssembly build at web/wasm/main.go.
Browser build
web/ contains a self-contained converter UI that runs entirely client-side
— the conversion happens in WebAssembly compiled from the same
internal/converter package, so no input ever leaves the page.
Build the wasm artifact and serve the directory:
cd web/wasm
GOOS=js GOARCH=wasm go build -o ../main.wasm .
cd ..
python3 -m http.server 8080 # or any static server
Then open http://localhost:8080/. The wasm module exposes a single global,
window.hyprlang2lua.convert(src), returning { lua, translated, passthrough, flagged, coverage, notes, error }.
Deploy
The browser build is deployed to GitHub Pages by .github/workflows/pages.yml
on every push to main/master that touches the converter, web/, or the
module graph. The workflow builds main.wasm from source, stages the static
assets into site/, and hands them to actions/deploy-pages@v4.
One-time repo configuration: Settings → Pages → Build and deployment → Source must be set to "GitHub Actions" (not "Deploy from a branch"). Without that, the deploy step fails with a 404. Trigger manually via the Actions tab → pages → Run workflow if a redeploy is needed without a code change.
Tests
go test ./...
go test ./internal/converter -fuzz FuzzConvert -fuzztime 30s # quick fuzz
go test ./internal/converter -run TestGolden -update # refresh goldens
Golden fixtures live in internal/converter/testdata/; each .conf is
paired with the expected .lua output. FuzzConvert exercises the lexer
and parser against random byte sequences to catch panics.
Authoritative sources
Mappings were derived from, in priority order:
/usr/share/hypr/stubs/hl.meta.lua— the autogenerated Lua API stubs shipped with Hyprland 0.55 (definitive list ofhl.*functions, theHL.ConfigKeyset, and every*Spectype)./usr/share/hypr/hyprland.lua— the shipped example, used as a style reference for idiomatic table layout.- The Hyprland wiki and release notes for legacy hyprlang field names.
License
MIT.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
hyprlang2lua
command
hyprlang2lua converts a hyprlang (.conf) file to Hyprland 0.55+ Lua.
|
hyprlang2lua converts a hyprlang (.conf) file to Hyprland 0.55+ Lua. |
|
internal
|
|
|
converter
Package converter translates hyprlang (.conf) source into the Hyprland 0.55+ Lua configuration format.
|
Package converter translates hyprlang (.conf) source into the Hyprland 0.55+ Lua configuration format. |
|
web
|
|
|
wasm
command
|