M config.def.h => config.def.h +2 -2
@@ 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)
A gen-rules.py => gen-rules.py +142 -0
@@ 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))
M patch/dwmc => patch/dwmc +0 -0
M => +0 -0
A rules.gen.h => rules.gen.h +8 -0
@@ 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)
A rules.json => rules.json +14 -0
@@ 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)"
+ }
+]