Skip to content

feat(converter): windowrule v2 matchers, hyphenated $variables, legacy-syntax notes - #11

Merged
EIonTusk merged 6 commits into
masterfrom
fix/windowrule-v2-and-hyphenated-vars
Sep 30, 2026
Merged

EIonTusk merged 6 commits into
masterfrom
fix/windowrule-v2-and-hyphenated-vars

Conversation

@EIonTusk

@EIonTusk EIonTusk commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

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: windowrule with v2 matchers

Since Hyprland 0.48, windowrule parses exactly like windowrulev2. The converter still read every windowrule line as v1, so windowrule = float, class:^(big)$ matched the literal class class:^(big)$, and lines with several matchers lost them all to TODOs.

  • classifyWindowRuleField now reads KEY:VALUE matchers for both keywords.
  • A windowrule line with no matcher still takes the v1 path, so windowrule = float, ^(kitty)$ keeps its class regex.
  • v1's 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-glass but found references with an identifier scan, so they were left as "$looking-glass". Inside a value they became gnome .. "-schema", which makes Hyprland reject the whole config.

  • New varScope (declared names sorted longest-first, name → Lua local map, and an undeclared-ref policy) replaces the map[string]bool / nil pair taken by formatValue / interpolate.
  • At each $, the longest declared name wins, as in hyprlang. When nothing declared matches, behaviour is unchanged: value contexts rewrite an undeclared $X to a Lua local (fails fast on a typo), and shell contexts keep $HOME literal.
  • fmtVal used to pass nil and so could never see a hyphenated name. It now uses the real scope, as do dispatcher args and bind modifiers ($main-mod SHIFT).
  • The Lua local map fixes two latent collisions: $a-b and $a_b no longer alias (a_b, a_b_2), and $end no longer emits local end.

Behaviour note: because matching follows hyprlang's substring rule, $mainModX with $mainMod declared now becomes mainMod .. "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 to xdg_tag.

  • Verbatim regression tests (e8b07a6). TestIssueRegressions runs 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 -update never rewrites. It also requires no TODOs and valid Lua. On master the three bug cases fail.

  • Legacy-syntax report notes (4967ee1). A bare-regex windowrule still 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 a legacy=N count (CLI), or a "legacy" tag (web UI). Legacy notes are informational: they don't count as flagged or trip --check. ReportNote gains a Kind field. The wasm build is rebuilt by wasm.yml after merge.

  • Variables that carry rule syntax (898bca0). windowrule = float, $center-float-large with $center-float-large = class:^(nautilus)$ gave a TODO, because hyprlang substitutes before parsing and the converter didn't. In windowrule / windowrulev2 / layerrule lines, 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 = $a and 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 in TestIssueRegressions.

Testing

Closes #9
Closes #10

EIonTusk 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.
@EIonTusk EIonTusk changed the title fix(converter): windowrule v2 matchers and hyphenated $variables feat(converter): windowrule v2 matchers, hyphenated $variables, legacy-syntax notes Sep 30, 2026
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.
@EIonTusk
EIonTusk merged commit fe71dbd into master Sep 30, 2026
1 check passed
@EIonTusk
EIonTusk deleted the fix/windowrule-v2-and-hyphenated-vars branch September 30, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Variables whose names contain '-' are declared but never substituted windowrule with v2 matchers (Hyprland >= 0.48) is converted as a v1 rule

1 participant