#!/usr/bin/env python3 """Hyprland windowrule dead-syntax guard (flake check `windowrule-syntax`). Usage: check-windowrule-syntax.py Hyprland 0.53 rewrote window rules (hyprlang `handleWindowrule`): the `windowrulev2` keyword is a hard error, and the old rule-first order (`windowrule = float, class:^…$`) no longer parses. Both surface as a red config-error banner on every session start and silently drop the rules (fixed in ed7fd93). The current, correct grammar is `windowrule = , match: ^…$` — e.g. `windowrule = float 1, match:class ^…$`. This guards the GENERATED hyprland.conf against a regression to the dead syntax, catching three markers: 1. the `windowrulev2` keyword anywhere; 2. rule-first order — an effect keyword directly followed by a comma (`windowrule = float, …` / `center, …` / `size, …`), i.e. no value; 3. a bare `:` matcher (`class:`, `title:`, …) instead of the `match:` form — this also catches an effect that DOES carry a value but kept the old matcher (`size 60% 65%, class:^…$`). The current correct config must PASS; any of the above must FAIL. """ import re import sys conf = sys.argv[1] lines = open(conf).read().splitlines() # Effect keywords that in the dead grammar sat directly before the comma. # Effect keywords that in the dead grammar sat directly before the comma. # Hyprland 0.55 renames: stayfocused → stay_focused (underscore). Keep the # old name in the dead-grammar list so a regression to bare `stayfocused,` # still fails the check. EFFECTS = ( "float", "center", "tile", "pin", "fullscreen", "maximize", "size", "move", "opacity", "workspace", "monitor", "fullscreenstate", "group", "stayfocused", "stay_focused", "pseudo", "immediate", ) rule_first = re.compile( r"windowrule\s*=\s*(?:" + "|".join(EFFECTS) + r")\s*," ) # A window-property matcher written the old (colon-suffixed) way. The new # grammar spells these `match:class …` — "class" is then followed by a # space, never a colon — so a bare `class:`/`title:` only ever appears in # the dead form. bare_matcher = re.compile( r"\b(?:initialclass|initialtitle|class|title|tag|xwayland|floating)\s*:", re.IGNORECASE, ) failures = [] for n, line in enumerate(lines, 1): stripped = line.strip() if stripped.startswith("#"): continue if "windowrulev2" in line: failures.append((n, "dead `windowrulev2` keyword", stripped)) continue if not re.match(r"\s*windowrule\s*=", line): continue if rule_first.search(line): failures.append((n, "rule-first order (effect directly before comma)", stripped)) if bare_matcher.search(line): failures.append((n, "bare `:` matcher — expected `match:`", stripped)) if failures: print(f"windowrule-syntax: {len(failures)} dead-syntax rule(s) in {conf}") for n, why, text in failures: print(f" line {n}: {why}\n {text}") sys.exit(1) rules = sum(1 for l in lines if re.match(r"\s*windowrule\s*=", l)) print(f"windowrule-syntax: {rules} windowrule line(s), all post-0.53 grammar")