From 9d111695d836f7a8edb2884da93497b488c57422 Mon Sep 17 00:00:00 2001 From: Kris Yotam <75515498+krisyotam@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:18:57 -0500 Subject: [PATCH] chore: sync local state after restore (push updates, no pull) --- config.def.h | 4 +- gen-rules.py | 142 ++++++++++++++++++++++++++++++++++++++++++++ patch/dwmc | 0 patch/layoutmenu.sh | 0 rules.gen.h | 8 +++ rules.json | 14 +++++ 6 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 gen-rules.py mode change 100755 => 100644 patch/dwmc mode change 100755 => 100644 patch/layoutmenu.sh create mode 100644 rules.gen.h create mode 100644 rules.json diff --git a/config.def.h b/config.def.h index e021d97e35c191abb9b78b30c0877790ab36eb96..df95f838b8aff03711f9e5b491cf4deb5f34ed59 100644 --- a/config.def.h +++ b/config.def.h @@ -533,13 +533,13 @@ static const Rule rules[] = { RULE(.wintype = WTYPE "UTILITY", .isfloating = 1) RULE(.wintype = WTYPE "TOOLBAR", .isfloating = 1) RULE(.wintype = WTYPE "SPLASH", .isfloating = 1) - RULE(.class = "Gimp", .tags = 1 << 8) + /* App tag rules: edit rules.json (no monitor — lands on selmon). Rebuild after changes. */ + #include "rules.gen.h" #if SWALLOW_PATCH RULE(.class = "St", .isterminal = 1) RULE(.title = "Event Tester", .noswallow = 1) RULE(.instance = "floatterm", .class = "St", .isfloating = 1, .isterminal = 1) RULE(.instance = "fzfmenu", .class = "St", .isfloating = 1, .isterminal = 1) - RULE(.instance = "bg", .class = "St", .tags = 1 << 7, .isterminal = 1) #endif // SWALLOW_PATCH #if RENAMED_SCRATCHPADS_PATCH RULE(.instance = "spterm", .scratchkey = 's', .isfloating = 1) diff --git a/gen-rules.py b/gen-rules.py new file mode 100644 index 0000000000000000000000000000000000000000..d27ad1ee92dc4fe7f1e43cd33b78552162264d99 --- /dev/null +++ b/gen-rules.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Compile rules.json into C RULE(...) macros for dwm. + +Tag numbers are 1-based (tag 1 .. NUMTAGS). Monitor is intentionally omitted: +windows land on selmon (wherever you focused / put the pointer). + +Usage: + ./gen-rules.py [rules.json] > rules.gen.h + ./gen-rules.py rules.json rules.gen.h +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +# Fields we never emit from JSON (per design). +BLOCKED = frozenset({"monitor", "mon"}) + + +def c_string(s: str) -> str: + """Escape a Python str as a C string literal.""" + out = [] + for ch in s: + if ch == "\\": + out.append("\\\\") + elif ch == '"': + out.append('\\"') + elif ch == "\n": + out.append("\\n") + elif ch == "\t": + out.append("\\t") + elif ord(ch) < 32: + out.append(f"\\x{ord(ch):02x}") + else: + out.append(ch) + return '"' + "".join(out) + '"' + + +def tag_mask(tag: int) -> str: + if not isinstance(tag, int) or tag < 1: + raise ValueError(f"tag must be integer >= 1, got {tag!r}") + # tag 1 => 1 << 0, tag 9 => 1 << 8 + return f"1 << {tag - 1}" + + +def rule_to_c(rule: dict, index: int) -> str: + if not isinstance(rule, dict): + raise ValueError(f"rule[{index}] must be an object") + + for bad in BLOCKED: + if bad in rule: + raise ValueError( + f"rule[{index}]: '{bad}' is not allowed " + "(select the monitor yourself before launching)" + ) + + parts: list[str] = [] + + if "class" in rule and rule["class"] is not None: + parts.append(f".class = {c_string(str(rule['class']))}") + if "instance" in rule and rule["instance"] is not None: + parts.append(f".instance = {c_string(str(rule['instance']))}") + if "title" in rule and rule["title"] is not None: + parts.append(f".title = {c_string(str(rule['title']))}") + if "wintype" in rule and rule["wintype"] is not None: + # Expect short name like DIALOG, or full if already WTYPE-prefixed handling + wt = str(rule["wintype"]) + if wt.startswith("WTYPE ") or wt.startswith("_NET_"): + parts.append(f".wintype = {c_string(wt)}") + else: + parts.append(f'.wintype = WTYPE "{wt}"') + + if "tag" in rule and rule["tag"] is not None: + parts.append(f".tags = {tag_mask(int(rule['tag']))}") + + if rule.get("floating"): + parts.append(".isfloating = 1") + if rule.get("terminal"): + parts.append(".isterminal = 1") + if rule.get("noswallow"): + parts.append(".noswallow = 1") + + if "floatpos" in rule and rule["floatpos"] is not None: + parts.append(f".floatpos = {c_string(str(rule['floatpos']))}") + + if not parts: + raise ValueError(f"rule[{index}]: empty rule (need class/instance/title/tag/...)") + + comment = rule.get("comment") + line = "RULE(" + ", ".join(parts) + ")" + if comment: + # single-line C comment only + safe = str(comment).replace("*/", "* /").replace("\n", " ") + return f"/* {safe} */\n{line}" + return line + + +def generate(rules_path: Path) -> str: + data = json.loads(rules_path.read_text(encoding="utf-8")) + if not isinstance(data, list): + raise ValueError("rules.json must be a JSON array") + + lines = [ + "/* Generated by gen-rules.py from rules.json — do not edit by hand. */", + "/* Edit rules.json, then rebuild dwm. Monitor is never set (selmon). */", + "", + ] + for i, rule in enumerate(data): + lines.append(rule_to_c(rule, i)) + lines.append("") + + return "\n".join(lines).rstrip() + "\n" + + +def main(argv: list[str]) -> int: + if len(argv) >= 2 and argv[1] in ("-h", "--help"): + print(__doc__.strip()) + return 0 + + src = Path(argv[1] if len(argv) >= 2 else "rules.json") + if not src.is_file(): + print(f"gen-rules: not found: {src}", file=sys.stderr) + return 1 + + try: + text = generate(src) + except (json.JSONDecodeError, ValueError, TypeError) as e: + print(f"gen-rules: {e}", file=sys.stderr) + return 1 + + if len(argv) >= 3: + out = Path(argv[2]) + out.write_text(text, encoding="utf-8") + else: + sys.stdout.write(text) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/patch/dwmc b/patch/dwmc old mode 100755 new mode 100644 diff --git a/patch/layoutmenu.sh b/patch/layoutmenu.sh old mode 100755 new mode 100644 diff --git a/rules.gen.h b/rules.gen.h new file mode 100644 index 0000000000000000000000000000000000000000..507b2a3c68ec356f525d513eeee6964201f26ef5 --- /dev/null +++ b/rules.gen.h @@ -0,0 +1,8 @@ +/* Generated by gen-rules.py from rules.json — do not edit by hand. */ +/* Edit rules.json, then rebuild dwm. Monitor is never set (selmon). */ + +/* GNU Image Manipulation Program */ +RULE(.class = "Gimp", .tags = 1 << 8) + +/* st -n bg (background/workspace terminal) */ +RULE(.class = "St", .instance = "bg", .tags = 1 << 7, .isterminal = 1) diff --git a/rules.json b/rules.json new file mode 100644 index 0000000000000000000000000000000000000000..44da22a5a2ccbaf76a1249ab2ff19c04a43d0863 --- /dev/null +++ b/rules.json @@ -0,0 +1,14 @@ +[ + { + "class": "Gimp", + "tag": 9, + "comment": "GNU Image Manipulation Program" + }, + { + "class": "St", + "instance": "bg", + "tag": 8, + "terminal": true, + "comment": "st -n bg (background/workspace terminal)" + } +]