From c76bfc018b6423644808e4f13ca3c1459e6437e7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:02:10 +0200 Subject: [PATCH] ci: auto-sync GitHub wiki module pages from man pages Add tooling that makes the scdoc man pages the single source of truth and regenerates the wiki module pages from them: - .github/wiki/mapping.json: man-page -> wiki-page mapping (aggregation-aware) - .github/wiki/generate.py: scdoc -> pandoc -> gfm, strips man-only sections, concatenates aggregated pages, appends optional extras/.md - .github/wiki/extras/: hand-kept appendices (screenshots) with no man equivalent - .github/workflows/wiki.yml: on push to man/** (or the tooling), regenerate and push the wiki; other wiki pages are left untouched Covers all 65 man pages -> 45 wiki pages (5 new: GPS, Inhibitor, Mango, Menu, WWAN). Non-module and hand-written wiki pages are never modified. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/wiki/README.md | 42 ++++++++ .github/wiki/extras/Module:-Clock.md | 3 + .github/wiki/extras/Module:-Hyprland.md | 6 ++ .github/wiki/generate.py | 137 ++++++++++++++++++++++++ .github/wiki/mapping.json | 49 +++++++++ .github/workflows/wiki.yml | 56 ++++++++++ 6 files changed, 293 insertions(+) create mode 100644 .github/wiki/README.md create mode 100644 .github/wiki/extras/Module:-Clock.md create mode 100644 .github/wiki/extras/Module:-Hyprland.md create mode 100755 .github/wiki/generate.py create mode 100644 .github/wiki/mapping.json create mode 100644 .github/workflows/wiki.yml diff --git a/.github/wiki/README.md b/.github/wiki/README.md new file mode 100644 index 00000000..6588bec4 --- /dev/null +++ b/.github/wiki/README.md @@ -0,0 +1,42 @@ +# Wiki sync + +The scdoc man pages under [`man/`](../../man) are the **single source of truth** +for module documentation. The GitHub wiki module pages are generated from them by +[`generate.py`](generate.py) and kept in sync automatically by the +[`wiki.yml`](../workflows/wiki.yml) workflow on every push to `master` that +touches `man/**` or this tooling. + +## Files + +- `mapping.json` — maps each wiki page to the ordered list of man-page basenames + that compose it. Several man pages are concatenated into one aggregated page + (e.g. `Module:-Hyprland` ← the four `waybar-hyprland-*` pages). **Add an entry + here when you add a new man page** — the workflow fails the mapping check + otherwise. +- `generate.py` — `scdoc → roff → pandoc → gfm`, strips the `NAME`/`FILES`/`AUTHOR` + man sections, and writes one `Module:-*.md` per mapping entry. +- `extras/.md` — optional hand-maintained appendix (screenshots, showcase + snippets that have no man equivalent), appended verbatim after the generated body. + +## What it touches + +Only the pages listed in `mapping.json` are (re)written. Every other wiki page +(`Home`, `Installation`, user showcases, the hand-written `Module:-Cava:-GLSL`, +`Module:-Group`, `Module:-Load`, …) is left untouched. + +## To edit a module's docs + +Edit the man page under `man/`, not the wiki. The wiki page is overwritten on the +next sync. Put anything with no man equivalent (images, etc.) in `extras/`. + +## Run locally + +```sh +python3 .github/wiki/generate.py --check # validate mapping only +python3 .github/wiki/generate.py --out-dir /tmp/wiki-out # generate a preview +``` + +## One-time setup + +The repository wiki must be enabled (Settings → Features → Wikis) with at least +one initial page so the `.wiki.git` remote exists. diff --git a/.github/wiki/extras/Module:-Clock.md b/.github/wiki/extras/Module:-Clock.md new file mode 100644 index 00000000..02c31a8a --- /dev/null +++ b/.github/wiki/extras/Module:-Clock.md @@ -0,0 +1,3 @@ +## Screenshots + +![calendar](https://github.com/Alexays/Waybar/assets/6098822/e5c38984-6b3c-43ef-8cac-05122a078d58) diff --git a/.github/wiki/extras/Module:-Hyprland.md b/.github/wiki/extras/Module:-Hyprland.md new file mode 100644 index 00000000..ea1a8046 --- /dev/null +++ b/.github/wiki/extras/Module:-Hyprland.md @@ -0,0 +1,6 @@ +## Screenshots + +![](https://user-images.githubusercontent.com/25804378/270065881-acef7b73-e528-4648-82c1-ba70ea9f7804.png) +![Workspace taskbars example](https://github.com/user-attachments/assets/c5387d8b-6612-442a-83af-2a0d881fa6db) +![image](https://github.com/Alexays/Waybar/assets/1778670/6421e9ce-50be-4086-8045-688338d5fbe7) +![image](https://github.com/Alexays/Waybar/assets/1778670/48c58198-aae4-42ac-802a-33b1e3db5e33) diff --git a/.github/wiki/generate.py b/.github/wiki/generate.py new file mode 100755 index 00000000..7e84e0d9 --- /dev/null +++ b/.github/wiki/generate.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Generate GitHub wiki pages from the scdoc man pages. + +The man pages under man/ are the single source of truth for module +documentation. This script converts them to GitHub-flavoured Markdown +(scdoc -> roff -> pandoc -> gfm) and writes one wiki page per entry in +mapping.json, concatenating several man pages into one page where the +wiki keeps an aggregated page (e.g. Module:-Hyprland). + +Only the pages listed in mapping.json are (re)written; every other wiki +page (Home, Installation, user showcases, ...) is left untouched. + +Usage: + generate.py --man-dir man --out-dir [--check] + +Requires: scdoc, pandoc. +""" +import argparse +import json +import os +import re +import subprocess +import sys + +# Man-page sections that are meaningless on the wiki and must be dropped. +DROP_SECTIONS = {"NAME", "FILES", "AUTHOR", "AUTHORS"} + +HEADING_RE = re.compile(r"^(#+)\s+(.*)$") + + +def sh(cmd, stdin=None): + return subprocess.run( + cmd, input=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True, + ).stdout + + +def man_to_gfm(scd_path): + """scdoc -> roff -> pandoc gfm, as raw markdown text.""" + roff = sh(["scdoc"], stdin=open(scd_path, "rb").read()) + gfm = sh(["pandoc", "-f", "man", "-t", "gfm", "--wrap=none"], stdin=roff) + return gfm.decode("utf-8") + + +def strip_sections(md): + """Remove top-level man-only sections (NAME/FILES/AUTHOR).""" + out, drop = [], False + for line in md.splitlines(): + m = HEADING_RE.match(line) + if m and len(m.group(1)) == 1: + drop = m.group(2).strip().upper() in DROP_SECTIONS + if not drop: + out.append(line) + return "\n".join(out).strip("\n") + + +def demote(md, levels=1): + """Add `levels` extra '#' to every heading (for aggregated pages).""" + out = [] + for line in md.splitlines(): + m = HEADING_RE.match(line) + out.append("#" * levels + line if m else line) + return "\n".join(out) + + +def pretty(basename): + """waybar-sway-mode -> sway/mode (submodule heading for aggregated pages).""" + return basename.removeprefix("waybar-").replace("-", "/", 1) + + +def build_page(man_dir, sources, page, extras_dir): + parts = [] + banner = ("") + parts.append(banner) + aggregated = len(sources) > 1 + for src in sources: + scd = os.path.join(man_dir, src + ".5.scd") + body = strip_sections(man_to_gfm(scd)) + if aggregated: + parts.append(f"\n# {pretty(src)}\n\n" + demote(body)) + else: + parts.append("\n" + body) + # Optional hand-maintained appendix (screenshots, showcase snippets) that + # has no man-page equivalent: .github/wiki/extras/.md, appended verbatim. + extra = os.path.join(extras_dir, page + ".md") + if os.path.exists(extra): + parts.append("\n" + open(extra).read().strip("\n")) + return "\n".join(parts).rstrip() + "\n" + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--man-dir", default="man") + ap.add_argument("--out-dir") + ap.add_argument("--mapping", default=os.path.join(os.path.dirname(__file__), "mapping.json")) + ap.add_argument("--check", action="store_true", + help="Only validate the mapping vs man/, write nothing.") + args = ap.parse_args() + + mapping = {k: v for k, v in json.load(open(args.mapping)).items() + if not k.startswith("_")} + + # Validate: every man page mapped exactly once; every source exists. + mapped, errors = {}, [] + for page, sources in mapping.items(): + for s in sources: + if not os.path.exists(os.path.join(args.man_dir, s + ".5.scd")): + errors.append(f"{page}: man/{s}.5.scd does not exist") + if s in mapped: + errors.append(f"{s} mapped to both {mapped[s]} and {page}") + mapped[s] = page + on_disk = {f[:-6] for f in os.listdir(args.man_dir) if f.endswith(".5.scd")} + for s in sorted(on_disk - set(mapped)): + errors.append(f"man/{s}.5.scd is not referenced in mapping.json") + if errors: + print("Mapping errors:\n " + "\n ".join(errors), file=sys.stderr) + return 1 + print(f"Mapping OK: {len(on_disk)} man pages -> {len(mapping)} wiki pages") + if args.check: + return 0 + if not args.out_dir: + print("--out-dir is required unless --check", file=sys.stderr) + return 2 + + extras_dir = os.path.join(os.path.dirname(args.mapping), "extras") + os.makedirs(args.out_dir, exist_ok=True) + for page, sources in mapping.items(): + out = os.path.join(args.out_dir, page + ".md") + open(out, "w").write(build_page(args.man_dir, sources, page, extras_dir)) + print(f" wrote {page}.md ({', '.join(sources)})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/wiki/mapping.json b/.github/wiki/mapping.json new file mode 100644 index 00000000..1bee1bc3 --- /dev/null +++ b/.github/wiki/mapping.json @@ -0,0 +1,49 @@ +{ + "_comment": "Maps each wiki page to the ordered list of man-page basenames that compose it. Edit this when adding a module or man page. The generator (generate.py) turns each man/.5.scd into the wiki page named by the key. Keys use the exact wiki page title (spaces become dashes in the file name, ':' is kept).", + + "Module:-Backlight": ["waybar-backlight"], + "Module:-Backlight-Slider": ["waybar-backlight-slider"], + "Module:-Battery": ["waybar-battery"], + "Module:-Bluetooth": ["waybar-bluetooth"], + "Module:-Cava": ["waybar-cava"], + "Module:-CFFI": ["waybar-cffi"], + "Module:-Clock": ["waybar-clock"], + "Module:-CPU": ["waybar-cpu", "waybar-cpu-graph"], + "Module:-Custom": ["waybar-custom", "waybar-custom-graph"], + "Module:-Disk": ["waybar-disk"], + "Module:-Dwl": ["waybar-dwl-tags", "waybar-dwl-window"], + "Module:-Gamemode": ["waybar-gamemode"], + "Module:-GPS": ["waybar-gps"], + "Module:-Hyprland": ["waybar-hyprland-window", "waybar-hyprland-workspaces", "waybar-hyprland-submap", "waybar-hyprland-windowcount"], + "Module:-Idle-Inhibitor": ["waybar-idle-inhibitor"], + "Module:-Image": ["waybar-image"], + "Module:-Inhibitor": ["waybar-inhibitor"], + "Module:-JACK": ["waybar-jack"], + "Module:-Keyboard-State": ["waybar-keyboard-state"], + "Module:-Language": ["waybar-sway-language", "waybar-hyprland-language", "waybar-niri-language", "waybar-mango-language"], + "Module:-Mango": ["waybar-mango-window", "waybar-mango-workspaces", "waybar-mango-keymode", "waybar-mango-layout"], + "Module:-Memory": ["waybar-memory"], + "Module:-Menu": ["waybar-menu"], + "Module:-MPD": ["waybar-mpd"], + "Module:-MPRIS": ["waybar-mpris"], + "Module:-Network": ["waybar-network"], + "Module:-Niri": ["waybar-niri-window", "waybar-niri-workspaces"], + "Module:-PowerProfilesDaemon": ["waybar-power-profiles-daemon"], + "Module:-Privacy": ["waybar-privacy"], + "Module:-PulseAudio": ["waybar-pulseaudio"], + "Module:-PulseAudio-Slider": ["waybar-pulseaudio-slider"], + "Module:-River": ["waybar-river-tags", "waybar-river-mode", "waybar-river-window", "waybar-river-layout"], + "Module:-Sndio": ["waybar-sndio"], + "Module:-Sway": ["waybar-sway-workspaces", "waybar-sway-window", "waybar-sway-mode", "waybar-sway-scratchpad"], + "Module:-Systemd-failed-units": ["waybar-systemd-failed-units"], + "Module:-Taskbar": ["waybar-wlr-taskbar"], + "Module:-Temperature": ["waybar-temperature"], + "Module:-Tray": ["waybar-tray"], + "Module:-UPower": ["waybar-upower"], + "Module:-User": ["waybar-user"], + "Module:-Wayfire": ["waybar-wayfire-window", "waybar-wayfire-workspaces"], + "Module:-WirePlumber": ["waybar-wireplumber"], + "Module:-Workspaces": ["waybar-ext-workspaces"], + "Module:-WWAN": ["waybar-wwan"], + "States": ["waybar-states"] +} diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml new file mode 100644 index 00000000..8875ca81 --- /dev/null +++ b/.github/workflows/wiki.yml @@ -0,0 +1,56 @@ +name: Sync wiki from man pages + +# The scdoc man pages under man/ are the single source of truth for module +# documentation. This workflow regenerates the corresponding GitHub wiki pages +# whenever a man page (or the sync tooling) changes on the default branch. +# +# Only the pages listed in .github/wiki/mapping.json are (re)written; every +# other wiki page (Home, Installation, user showcases, ...) is left untouched. + +on: + push: + branches: [master] + paths: + - 'man/**' + - '.github/wiki/**' + - '.github/workflows/wiki.yml' + workflow_dispatch: {} + +permissions: + contents: write + +concurrency: + group: wiki-sync + cancel-in-progress: true + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install scdoc and pandoc + run: sudo apt-get update && sudo apt-get install -y scdoc pandoc + + - name: Clone the wiki + run: | + git clone \ + "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.wiki.git" \ + wiki + + - name: Generate wiki pages from man pages + run: python3 .github/wiki/generate.py --man-dir man --out-dir wiki + + - name: Commit and push if changed + working-directory: wiki + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --staged --quiet; then + echo "Wiki already up to date." + else + git commit -m "docs: sync module pages from man (${{ github.sha }})" + git push + fi