krz/nfogen

A scene-style ASCII .nfo generator. ascii cli generator nfo

Commit 2a15d6b26a

2a15d6b26a392341420db24bfb17a8ec28d4ff18

parent: e7963c089a

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-08 06:11 UTC

add mediainfo import, ASCII banner generator, and template gallery (tier 4)

Layout: unified · split

README.nfo +9 −2
@@ -45,8 +45,15 @@
45│ members, couriers, boards, affiliates, │ 45│ members, couriers, boards, affiliates, │
46│ news, outposts. release cfg overrides it. │ 46│ news, outposts. release cfg overrides it. │
47│ │ 47│ │
48│ OPTIONS: -o -c -p -g -s -w --style --layout --logo │ 48│ MEDIAINFO: --mediainfo dump.json auto-fills │
49│ --encoding --presents --footer -i --no-input │ 49│ video/audio/res/size/runtime/format. │
50│ BANNER: --banner makes an ASCII logo (--banner-text) │
51│ TEMPLATES: -t razor|modern|minimal presets │
52│ (--list-templates) │
53│ │
54│ OPTIONS: -o -c -p -t -g -s -w --style --layout │
55│ --logo --banner --mediainfo --encoding │
56│ --presents --footer -i --no-input │
50│ │ 57│ │
51│ Generated by nfogen itself. │ 58│ Generated by nfogen itself. │
52├──────────────────────────────────────────────────────────────┤ 59├──────────────────────────────────────────────────────────────┤
examples/mediainfo-demo.cp437.nfo added +39
@@ -0,0 +1,39 @@
1 # # #### #####
2 # # # # #
3 ### #### #
4 # # # # #
5 # # # # #####
6
7ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ»
8º THE.MIDNIGHT.CARTOGRAPHER.2026.1080P.BLURAY.X264-KRZ º
9º [ KRZ ] krz.sh º
10º -={ KRZ proudly presents }=- º
11ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍËÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
12º Release Information º Game Information º
13ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÎÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
14º DATE : 2026-08-08 º TYPE : MOVIE º
15º SOURCE : BluRay (Retail) º FORMAT : Matroska º
16º CRACKER : n/a º VIDEO : x264, 8500 kbps, 23... º
17º SUPPLIER : The Witch King º AUDIO : DTS, 5.1, 1509 kbps .. º
18º º RESOLUTION : 1920x1080 º
19º º LANGUAGE : en (subs: en, es, fr) º
20º º GENRE : Drama / Mystery º
21º º RUNTIME : 1h 52m º
22º º SIZE : 8.42 GiB º
23º º URL : imdb.com/title/tt999.. º
24ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÊÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
25º NOTES º
26º º
27º A quiet cartographer maps a city that rearranges itself º
28º after midnight. Media details below were read straight from º
29º the source with mediainfo -- nothing typed by hand. º
30ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
31º GREETS º
32º º
33º the-scene our-suppliers you º
34ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
35º º
36º SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE º
37º if you enjoyed this release, buy it! º
38º º
39ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ
examples/mediainfo-demo.nfo added +39
@@ -0,0 +1,39 @@
1 # # #### #####
2 # # # # #
3 ### #### #
4 # # # # #
5 # # # # #####
6
7╔�����������������������������������������������������������������������������╗
8â•‘ THE.MIDNIGHT.CARTOGRAPHER.2026.1080P.BLURAY.X264-KRZ â•‘
9â•‘ [ KRZ ] krz.sh â•‘
10â•‘ -={ KRZ proudly presents }=- â•‘
11╠��������������������������������������╦��������������������������������������╣
12â•‘ Release Information â•‘ Game Information â•‘
13╠��������������������������������������╬��������������������������������������╣
14â•‘ DATE : 2026-08-08 â•‘ TYPE : MOVIE â•‘
15â•‘ SOURCE : BluRay (Retail) â•‘ FORMAT : Matroska â•‘
16â•‘ CRACKER : n/a â•‘ VIDEO : x264, 8500 kbps, 23... â•‘
17â•‘ SUPPLIER : The Witch King â•‘ AUDIO : DTS, 5.1, 1509 kbps .. â•‘
18â•‘ â•‘ RESOLUTION : 1920x1080 â•‘
19â•‘ â•‘ LANGUAGE : en (subs: en, es, fr) â•‘
20â•‘ â•‘ GENRE : Drama / Mystery â•‘
21â•‘ â•‘ RUNTIME : 1h 52m â•‘
22â•‘ â•‘ SIZE : 8.42 GiB â•‘
23â•‘ â•‘ URL : imdb.com/title/tt999.. â•‘
24╠��������������������������������������╩��������������������������������������╣
25â•‘ NOTES â•‘
26â•‘ â•‘
27â•‘ A quiet cartographer maps a city that rearranges itself â•‘
28â•‘ after midnight. Media details below were read straight from â•‘
29â•‘ the source with mediainfo -- nothing typed by hand. â•‘
30╠�����������������������������������������������������������������������������╣
31â•‘ GREETS â•‘
32â•‘ â•‘
33â•‘ the-scene our-suppliers you â•‘
34╠�����������������������������������������������������������������������������╣
35â•‘ â•‘
36â•‘ SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE â•‘
37â•‘ if you enjoyed this release, buy it! â•‘
38â•‘ â•‘
39╚������������������������������������������������������������������������������
examples/mediainfo-demo.toml added +32
@@ -0,0 +1,32 @@
1# Tier 4 demo: the media fields (video/audio/resolution/size/runtime/format/
2# language) are auto-filled from a mediainfo dump, and the header logo is a
3# generated ASCII banner. Regenerate with:
4# python3 nfogen.py -c examples/mediainfo-demo.toml -o examples/mediainfo-demo.nfo
5group = "KRZ"
6site = "krz.sh"
7style = "double"
8layout = "panel"
9presents = true
10footer = true
11
12banner = true
13banner_text = "KRZ"
14
15mediainfo = "examples/sample.mediainfo.json"
16
17title = "The.Midnight.Cartographer.2026.1080p.BluRay.x264-KRZ"
18date = "2026-08-08"
19type = "MOVIE"
20source = "BluRay (Retail)"
21cracker = "n/a"
22supplier = "The Witch King"
23genre = "Drama / Mystery"
24url = "imdb.com/title/tt9999999"
25
26notes = """
27A quiet cartographer maps a city that rearranges itself
28after midnight. Media details below were read straight from
29the source with mediainfo -- nothing typed by hand.
30"""
31
32greets = ["the-scene", "our-suppliers", "you"]
examples/sample.mediainfo.json added +13
@@ -0,0 +1,13 @@
1{
2 "media": {
3 "track": [
4 { "@type": "General", "Format": "Matroska", "FileSize": "9040412672", "Duration": "6742.038" },
5 { "@type": "Video", "Format": "AVC", "Encoded_Library_Name": "x264",
6 "Width": "1920", "Height": "1080", "BitRate": "8500000", "FrameRate": "23.976" },
7 { "@type": "Audio", "Format": "DTS", "Channels": "6", "BitRate": "1509000", "Language": "en" },
8 { "@type": "Text", "Language": "en" },
9 { "@type": "Text", "Language": "es" },
10 { "@type": "Text", "Language": "fr" }
11 ]
12 }
13}
nfogen.py +182 −3
@@ -10,12 +10,15 @@ from __future__ import annotations
10import argparse 10import argparse
11import json 11import json
12import os 12import os
13import subprocess
13import sys 14import sys
14import textwrap 15import textwrap
15from datetime import date 16from datetime import date
16from itertools import zip_longest 17from itertools import zip_longest
17from pathlib import Path 18from pathlib import Path
18 19
20SCRIPT_DIR = Path(__file__).resolve().parent
21
19# --- field schema ------------------------------------------------------------ 22# --- field schema ------------------------------------------------------------
20# One source of truth. Drives argparse flags, interactive prompts, and render. 23# One source of truth. Drives argparse flags, interactive prompts, and render.
21# key : internal name / config key / flag name 24# key : internal name / config key / flag name
@@ -112,6 +115,104 @@ def load_config(explicit: str | None) -> dict:
112 return tomllib.loads(text) 115 return tomllib.loads(text)
113 116
114 117
118# --- mediainfo import --------------------------------------------------------
119def load_mediainfo(path: str) -> dict:
120 """Read a `mediainfo --Output=JSON` dump (or run mediainfo on a media file)
121 and return values for the video/audio/resolution/size/runtime/format keys."""
122 p = Path(path)
123 if p.suffix.lower() == ".json":
124 raw = p.read_text(encoding="utf-8")
125 else:
126 try:
127 proc = subprocess.run(["mediainfo", "--Output=JSON", str(p)],
128 capture_output=True, text=True, check=True)
129 except FileNotFoundError:
130 sys.exit("nfogen: mediainfo not installed; pass a JSON dump to --mediainfo")
131 except subprocess.CalledProcessError as e:
132 sys.exit(f"nfogen: mediainfo failed: {(e.stderr or '').strip()}")
133 raw = proc.stdout
134 try:
135 doc = json.loads(raw)
136 except json.JSONDecodeError:
137 sys.exit(f"nfogen: not valid mediainfo JSON: {path}")
138 return _extract_media(doc)
139
140
141def _mi_int(x) -> str:
142 try:
143 return str(int(float(str(x).replace(" ", ""))))
144 except ValueError:
145 return str(x)
146
147
148def _mi_size(b) -> str:
149 try:
150 n = float(b)
151 except (TypeError, ValueError):
152 return str(b)
153 for unit, div in (("GiB", 1 << 30), ("MiB", 1 << 20), ("KiB", 1 << 10)):
154 if n >= div:
155 return f"{n / div:.2f} {unit}"
156 return f"{int(n)} B"
157
158
159def _mi_duration(s) -> str:
160 try:
161 sec = int(float(s))
162 except (TypeError, ValueError):
163 return str(s)
164 h, m = sec // 3600, (sec % 3600) // 60
165 return f"{h}h {m:02d}m" if h else f"{m}m {sec % 60:02d}s"
166
167
168def _extract_media(doc: dict) -> dict:
169 tracks = (doc.get("media") or {}).get("track") or []
170 g = next((t for t in tracks if t.get("@type") == "General"), {})
171 v = next((t for t in tracks if t.get("@type") == "Video"), {})
172 auds = [t for t in tracks if t.get("@type") == "Audio"]
173 txts = [t for t in tracks if t.get("@type") == "Text"]
174 a = auds[0] if auds else {}
175 channels = {"1": "1.0", "2": "2.0", "6": "5.1", "8": "7.1"}
176 out: dict = {}
177
178 if g.get("Format"):
179 out["format"] = g["Format"]
180 if g.get("FileSize"):
181 out["size"] = _mi_size(g["FileSize"])
182 if g.get("Duration") or v.get("Duration"):
183 out["runtime"] = _mi_duration(g.get("Duration") or v.get("Duration"))
184
185 if v:
186 parts = [v.get("Encoded_Library_Name") or v.get("Format")]
187 if v.get("BitRate"):
188 parts.append(f"{int(float(v['BitRate'])) // 1000} kbps")
189 if v.get("FrameRate"):
190 parts.append(f"{float(v['FrameRate']):g} fps")
191 out["video"] = ", ".join(p for p in parts if p)
192 if v.get("Width") and v.get("Height"):
193 out["resolution"] = f"{_mi_int(v['Width'])}x{_mi_int(v['Height'])}"
194
195 if a:
196 parts = [a.get("Format")]
197 if a.get("Channels"):
198 parts.append(channels.get(str(a["Channels"]), f"{a['Channels']}ch"))
199 if a.get("BitRate"):
200 parts.append(f"{int(float(a['BitRate'])) // 1000} kbps")
201 audio = ", ".join(p for p in parts if p)
202 if a.get("Language"):
203 audio = f"{audio} ({a['Language']})" if audio else a["Language"]
204 out["audio"] = audio
205
206 subs = list(dict.fromkeys(t.get("Language") for t in txts if t.get("Language")))
207 lang = a.get("Language") or ""
208 if subs:
209 out["language"] = (f"{lang} (subs: {', '.join(subs)})").strip()
210 elif lang:
211 out["language"] = lang
212
213 return {k: val for k, val in out.items() if val}
214
215
115# --- interactive ------------------------------------------------------------- 216# --- interactive -------------------------------------------------------------
116def prompt_missing(data: dict, force_all: bool) -> None: 217def prompt_missing(data: dict, force_all: bool) -> None:
117 """Fill fields from stdin. Prompts every field when force_all, else only 218 """Fill fields from stdin. Prompts every field when force_all, else only
@@ -148,6 +249,43 @@ def _col_widths(available: int, fracs: list[float]) -> list[int]:
148 return widths 249 return widths
149 250
150 251
252# A compact 5-row block font for the banner generator. Each glyph is five
253# rows joined by "/". Uppercase letters, digits, space, and a few symbols.
254_FONT = {
255 "A": " ### /# #/#####/# #/# #", "B": "#### /# #/#### /# #/#### ",
256 "C": " ####/# /# /# / ####", "D": "#### /# #/# #/# #/#### ",
257 "E": "#####/# /### /# /#####", "F": "#####/# /### /# /# ",
258 "G": " ####/# /# ##/# #/ ####", "H": "# #/# #/#####/# #/# #",
259 "I": "#####/ # / # / # /#####", "J": "#####/ # / # /# # / ## ",
260 "K": "# #/# # /### /# # /# #", "L": "# /# /# /# /#####",
261 "M": "# #/## ##/# # #/# #/# #", "N": "# #/## #/# # #/# ##/# #",
262 "O": " ### /# #/# #/# #/ ### ", "P": "#### /# #/#### /# /# ",
263 "Q": " ### /# #/# # #/# # / ## #", "R": "#### /# #/#### /# # /# #",
264 "S": " ####/# / ### / #/#### ", "T": "#####/ # / # / # / # ",
265 "U": "# #/# #/# #/# #/ ### ", "V": "# #/# #/# #/ # # / # ",
266 "W": "# #/# #/# # #/## ##/# #", "X": "# #/ # # / # / # # /# #",
267 "Y": "# #/ # # / # / # / # ", "Z": "#####/ # / # / # /#####",
268 "0": " ### /# ##/# # #/## #/ ### ", "1": " # / ## / # / # /#####",
269 "2": " ### /# #/ ## / # /#####", "3": "#### / #/ ### / #/#### ",
270 "4": "# #/# #/#####/ #/ #", "5": "#####/# /#### / #/#### ",
271 "6": " ####/# /#### /# #/ ### ", "7": "#####/ # / # / # /# ",
272 "8": " ### /# #/ ### /# #/ ### ", "9": " ### /# #/ ####/ #/#### ",
273 " ": " / / / / ", "-": " / /#####/ / ",
274 ".": " / / / / # ", "!": " # / # / # / / # ",
275 ":": " / # / / # / ",
276}
277
278
279def banner(text: str) -> str:
280 """Render text as a 5-row ASCII banner using the built-in block font."""
281 rows = ["", "", "", "", ""]
282 for ch in text.upper():
283 glyph = _FONT.get(ch, _FONT[" "]).split("/")
284 for i in range(5):
285 rows[i] += glyph[i] + " "
286 return "\n".join(r.rstrip() for r in rows)
287
288
151def render(data: dict, group: str, site: str, width: int, 289def render(data: dict, group: str, site: str, width: int,
152 style: str = DEFAULT_STYLE, presents: bool = False, 290 style: str = DEFAULT_STYLE, presents: bool = False,
153 footer: str | None = None, layout: str = DEFAULT_LAYOUT, 291 footer: str | None = None, layout: str = DEFAULT_LAYOUT,
@@ -338,6 +476,12 @@ def build_parser() -> argparse.ArgumentParser:
338 help="config file (.toml or .json) with field defaults") 476 help="config file (.toml or .json) with field defaults")
339 p.add_argument("-p", "--profile", metavar="PATH", 477 p.add_argument("-p", "--profile", metavar="PATH",
340 help="group profile (logo, roster, boards) merged under the config") 478 help="group profile (logo, roster, boards) merged under the config")
479 p.add_argument("-t", "--template", metavar="NAME",
480 help="bundled style template (see --list-templates)")
481 p.add_argument("--list-templates", action="store_true",
482 help="list bundled templates and exit")
483 p.add_argument("--mediainfo", metavar="PATH",
484 help="mediainfo JSON dump (or media file) to auto-fill media fields")
341 p.add_argument("-o", "--output", metavar="PATH", 485 p.add_argument("-o", "--output", metavar="PATH",
342 help="write to PATH instead of stdout") 486 help="write to PATH instead of stdout")
343 p.add_argument("-g", "--group", help=f"release group (default {DEFAULT_GROUP})") 487 p.add_argument("-g", "--group", help=f"release group (default {DEFAULT_GROUP})")
@@ -352,6 +496,9 @@ def build_parser() -> argparse.ArgumentParser:
352 help="art file to place above the box") 496 help="art file to place above the box")
353 p.add_argument("--logo-encoding", choices=["cp437", "utf8"], 497 p.add_argument("--logo-encoding", choices=["cp437", "utf8"],
354 help="encoding of the logo file (default cp437)") 498 help="encoding of the logo file (default cp437)")
499 p.add_argument("--banner", action=argparse.BooleanOptionalAction,
500 default=None, help="generate an ASCII banner logo (defaults to the group)")
501 p.add_argument("--banner-text", help="text for the generated banner")
355 p.add_argument("--encoding", choices=["utf8", "cp437"], default="utf8", 502 p.add_argument("--encoding", choices=["utf8", "cp437"], default="utf8",
356 help="output encoding (default utf8; cp437 for true DOS nfos)") 503 help="output encoding (default utf8; cp437 for true DOS nfos)")
357 p.add_argument("--presents", action=argparse.BooleanOptionalAction, 504 p.add_argument("--presents", action=argparse.BooleanOptionalAction,
@@ -368,15 +515,37 @@ def build_parser() -> argparse.ArgumentParser:
368 return p 515 return p
369 516
370 517
518def _templates_dir() -> Path:
519 return SCRIPT_DIR / "templates"
520
521
522def _list_templates() -> list[str]:
523 d = _templates_dir()
524 return sorted(p.stem for p in d.glob("*.toml")) if d.is_dir() else []
525
526
371def main(argv: list[str] | None = None) -> int: 527def main(argv: list[str] | None = None) -> int:
372 args = build_parser().parse_args(argv) 528 args = build_parser().parse_args(argv)
529
530 if args.list_templates:
531 names = _list_templates()
532 print("\n".join(names) if names else "nfogen: no templates bundled")
533 return 0
534
373 release = load_config(args.config) 535 release = load_config(args.config)
374 536
375 # a group profile supplies base values (logo, roster, style, footer, ...); 537 # layered base: a style template underneath a group profile; the release
376 # the release config overrides it, and CLI flags override both. 538 # config overrides both, and CLI flags override everything.
539 template = {}
540 if args.template:
541 tpath = _templates_dir() / f"{args.template}.toml"
542 if not tpath.exists():
543 sys.exit(f"nfogen: unknown template '{args.template}' "
544 f"(have: {', '.join(_list_templates()) or 'none'})")
545 template = load_config(str(tpath))
377 profile_path = args.profile or release.get("profile") 546 profile_path = args.profile or release.get("profile")
378 profile = load_config(profile_path) if profile_path else {} 547 profile = load_config(profile_path) if profile_path else {}
379 config = {**profile, **release} 548 config = {**template, **profile, **release}
380 549
381 # merge: config defaults, then any CLI flag that was supplied 550 # merge: config defaults, then any CLI flag that was supplied
382 data = {k: config[k] for k, *_ in FIELDS if k in config and config[k] != ""} 551 data = {k: config[k] for k, *_ in FIELDS if k in config and config[k] != ""}
@@ -384,6 +553,13 @@ def main(argv: list[str] | None = None) -> int:
384 val = getattr(args, key) 553 val = getattr(args, key)
385 if val is not None: 554 if val is not None:
386 data[key] = val 555 data[key] = val
556
557 # mediainfo fills any media field not already set by config or flags
558 mediainfo_src = args.mediainfo or config.get("mediainfo")
559 if mediainfo_src:
560 for key, val in load_mediainfo(mediainfo_src).items():
561 data.setdefault(key, val)
562
387 data.setdefault("date", date.today().isoformat()) 563 data.setdefault("date", date.today().isoformat())
388 564
389 group = args.group or config.get("group") or DEFAULT_GROUP 565 group = args.group or config.get("group") or DEFAULT_GROUP
@@ -405,6 +581,7 @@ def main(argv: list[str] | None = None) -> int:
405 panel_titles = (config.get("panel_left") or PANEL_TITLES[0], 581 panel_titles = (config.get("panel_left") or PANEL_TITLES[0],
406 config.get("panel_right") or PANEL_TITLES[1]) 582 config.get("panel_right") or PANEL_TITLES[1])
407 583
584 # logo: an art file wins; otherwise an optional generated banner
408 logo = None 585 logo = None
409 logo_path = args.logo or config.get("logo") 586 logo_path = args.logo or config.get("logo")
410 if logo_path: 587 if logo_path:
@@ -413,6 +590,8 @@ def main(argv: list[str] | None = None) -> int:
413 logo = Path(logo_path).read_text(encoding=enc, errors="replace") 590 logo = Path(logo_path).read_text(encoding=enc, errors="replace")
414 except FileNotFoundError: 591 except FileNotFoundError:
415 sys.exit(f"nfogen: logo not found: {logo_path}") 592 sys.exit(f"nfogen: logo not found: {logo_path}")
593 elif resolve(args.banner, "banner", False):
594 logo = banner(args.banner_text or config.get("banner_text") or group)
416 595
417 roster = {k: config[k] for k in 596 roster = {k: config[k] for k in
418 ("news", "members", "couriers", "boards", "affiliates", "outposts") 597 ("news", "members", "couriers", "boards", "affiliates", "outposts")
nfogen.toml +9 −2
@@ -44,8 +44,15 @@ PROFILE: -p group.toml supplies logo, footer,
44 members, couriers, boards, affiliates, 44 members, couriers, boards, affiliates,
45 news, outposts. release cfg overrides it. 45 news, outposts. release cfg overrides it.
46 46
47OPTIONS: -o -c -p -g -s -w --style --layout --logo 47MEDIAINFO: --mediainfo dump.json auto-fills
48 --encoding --presents --footer -i --no-input 48 video/audio/res/size/runtime/format.
49BANNER: --banner makes an ASCII logo (--banner-text)
50TEMPLATES: -t razor|modern|minimal presets
51 (--list-templates)
52
53OPTIONS: -o -c -p -t -g -s -w --style --layout
54 --logo --banner --mediainfo --encoding
55 --presents --footer -i --no-input
49 56
50Generated by nfogen itself. 57Generated by nfogen itself.
51""" 58"""
templates/minimal.toml added +6
@@ -0,0 +1,6 @@
1# Bare card: single-line box, simple rows, house width. Just the facts.
2style = "single"
3layout = "rows"
4width = 64
5presents = false
6footer = false
templates/modern.toml added +6
@@ -0,0 +1,6 @@
1# Cleaner modern look: single-line box, two-column panel, no scene boilerplate.
2style = "single"
3layout = "panel"
4width = 79
5presents = false
6footer = false
templates/razor.toml added +12
@@ -0,0 +1,12 @@
1# RAZOR-era look: double box, two-column panel, generated banner + presents,
2# and the classic disclaimer footer. Bring your own title and fields.
3style = "double"
4layout = "panel"
5width = 79
6presents = true
7banner = true
8footer = true
9footer_text = """
10SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE
11if you enjoyed this release, buy the original!
12"""