Repository navigation
feat(converter): windowrule v2 matchers, hyphenated $variables, legacy-syntax notes - #11
Merged
Merged
Conversation
added 5 commits
September 30, 2026 13:57
Hyprland 0.48 removed the v1 windowrule grammar: `windowrule` now parses exactly like `windowrulev2`. The converter still treated every `windowrule` line as v1 (an effect plus one bare class regex), so windowrule = float, class:^(big)$ became a rule matching the literal class "class:^(big)$", and a line with more than one matcher lost all of them to "unmapped window rule action" TODOs. Only the `windowrulev2` spelling converted correctly. classifyWindowRuleField now recognizes KEY:VALUE matchers for both keywords, and drops its v2 parameter. A `windowrule` line with no matcher at all still takes the v1 path, so `windowrule = float, ^(kitty)$` keeps its trailing field as the class regex. v1 also accepted a `title:` window field, which means the same thing as the v2 matcher, so no rule that used to match changes meaning. Closes #9.
hyprlang has no identifier grammar for variable names. `$name = value`
declares everything between '$' and '=' (CConfig::parseVariable stores
lhs.substr(1)), and later lines are expanded by replacing each "$"+name
substring, longest declared name first. The converter declared
`$looking-glass` but found references with an identifier scan that
stops at '-', so
bind = SUPER, W, exec, $looking-glass
became exec_cmd("$looking-glass"), which the shell reads as $looking
followed by "-glass". Inside another variable's value it was worse:
`$gnome-schema` became `gnome .. "-schema"`, a concat with an undefined
global that makes Hyprland reject the whole config.
A new varScope carries the declared names (longest first) and replaces
the map[string]bool / nil pair that formatValue and interpolate took. At
each '$' the longest declared name the text continues with wins, which is
hyprlang's own lookup, so `$center-float-large` beats `$center-float`.
When nothing declared matches, behaviour is unchanged: value contexts
still rewrite an identifier-shaped $X to a Lua local so a typo fails at
load, and shell contexts keep $HOME for /bin/sh. fmtVal used to pass a
nil set and so could never see a hyphenated name; it now uses the real
scope, as do the dispatcher arguments and bind modifiers
(`$main-mod SHIFT`).
The scope also owns the name -> Lua local mapping. `$a-b` and `$a_b`
used to both become `a_b` and alias one another; a later colliding name
now gets a numeric suffix. luaIdent also steps off Lua keywords, so
`$end` no longer emits `local end`.
Closes #10.
Hyprland 0.49 through 0.52 accepted `xdgTag:` as a compact matcher
(CConfigManager's rule parser, `get("xdgTag")`), which 0.53 renamed to
xdg_tag. normalizeMatchKey knew only the new spelling, so
`windowrule = float, xdgTag:^(portal)$` left the matcher as an
"unmapped window rule action" TODO.
Checked against the parsers of every release from 0.48 to 0.52; this was
the only compact key missing.
TestIssueRegressions feeds the "Input" blocks of #9 and #10, plus the two windowrule lines #9 quotes in its text (the wiki's XWayland drag rule and the bare-regex v1 form), through Convert and compares against hand-written expected output. It also requires zero TODO flags and, when luac is available, valid Lua. Unlike the goldens, `-update` never rewrites these expectations, so a regression can't be accepted by regenerating files. Against master the three bug cases fail; the v1 case passes and guards the fallback.
Some hyprlang configs carry lines from before a syntax change that Hyprland later rejected. The converter still translates them to their original meaning, but the person converting has no way to know that the line was dead on their Hyprland and the Lua rule will now take effect. Report gains a Legacy count and ReportNote a Kind (flagged or legacy). A legacy note is informational: it doesn't count toward Flagged, leave a TODO in the output, change the line's gutter class, or trip --check. First (and so far only verified) case: a `windowrule` whose window field is a bare regex. That was the v1 grammar; Hyprland 0.48 and later reject the line with "Invalid rulev2 syntax" and drop the rule. - CLI --report adds `legacy=N` to the summary when non-zero and tags each note `(legacy)`; output for configs without legacy lines is unchanged. - The wasm shim passes `legacy` and each note's `kind`; the web UI shows a "legacy" tag on those notes. - The issue #9 regression case now also pins the note to its line.
hyprlang substitutes variables into the raw line before the rule parser runs, so a variable can hold rule syntax: $center-float-large = class:^(nautilus)$ windowrule = float, $center-float-large is the rule `float, class:^(nautilus)$`. The converter classified the unexpanded `$center-float-large` field as an effect and emitted an "unmapped window rule action" TODO. #10 reports this pattern in published configs. For windowrule, windowrulev2 and layerrule, a comma-separated field that is exactly one declared reference is now replaced by the variable's value, re-split on commas (one variable may hold several fields) and expanded again. The value used is the latest declaration above the line, as in hyprlang; a name only declared further down resolves to its first declaration, the converter's usual forward-reference leniency. Partial references (`class:$cls`) are untouched and still become Lua locals. Expansion is bounded in depth (so `$a = $a` terminates, as hyprlang's own loop cap does) and in total fields (so a doubling `$a = $b, $b` chain can't blow up on untrusted input in the web converter). varScope now records each declaration's value and line, and newVarScopes takes varDefs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes two conversion bugs reported from a run over ~200 published configs, with one commit per issue, plus follow-ups found while checking the fixes against Hyprland's source.
#9:
windowrulewith v2 matchersSince Hyprland 0.48,
windowruleparses exactly likewindowrulev2. The converter still read everywindowruleline as v1, sowindowrule = float, class:^(big)$matched the literal classclass:^(big)$, and lines with several matchers lost them all to TODOs.classifyWindowRuleFieldnow readsKEY:VALUEmatchers for both keywords.windowruleline with no matcher still takes the v1 path, sowindowrule = float, ^(kitty)$keeps its class regex.title:window field meant the same as the v2 matcher, so no rule that used to match changes meaning.#10: variables whose names contain
-hyprlang declares everything between
$and=as the name, and expands references by substring, longest declared name first (src/config.cpp). The converter declared$looking-glassbut found references with an identifier scan, so they were left as"$looking-glass". Inside a value they becamegnome .. "-schema", which makes Hyprland reject the whole config.varScope(declared names sorted longest-first, name → Lua local map, and an undeclared-ref policy) replaces themap[string]bool/nilpair taken byformatValue/interpolate.$, the longest declared name wins, as in hyprlang. When nothing declared matches, behaviour is unchanged: value contexts rewrite an undeclared$Xto a Lua local (fails fast on a typo), and shell contexts keep$HOMEliteral.fmtValused to passniland so could never see a hyphenated name. It now uses the real scope, as do dispatcher args and bind modifiers ($main-mod SHIFT).$a-band$a_bno longer alias (a_b,a_b_2), and$endno longer emitslocal end.Behaviour note: because matching follows hyprlang's substring rule,
$mainModXwith$mainModdeclared now becomesmainMod .. "X". That is what hyprlang itself does, and it is pinned in a test.Follow-ups in this PR
xdgTag:matcher (82f529e). I checked the converter's matcher keys against Hyprland's rule parser in every release from 0.48 to 0.52 and against the current wiki.xdgTag:(added in v0.49) was the only compact key missing. It now maps toxdg_tag.Verbatim regression tests (e8b07a6).
TestIssueRegressionsruns the issues' Input blocks and the two lines quoted in windowrule with v2 matchers (Hyprland >= 0.48) is converted as a v1 rule #9's text, and compares against hand-written expected output that-updatenever rewrites. It also requires no TODOs and valid Lua. Onmasterthe three bug cases fail.Legacy-syntax report notes (4967ee1). A bare-regex
windowrulestill converts to its v1 meaning, but Hyprland 0.48 and later rejected that line, so it had no effect there. The report now says so with a(legacy)note and alegacy=Ncount (CLI), or a "legacy" tag (web UI). Legacy notes are informational: they don't count as flagged or trip--check.ReportNotegains aKindfield. The wasm build is rebuilt bywasm.ymlafter merge.Variables that carry rule syntax (898bca0).
windowrule = float, $center-float-largewith$center-float-large = class:^(nautilus)$gave a TODO, because hyprlang substitutes before parsing and the converter didn't. Inwindowrule/windowrulev2/layerrulelines, a field that is exactly one declared reference now expands to the variable's value. The value is re-split on commas and expanded recursively, using the latest declaration above the line. Depth and total fields are capped, so$a = $aand doubling chains terminate. Partial references (class:$cls) still become Lua locals. Variables whose names contain '-' are declared but never substituted #10's example line is pinned verbatim inTestIssueRegressions.Testing
02_rules.conf(windowrule with v2 matchers (Hyprland >= 0.48) is converted as a v1 rule #9) and04_edge_cases.conf(Variables whose names contain '-' are declared but never substituted #10). Every golden also passesluac -p.go test ./...passes, and a 30sFuzzConvertrun (~1.9M execs) found nothing.Closes #9
Closes #10