krz/nfogen

A scene-style ASCII .nfo generator.

clone: git clone https://gitbay.org/krz/nfogen.git

cc082e51fff8fccc1d9d8f9285a8439edc806733

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-08-08T06:01:10Z

add group profiles with roster/couriers/boards sections (tier 3)
 README.nfo                        |  6 ++-
 examples/krz.profile.toml         | 82 +++++++++++++++++++++++++++++++++++++
 examples/nightfall-demo.cp437.nfo | 83 +++++++++++++++++++++++++++++++++++++
 examples/nightfall-demo.nfo       | 83 +++++++++++++++++++++++++++++++++++++
 examples/nightfall-demo.toml      | 31 ++++++++++++++
 nfogen.py                         | 86 +++++++++++++++++++++++++++++++++++++--
 nfogen.toml                       |  6 ++-
 7 files changed, 372 insertions(+), 5 deletions(-)

diff --git a/README.nfo b/README.nfo
index cdc2eb6..2dfe8a6 100644
--- a/README.nfo
+++ b/README.nfo
@@ -41,7 +41,11 @@
 │   LOGO: --logo art.asc places art above the box              │
 │   BANNERS: --presents  --footer   scene boilerplate          │
 │                                                              │
-│   OPTIONS: -o -c -g -s -w --style --layout --logo            │
+│   PROFILE: -p group.toml supplies logo, footer,              │
+│            members, couriers, boards, affiliates,            │
+│            news, outposts. release cfg overrides it.         │
+│                                                              │
+│   OPTIONS: -o -c -p -g -s -w --style --layout --logo         │
 │            --encoding --presents --footer -i --no-input      │
 │                                                              │
 │   Generated by nfogen itself.                                │
diff --git a/examples/krz.profile.toml b/examples/krz.profile.toml
new file mode 100644
index 0000000..d4472d2
--- /dev/null
+++ b/examples/krz.profile.toml
@@ -0,0 +1,82 @@
+# A group profile: the reusable KRZ boilerplate. Point any release config at
+# it with `profile = "examples/krz.profile.toml"` (or nfogen -p PATH) and it
+# supplies the logo, roster, boards, footer and house style. The release
+# config only carries per-release fields (title, cracker, notes, ...).
+
+group    = "KRZ"
+site     = "krz.sh"
+style    = "double"
+layout   = "panel"
+presents = true
+footer   = true
+logo     = "examples/krz-logo.asc"
+logo_encoding = "utf8"
+
+footer_text = """
+SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE
+if you enjoyed this release, buy it!
+KRZ does not accept cash or hardware of any kind.
+"""
+
+news = """
+Welcome Celestial Wizard back to the group!
+We are in need of couriers for the release rush -- especially
+those available in the daytime. Contact a senior member.
+"""
+
+members = [
+  "Avalon", "Black Spyrit", "Butcher", "The Chairman", "Darwin",
+  "Doctor No", "Eldar", "Grimstalk", "Hologram", "Intrepid", "Jack",
+  "Knightmare", "Marko Ramius", "ONYX", "Razor Blade", "Red Baron",
+  "Savage Steel", "StingRay", "The Unknown", "The Witch King",
+]
+
+[couriers]
+Director = ["The Unforgiven"]
+Senior   = ["Ur Lord", "Lord Tiamat", "Wind Walker", "Bubba"]
+Elite    = ["Shadow Phantom", "Baddog", "Load Master", "Joebee", "Azmodeus"]
+Trial    = ["Dead Pool", "Beach Bum", "Black Mage", "Ben Jammin"]
+
+[[boards]]
+name  = "Suburbia"
+role  = "World HQ"
+sysop = "The Chairman"
+phone = "ITS/PRIVATE"
+
+[[boards]]
+name  = "TerrorDome"
+role  = "Canadian HQ"
+sysop = "StingRay"
+phone = "ITS/PRIVATE"
+
+[[boards]]
+name  = "Kindergarden"
+role  = "European HQ"
+sysop = "Doctor No"
+phone = "+47-7-525156"
+
+[[boards]]
+name  = "Apocalypse"
+role  = "Courier HQ"
+sysop = "P.O.W."
+phone = "ITS/PRIVATE"
+
+[[affiliates]]
+name  = "Reign In Blood"
+role  = "Affiliate"
+sysop = "Uriel"
+phone = "718/698-1969"
+
+[[affiliates]]
+name  = "So-Krate's"
+role  = "Affiliate"
+sysop = "Philosopher"
+phone = "310/578-7226"
+
+[[affiliates]]
+name  = "The Space Zone"
+role  = "Affiliate"
+sysop = "Death Ray"
+phone = "301/416-0118"
+
+outposts = ["Infinite Darkness", "The Atomic Cafe", "Sanctuary"]
diff --git a/examples/nightfall-demo.cp437.nfo b/examples/nightfall-demo.cp437.nfo
new file mode 100644
index 0000000..c1581e1
--- /dev/null
+++ b/examples/nightfall-demo.cp437.nfo
@@ -0,0 +1,83 @@
+               _  _______   ___
+              | |/ /  _ \ \ / / |
+              | ' /| |_) \ V /| |     k r a z y   w a r e z
+              | . \|  _ < | | |_|
+              |_|\_\_| \_\|_| (_)     ------- since 1985 -------
+

+            N I G H T F A L L . P R O T O C O L . V 1 . 4 - K R Z            
+                               [ KRZ ]   krz.sh                              
+                         -={ KRZ proudly presents }=-                        

+         Release Information                     Game Information           

+  DATE       : 2026-08-08               TYPE      : GAME                    
+  CRACKER    : Lime                     PUBLISHER : Nocturne Interactive    
+  SUPPLIER   : The Witch King           FORMAT    : ISO + patch             
+  PACKAGER   : Renegade Chemist         DISKS     : 3 x CD                  
+  PROTECTION : SafeDisc + Serial        SIZE      : 1.87 GB                 
+  INSTALL    : unzip, run SETUP, ap..   FILES     : 96 x 20 MB              
+  RATING     : 9/10                     URL       : krz.sh/nightfall        

+   NOTES                                                                     
+                                                                             
+   A stealth-action thriller set in a city that never sleeps.                
+   Cracked clean -- no disc check, no online activation.                     
+                                                                             
+   Mount or burn the ISO, install, then run the KRZ patch over               
+   the game directory. Serial is in the keygen. Tested working.              

+   GREETS                                                                    
+                                                                             
+   razor-blade   jack   the-couriers   everyone-in-the-scene                 

+   GROUP NEWS                                                                
+                                                                             
+   Welcome Celestial Wizard back to the group!                               
+   We are in need of couriers for the release rush -- especially             
+   those available in the daytime. Contact a senior member.                  

+   MEMBERS                                                                   
+                                                                             
+     Avalon   Black Spyrit   Butcher   The Chairman   Darwin   Doctor No     
+     Eldar   Grimstalk   Hologram   Intrepid   Jack   Knightmare   Marko     
+   Ramius   ONYX   Razor Blade   Red Baron   Savage Steel   StingRay   The   
+                           Unknown   The Witch King                          
+                                                                             

+   COURIERS                                                                  
+                                                                             
+                                 - Director -                                
+                                The Unforgiven                               
+                                                                             
+                                  - Senior -                                 
+                 Ur Lord   Lord Tiamat   Wind Walker   Bubba                 
+                                                                             
+                                  - Elite -                                  
+          Shadow Phantom   Baddog   Load Master   Joebee   Azmodeus          
+                                                                             
+                                  - Trial -                                  
+               Dead Pool   Beach Bum   Black Mage   Ben Jammin               
+                                                                             

+   BOARDS                                                                    
+                                                                             
+   BOARD                   ROLE             SYSOP           CONTACT          
+   Suburbia                World HQ         The Chairman    ITS/PRIVATE      
+   TerrorDome              Canadian HQ      StingRay        ITS/PRIVATE      
+   Kindergarden            European HQ      Doctor No       +47-7-525156     
+   Apocalypse              Courier HQ       P.O.W.          ITS/PRIVATE      

+   AFFILIATES                                                                
+                                                                             
+   BOARD                   ROLE             SYSOP           CONTACT          
+   Reign In Blood          Affiliate        Uriel           718/698-1969     
+   So-Krate's              Affiliate        Philosopher     310/578-7226     
+   The Space Zone          Affiliate        Death Ray       301/416-0118     

+                                                                             
+             SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE             
+                     if you enjoyed this release, buy it!                    
+              KRZ does not accept cash or hardware of any kind.              
+                                                                             

diff --git a/examples/nightfall-demo.nfo b/examples/nightfall-demo.nfo
new file mode 100644
index 0000000..2b84d3a
--- /dev/null
+++ b/examples/nightfall-demo.nfo
@@ -0,0 +1,83 @@
+               _  _______   ___
+              | |/ /  _ \ \ / / |
+              | ' /| |_) \ V /| |     k r a z y   w a r e z
+              | . \|  _ < | | |_|
+              |_|\_\_| \_\|_| (_)     ------- since 1985 -------
+
+╔═════════════════════════════════════════════════════════════════════════════╗
+║            N I G H T F A L L . P R O T O C O L . V 1 . 4 - K R Z            ║
+║                               [ KRZ ]   krz.sh                              ║
+║                         -={ KRZ proudly presents }=-                        ║
+╠══════════════════════════════════════╦══════════════════════════════════════╣
+║         Release Information          ║           Game Information           ║
+╠══════════════════════════════════════╬══════════════════════════════════════╣
+║  DATE       : 2026-08-08             ║  TYPE      : GAME                    ║
+║  CRACKER    : Lime                   ║  PUBLISHER : Nocturne Interactive    ║
+║  SUPPLIER   : The Witch King         ║  FORMAT    : ISO + patch             ║
+║  PACKAGER   : Renegade Chemist       ║  DISKS     : 3 x CD                  ║
+║  PROTECTION : SafeDisc + Serial      ║  SIZE      : 1.87 GB                 ║
+║  INSTALL    : unzip, run SETUP, ap.. ║  FILES     : 96 x 20 MB              ║
+║  RATING     : 9/10                   ║  URL       : krz.sh/nightfall        ║
+╠══════════════════════════════════════╩══════════════════════════════════════╣
+║   NOTES                                                                     ║
+║                                                                             ║
+║   A stealth-action thriller set in a city that never sleeps.                ║
+║   Cracked clean -- no disc check, no online activation.                     ║
+║                                                                             ║
+║   Mount or burn the ISO, install, then run the KRZ patch over               ║
+║   the game directory. Serial is in the keygen. Tested working.              ║
+╠═════════════════════════════════════════════════════════════════════════════╣
+║   GREETS                                                                    ║
+║                                                                             ║
+║   razor-blade   jack   the-couriers   everyone-in-the-scene                 ║
+╠═════════════════════════════════════════════════════════════════════════════╣
+║   GROUP NEWS                                                                ║
+║                                                                             ║
+║   Welcome Celestial Wizard back to the group!                               ║
+║   We are in need of couriers for the release rush -- especially             ║
+║   those available in the daytime. Contact a senior member.                  ║
+╠═════════════════════════════════════════════════════════════════════════════╣
+║   MEMBERS                                                                   ║
+║                                                                             ║
+║     Avalon   Black Spyrit   Butcher   The Chairman   Darwin   Doctor No     ║
+║     Eldar   Grimstalk   Hologram   Intrepid   Jack   Knightmare   Marko     ║
+║   Ramius   ONYX   Razor Blade   Red Baron   Savage Steel   StingRay   The   ║
+║                           Unknown   The Witch King                          ║
+║                                                                             ║
+╠═════════════════════════════════════════════════════════════════════════════╣
+║   COURIERS                                                                  ║
+║                                                                             ║
+║                                 - Director -                                ║
+║                                The Unforgiven                               ║
+║                                                                             ║
+║                                  - Senior -                                 ║
+║                 Ur Lord   Lord Tiamat   Wind Walker   Bubba                 ║
+║                                                                             ║
+║                                  - Elite -                                  ║
+║          Shadow Phantom   Baddog   Load Master   Joebee   Azmodeus          ║
+║                                                                             ║
+║                                  - Trial -                                  ║
+║               Dead Pool   Beach Bum   Black Mage   Ben Jammin               ║
+║                                                                             ║
+╠═════════════════════════════════════════════════════════════════════════════╣
+║   BOARDS                                                                    ║
+║                                                                             ║
+║   BOARD                   ROLE             SYSOP           CONTACT          ║
+║   Suburbia                World HQ         The Chairman    ITS/PRIVATE      ║
+║   TerrorDome              Canadian HQ      StingRay        ITS/PRIVATE      ║
+║   Kindergarden            European HQ      Doctor No       +47-7-525156     ║
+║   Apocalypse              Courier HQ       P.O.W.          ITS/PRIVATE      ║
+╠═════════════════════════════════════════════════════════════════════════════╣
+║   AFFILIATES                                                                ║
+║                                                                             ║
+║   BOARD                   ROLE             SYSOP           CONTACT          ║
+║   Reign In Blood          Affiliate        Uriel           718/698-1969     ║
+║   So-Krate's              Affiliate        Philosopher     310/578-7226     ║
+║   The Space Zone          Affiliate        Death Ray       301/416-0118     ║
+╠═════════════════════════════════════════════════════════════════════════════╣
+║                                                                             ║
+║             SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE             ║
+║                     if you enjoyed this release, buy it!                    ║
+║              KRZ does not accept cash or hardware of any kind.              ║
+║                                                                             ║
+╚═════════════════════════════════════════════════════════════════════════════╝
diff --git a/examples/nightfall-demo.toml b/examples/nightfall-demo.toml
new file mode 100644
index 0000000..2ce6781
--- /dev/null
+++ b/examples/nightfall-demo.toml
@@ -0,0 +1,31 @@
+# A release config. It names a group profile for all the shared boilerplate
+# and only sets the fields that change from one release to the next.
+profile = "examples/krz.profile.toml"
+
+title      = "Nightfall.Protocol.v1.4-KRZ"
+date       = "2026-08-08"
+
+cracker    = "Lime"
+supplier   = "The Witch King"
+packager   = "Renegade Chemist"
+protection = "SafeDisc + Serial"
+install    = "unzip, run SETUP, apply keygen"
+rating     = "9/10"
+
+type       = "GAME"
+publisher  = "Nocturne Interactive"
+format     = "ISO + patch"
+disks      = "3 x CD"
+size       = "1.87 GB"
+files      = "96 x 20 MB"
+url        = "krz.sh/nightfall"
+
+notes = """
+A stealth-action thriller set in a city that never sleeps.
+Cracked clean -- no disc check, no online activation.
+
+Mount or burn the ISO, install, then run the KRZ patch over
+the game directory. Serial is in the keygen. Tested working.
+"""
+
+greets = ["razor-blade", "jack", "the-couriers", "everyone-in-the-scene"]
diff --git a/nfogen.py b/nfogen.py
index c403d2d..aae6817 100755
--- a/nfogen.py
+++ b/nfogen.py
@@ -140,10 +140,18 @@ def _split_list(value) -> list[str]:
     return [p for p in str(value).replace(",", " ").split() if p]
 
 
+def _col_widths(available: int, fracs: list[float]) -> list[int]:
+    """Split `available` chars into columns by fraction; last column absorbs
+    the remainder so the widths always sum exactly."""
+    widths = [max(4, int(available * f)) for f in fracs[:-1]]
+    widths.append(available - sum(widths))
+    return widths
+
+
 def render(data: dict, group: str, site: str, width: int,
            style: str = DEFAULT_STYLE, presents: bool = False,
            footer: str | None = None, layout: str = DEFAULT_LAYOUT,
-           logo: str | None = None,
+           logo: str | None = None, roster: dict | None = None,
            panel_titles: tuple[str, str] = PANEL_TITLES) -> str:
     s = STYLES[style]
     h, v = s["h"], s["v"]
@@ -243,6 +251,66 @@ def render(data: dict, group: str, site: str, width: int,
         for wrapped in textwrap.wrap("   ".join(greets), inner - 4):
             out.append(line("  " + wrapped))
 
+    # group roster sections (from a profile): news, members, couriers,
+    # boards/affiliates tables, outposts
+    r = roster or {}
+
+    def section(header: str) -> None:
+        out.append(sep_before())
+        out.append(line(f"  {header}"))
+        out.append(line())
+
+    def names_block(names: list[str]) -> None:
+        for wrapped in textwrap.wrap("   ".join(names), inner - 4):
+            out.append(center(wrapped))
+        out.append(line())
+
+    def table(header: str, entries: list) -> None:
+        section(header)
+        if entries and isinstance(entries[0], dict):
+            ws = _col_widths(inner - 2, [0.34, 0.24, 0.22, 0.20])
+            keys = ["name", "role", "sysop", "phone"]
+
+            def row(vals: list[str]) -> str:
+                cells = "".join(_clip(str(x), w - 1).ljust(w)
+                                for x, w in zip(vals, ws))
+                return line("  " + cells)
+
+            out.append(row(["BOARD", "ROLE", "SYSOP", "CONTACT"]))
+            for e in entries:
+                out.append(row([e.get(k, "") for k in keys]))
+        else:
+            names_block(_split_list(entries))
+
+    news = str(r.get("news") or "").strip()
+    if news:
+        section("GROUP NEWS")
+        for para in news.splitlines():
+            for wrapped in (textwrap.wrap(para, inner - 4) or [""]):
+                out.append(line("  " + wrapped))
+
+    members = _split_list(r.get("members") or [])
+    if members:
+        section("MEMBERS")
+        names_block(members)
+
+    couriers = r.get("couriers") or {}
+    if couriers:
+        section("COURIERS")
+        for tier, who in couriers.items():
+            out.append(center(f"- {tier} -"))
+            names_block(_split_list(who))
+
+    if r.get("boards"):
+        table("BOARDS", r["boards"])
+    if r.get("affiliates"):
+        table("AFFILIATES", r["affiliates"])
+
+    outposts = _split_list(r.get("outposts") or [])
+    if outposts:
+        section("OUTPOSTS")
+        names_block(outposts)
+
     # footer disclaimer, centered
     if footer:
         out.append(sep_before())
@@ -268,6 +336,8 @@ def build_parser() -> argparse.ArgumentParser:
     )
     p.add_argument("-c", "--config", metavar="PATH",
                    help="config file (.toml or .json) with field defaults")
+    p.add_argument("-p", "--profile", metavar="PATH",
+                   help="group profile (logo, roster, boards) merged under the config")
     p.add_argument("-o", "--output", metavar="PATH",
                    help="write to PATH instead of stdout")
     p.add_argument("-g", "--group", help=f"release group (default {DEFAULT_GROUP})")
@@ -300,7 +370,13 @@ def build_parser() -> argparse.ArgumentParser:
 
 def main(argv: list[str] | None = None) -> int:
     args = build_parser().parse_args(argv)
-    config = load_config(args.config)
+    release = load_config(args.config)
+
+    # a group profile supplies base values (logo, roster, style, footer, ...);
+    # the release config overrides it, and CLI flags override both.
+    profile_path = args.profile or release.get("profile")
+    profile = load_config(profile_path) if profile_path else {}
+    config = {**profile, **release}
 
     # merge: config defaults, then any CLI flag that was supplied
     data = {k: config[k] for k, *_ in FIELDS if k in config and config[k] != ""}
@@ -338,6 +414,10 @@ def main(argv: list[str] | None = None) -> int:
         except FileNotFoundError:
             sys.exit(f"nfogen: logo not found: {logo_path}")
 
+    roster = {k: config[k] for k in
+              ("news", "members", "couriers", "boards", "affiliates", "outposts")
+              if config.get(k)}
+
     interactive = sys.stdin.isatty() and not args.no_input
     if args.interactive:
         prompt_missing(data, force_all=True)
@@ -349,7 +429,7 @@ def main(argv: list[str] | None = None) -> int:
 
     text = render(data, group=group, site=site, width=width, style=style,
                   presents=presents, footer=footer, layout=layout,
-                  logo=logo, panel_titles=panel_titles)
+                  logo=logo, roster=roster, panel_titles=panel_titles)
 
     if args.encoding == "cp437":
         raw = text.encode("cp437", errors="replace")
diff --git a/nfogen.toml b/nfogen.toml
index 17f35be..72efba6 100644
--- a/nfogen.toml
+++ b/nfogen.toml
@@ -40,7 +40,11 @@ ENCODING: utf8 or cp437  (--encoding cp437 = DOS)
 LOGO: --logo art.asc places art above the box
 BANNERS: --presents  --footer   scene boilerplate
 
-OPTIONS: -o -c -g -s -w --style --layout --logo
+PROFILE: -p group.toml supplies logo, footer,
+         members, couriers, boards, affiliates,
+         news, outposts. release cfg overrides it.
+
+OPTIONS: -o -c -p -g -s -w --style --layout --logo
          --encoding --presents --footer -i --no-input
 
 Generated by nfogen itself.