~kris/suckless

dwm

9d111695d836f7a8edb2884da93497b488c57422 — Kris Yotam a month ago ecba1fc main
chore: sync local state after restore (push updates, no pull)
6 files changed, 166 insertions(+), 2 deletions(-)

M config.def.h
A gen-rules.py
M patch/dwmc -rwxr-xr-x => -rw-r--r--
M patch/layoutmenu.sh -rwxr-xr-x => -rw-r--r--
A rules.gen.h
A rules.json
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 patch/layoutmenu.sh => patch/layoutmenu.sh +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)"
  }
]