krz/nfogen

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

Commit 83f9f24efa

83f9f24efa383a310ce1dfbdd50d5864764e4a10

parent: 907e55cc50

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-08 05:56 UTC

add two-column info panel, scene field aliases, and logo art (tier 2)

Layout: unified · split

.gitignore added +2
@@ -0,0 +1,2 @@
1__pycache__/
2*.pyc
README.nfo +9 −6
@@ -29,17 +29,20 @@
29│ default plus group, site, width. Or use -c PATH. │ 29│ default plus group, site, width. Or use -c PATH. │
30│ │ 30│ │
31│ FIELDS (all optional but title): │ 31│ FIELDS (all optional but title): │
32│ core: title date type source size files │ 32│ release: cracker supplier packager protection │
33│ url notes greets │ 33│ install rating date source │
34│ media: format video audio resolution │ 34│ program: type publisher format disks size files url │
35│ language genre runtime │ 35│ media: video audio resolution language genre runtime │
36│ text: notes greets │
36│ │ 37│ │
38│ LAYOUT: rows (default) or panel (2-col, --layout) │
37│ STYLES: single, double, block (--style) │ 39│ STYLES: single, double, block (--style) │
38│ ENCODING: utf8 or cp437 (--encoding cp437 = DOS) │ 40│ ENCODING: utf8 or cp437 (--encoding cp437 = DOS) │
41│ LOGO: --logo art.asc places art above the box │
39│ BANNERS: --presents --footer scene boilerplate │ 42│ BANNERS: --presents --footer scene boilerplate │
40│ │ 43│ │
41│ OPTIONS: -o -c -g -s -w --style --encoding │ 44│ OPTIONS: -o -c -g -s -w --style --layout --logo │
42│ --presents --footer -i --no-input │ 45│ --encoding --presents --footer -i --no-input │
43│ │ 46│ │
44│ Generated by nfogen itself. │ 47│ Generated by nfogen itself. │
45├──────────────────────────────────────────────────────────────┤ 48├──────────────────────────────────────────────────────────────┤
examples/krazy-toolkit-v3.cp437.nfo added +43
@@ -0,0 +1,43 @@
1 _ _______ ___
2 | |/ / _ \ \ / / |
3 | ' /| |_) \ V /| | k r a z y w a r e z
4 | . \| _ < | | |_|
5 |_|\_\_| \_\|_| (_) ------- since 1985 -------
6
7ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ»
8º K R A Z Y . T O O L K I T . V 3 . 2 . 1 - K R Z º
9º [ KRZ ] krz.sh º
10º -={ KRZ proudly presents }=- º
11ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍËÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
12º Release Information º Game Information º
13ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÎÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
14º DATE : 2026-08-08 º TYPE : APP º
15º CRACKER : Lime º PUBLISHER : Krazy Softworks º
16º SUPPLIER : The Witch King º FORMAT : ZIP / self-extracting º
17º PACKAGER : Renegade Chemist º DISKS : 7 x 1.44 HD º
18º PROTECTION : Serial + Doc Check º SIZE : 9.84 MB º
19º INSTALL : unzip, run SETUP.EXE.. º FILES : 42 x 240 KB º
20º RATING : 10/10 º URL : krz.sh/toolkit º
21ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÊÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
22º NOTES º
23º º
24º Here is the release everyone has been waiting for. The º
25º complete Krazy Toolkit, cracked clean -- no nag screens, º
26º no phone-home, no time limit. Tested on a fresh install. º
27º º
28º Install : unzip to a folder of your choice º
29º run SETUP.EXE and follow the prompts º
30º apply the included keygen when asked for a serial º
31º º
32º Greets to everyone who keeps the scene alive. Fix it º
33º yourself if you think you can do better. º
34ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
35º GREETS º
36º º
37º razor-blade jack knightmare stingray the-couriers º
38ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
39º º
40º SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE º
41º if you enjoyed this release, buy it! º
42º º
43ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ
examples/krazy-toolkit-v3.nfo added +43
@@ -0,0 +1,43 @@
1 _ _______ ___
2 | |/ / _ \ \ / / |
3 | ' /| |_) \ V /| | k r a z y w a r e z
4 | . \| _ < | | |_|
5 |_|\_\_| \_\|_| (_) ------- since 1985 -------
6
7╔�����������������������������������������������������������������������������╗
8â•‘ K R A Z Y . T O O L K I T . V 3 . 2 . 1 - K R Z â•‘
9â•‘ [ KRZ ] krz.sh â•‘
10â•‘ -={ KRZ proudly presents }=- â•‘
11╠��������������������������������������╦��������������������������������������╣
12â•‘ Release Information â•‘ Game Information â•‘
13╠��������������������������������������╬��������������������������������������╣
14â•‘ DATE : 2026-08-08 â•‘ TYPE : APP â•‘
15â•‘ CRACKER : Lime â•‘ PUBLISHER : Krazy Softworks â•‘
16â•‘ SUPPLIER : The Witch King â•‘ FORMAT : ZIP / self-extracting â•‘
17â•‘ PACKAGER : Renegade Chemist â•‘ DISKS : 7 x 1.44 HD â•‘
18â•‘ PROTECTION : Serial + Doc Check â•‘ SIZE : 9.84 MB â•‘
19â•‘ INSTALL : unzip, run SETUP.EXE.. â•‘ FILES : 42 x 240 KB â•‘
20â•‘ RATING : 10/10 â•‘ URL : krz.sh/toolkit â•‘
21╠��������������������������������������╩��������������������������������������╣
22â•‘ NOTES â•‘
23â•‘ â•‘
24â•‘ Here is the release everyone has been waiting for. The â•‘
25â•‘ complete Krazy Toolkit, cracked clean -- no nag screens, â•‘
26â•‘ no phone-home, no time limit. Tested on a fresh install. â•‘
27â•‘ â•‘
28â•‘ Install : unzip to a folder of your choice â•‘
29â•‘ run SETUP.EXE and follow the prompts â•‘
30â•‘ apply the included keygen when asked for a serial â•‘
31â•‘ â•‘
32â•‘ Greets to everyone who keeps the scene alive. Fix it â•‘
33â•‘ yourself if you think you can do better. â•‘
34╠�����������������������������������������������������������������������������╣
35â•‘ GREETS â•‘
36â•‘ â•‘
37â•‘ razor-blade jack knightmare stingray the-couriers â•‘
38╠�����������������������������������������������������������������������������╣
39â•‘ â•‘
40â•‘ SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE â•‘
41â•‘ if you enjoyed this release, buy it! â•‘
42â•‘ â•‘
43╚������������������������������������������������������������������������������
examples/krazy-toolkit-v3.toml added +43
@@ -0,0 +1,43 @@
1group = "KRZ"
2site = "krz.sh"
3style = "double"
4layout = "panel"
5presents = true
6footer = true
7logo = "examples/krz-logo.asc"
8logo_encoding = "utf8"
9
10title = "Krazy.Toolkit.v3.2.1-KRZ"
11date = "2026-08-08"
12
13# release information (left panel)
14cracker = "Lime"
15supplier = "The Witch King"
16packager = "Renegade Chemist"
17protection = "Serial + Doc Check"
18install = "unzip, run SETUP.EXE, use keygen"
19rating = "10/10"
20
21# game / program information (right panel)
22type = "APP"
23publisher = "Krazy Softworks"
24format = "ZIP / self-extracting"
25disks = "7 x 1.44 HD"
26size = "9.84 MB"
27files = "42 x 240 KB"
28url = "krz.sh/toolkit"
29
30notes = """
31Here is the release everyone has been waiting for. The
32complete Krazy Toolkit, cracked clean -- no nag screens,
33no phone-home, no time limit. Tested on a fresh install.
34
35Install : unzip to a folder of your choice
36 run SETUP.EXE and follow the prompts
37 apply the included keygen when asked for a serial
38
39Greets to everyone who keeps the scene alive. Fix it
40yourself if you think you can do better.
41"""
42
43greets = ["razor-blade", "jack", "knightmare", "stingray", "the-couriers"]
examples/krz-logo.asc added +5
@@ -0,0 +1,5 @@
1 _ _______ ___
2| |/ / _ \ \ / / |
3| ' /| |_) \ V /| | k r a z y w a r e z
4| . \| _ < | | |_|
5|_|\_\_| \_\|_| (_) ------- since 1985 -------
examples/the-midnight-cartographer-2026.cp437.nfo +1 −1
@@ -4,8 +4,8 @@
4º -={ KRZ proudly presents }=- º 4º -={ KRZ proudly presents }=- º
5ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹ 5ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹
6º DATE 2026-08-08 º 6º DATE 2026-08-08 º
7º TYPE MOVIE º
8º SOURCE BluRay (Retail) º 7º SOURCE BluRay (Retail) º
8º TYPE MOVIE º
9º FORMAT Matroska (MKV) º 9º FORMAT Matroska (MKV) º
10º VIDEO x264 @ 8500 kbps, 23.976 fps º 10º VIDEO x264 @ 8500 kbps, 23.976 fps º
11º AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) º 11º AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) º
examples/the-midnight-cartographer-2026.nfo +1 −1
@@ -4,8 +4,8 @@
4â•‘ -={ KRZ proudly presents }=- â•‘ 4â•‘ -={ KRZ proudly presents }=- â•‘
5╠�����������������������������������������������������������������������������╣ 5╠�����������������������������������������������������������������������������╣
6â•‘ DATE 2026-08-08 â•‘ 6â•‘ DATE 2026-08-08 â•‘
7â•‘ TYPE MOVIE â•‘
8â•‘ SOURCE BluRay (Retail) â•‘ 7â•‘ SOURCE BluRay (Retail) â•‘
8â•‘ TYPE MOVIE â•‘
9â•‘ FORMAT Matroska (MKV) â•‘ 9â•‘ FORMAT Matroska (MKV) â•‘
10â•‘ VIDEO x264 @ 8500 kbps, 23.976 fps â•‘ 10â•‘ VIDEO x264 @ 8500 kbps, 23.976 fps â•‘
11â•‘ AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) â•‘ 11â•‘ AUDIO DTS-HD MA 5.1 @ 1509 kbps (English) â•‘
nfogen.py +130 −49
@@ -13,6 +13,7 @@ import os
13import sys 13import sys
14import textwrap 14import textwrap
15from datetime import date 15from datetime import date
16from itertools import zip_longest
16from pathlib import Path 17from pathlib import Path
17 18
18# --- field schema ------------------------------------------------------------ 19# --- field schema ------------------------------------------------------------
@@ -23,27 +24,39 @@ from pathlib import Path
23# kind : "line" scalar shown as an aligned INFO row 24# kind : "line" scalar shown as an aligned INFO row
24# "list" many values, shown space-joined in its own section 25# "list" many values, shown space-joined in its own section
25# "block" free text, wrapped in its own section 26# "block" free text, wrapped in its own section
27# col : "L"/"R" panel column for "line" fields; None otherwise
26 28
27FIELDS = [ 29FIELDS = [
28 ("title", "TITLE", "release title (required)", "line"), 30 ("title", "TITLE", "release title (required)", "line", None),
29 ("date", "DATE", "release date", "line"), 31 # --- release information (left panel) ---
30 ("type", "TYPE", "release type (MOVIE, TV, APP, ...)", "line"), 32 ("date", "DATE", "release date", "line", "L"),
31 ("source", "SOURCE", "source (BluRay, WEB-DL, CD, ...)", "line"), 33 ("source", "SOURCE", "source (BluRay, WEB-DL, CD, ...)", "line", "L"),
32 ("format", "FORMAT", "container / format", "line"), 34 ("cracker", "CRACKER", "cracker / cracked by", "line", "L"),
33 ("video", "VIDEO", "video codec / details", "line"), 35 ("supplier", "SUPPLIER", "supplier / supplied by", "line", "L"),
34 ("audio", "AUDIO", "audio codec / details", "line"), 36 ("packager", "PACKAGER", "packager / packed by", "line", "L"),
35 ("resolution", "RESOLUTION", "resolution", "line"), 37 ("protection", "PROTECTION", "protection type", "line", "L"),
36 ("language", "LANGUAGE", "language(s)", "line"), 38 ("install", "INSTALL", "install method", "line", "L"),
37 ("genre", "GENRE", "genre", "line"), 39 ("rating", "RATING", "rating", "line", "L"),
38 ("runtime", "RUNTIME", "runtime / duration", "line"), 40 # --- game / media information (right panel) ---
39 ("size", "SIZE", "total size", "line"), 41 ("type", "TYPE", "release type (MOVIE, TV, APP, ...)", "line", "R"),
40 ("files", "FILES", "file count / listing", "line"), 42 ("publisher", "PUBLISHER", "publisher / vendor", "line", "R"),
41 ("url", "URL", "reference url (imdb, homepage, ...)", "line"), 43 ("format", "FORMAT", "container / format", "line", "R"),
42 ("notes", "NOTES", "free-text notes / description", "block"), 44 ("disks", "DISKS", "number of disks / archives", "line", "R"),
43 ("greets", "GREETS", "groups to greet (space/comma sep)", "list"), 45 ("video", "VIDEO", "video codec / details", "line", "R"),
46 ("audio", "AUDIO", "audio codec / details", "line", "R"),
47 ("resolution", "RESOLUTION", "resolution", "line", "R"),
48 ("language", "LANGUAGE", "language(s)", "line", "R"),
49 ("genre", "GENRE", "genre", "line", "R"),
50 ("runtime", "RUNTIME", "runtime / duration", "line", "R"),
51 ("size", "SIZE", "total size", "line", "R"),
52 ("files", "FILES", "file count / listing", "line", "R"),
53 ("url", "URL", "reference url (imdb, homepage, ...)", "line", "R"),
54 # --- free-form sections ---
55 ("notes", "NOTES", "free-text notes / description", "block", None),
56 ("greets", "GREETS", "groups to greet (space/comma sep)", "list", None),
44] 57]
45FIELD_KINDS = {k: kind for k, _, _, kind in FIELDS} 58FIELD_KINDS = {k: kind for k, _, _, kind, _ in FIELDS}
46INFO_KEYS = [k for k, _, _, kind in FIELDS if kind == "line" and k != "title"] 59INFO_KEYS = [k for k, _, _, kind, _ in FIELDS if kind == "line" and k != "title"]
47 60
48DEFAULT_SITE = "krz.sh" 61DEFAULT_SITE = "krz.sh"
49DEFAULT_GROUP = "KRZ" 62DEFAULT_GROUP = "KRZ"
@@ -53,16 +66,20 @@ DEFAULT_FOOTER = (
53 "SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE\n" 66 "SUPPORT THE COMPANIES THAT PRODUCE QUALITY SOFTWARE\n"
54 "if you enjoyed this release, buy it!" 67 "if you enjoyed this release, buy it!"
55) 68)
69DEFAULT_LAYOUT = "rows"
70PANEL_TITLES = ("Release Information", "Game Information")
56 71
57# Box-drawing character sets. "single"/"double" are CP437-encodable; "block" 72# Box-drawing character sets. "single"/"double" are CP437-encodable; "block"
58# is a solid fill frame. Keys: corners + horizontal/vertical + tee separators. 73# is a solid fill frame. Keys: corners + horizontal/vertical + tee separators.
74# jt/jm/jb are the down-tee, cross, and up-tee used where the panel's
75# vertical column divider meets a horizontal rule.
59STYLES = { 76STYLES = {
60 "single": dict(tl="┌", tr="�", bl="└", br="┘", 77 "single": dict(tl="┌", tr="�", bl="└", br="┘", h="─", v="│",
61 h="─", v="│", ls="├", rs="┤"), 78 ls="├", rs="┤", jt="┬", jm="┼", jb="┴"),
62 "double": dict(tl="╔", tr="╗", bl="╚", br="�", 79 "double": dict(tl="╔", tr="╗", bl="╚", br="�", h="�", v="║",
63 h="�", v="║", ls="╠", rs="╣"), 80 ls="╠", rs="╣", jt="╦", jm="╬", jb="╩"),
64 "block": dict(tl="â–ˆ", tr="â–ˆ", bl="â–ˆ", br="â–ˆ", 81 "block": dict(tl="â–ˆ", tr="â–ˆ", bl="â–ˆ", br="â–ˆ", h="â–ˆ", v="â–ˆ",
65 h="â–ˆ", v="â–ˆ", ls="â–ˆ", rs="â–ˆ"), 82 ls="â–ˆ", rs="â–ˆ", jt="â–ˆ", jm="â–ˆ", jb="â–ˆ"),
66} 83}
67 84
68CONFIG_CANDIDATES = [ 85CONFIG_CANDIDATES = [
@@ -105,7 +122,7 @@ def prompt_missing(data: dict, force_all: bool) -> None:
105 return 122 return
106 print("nfogen: interactive mode (blank to skip)\n", file=sys.stderr) 123 print("nfogen: interactive mode (blank to skip)\n", file=sys.stderr)
107 for key in keys: 124 for key in keys:
108 _, label, help_text, _ = next(f for f in FIELDS if f[0] == key) 125 _, label, help_text, _, _ = next(f for f in FIELDS if f[0] == key)
109 current = data.get(key, "") 126 current = data.get(key, "")
110 suffix = f" [{current}]" if current else "" 127 suffix = f" [{current}]" if current else ""
111 try: 128 try:
@@ -125,20 +142,42 @@ def _split_list(value) -> list[str]:
125 142
126def render(data: dict, group: str, site: str, width: int, 143def render(data: dict, group: str, site: str, width: int,
127 style: str = DEFAULT_STYLE, presents: bool = False, 144 style: str = DEFAULT_STYLE, presents: bool = False,
128 footer: str | None = None) -> str: 145 footer: str | None = None, layout: str = DEFAULT_LAYOUT,
129 inner = width - 4 # borders + one space of padding on each side 146 logo: str | None = None,
147 panel_titles: tuple[str, str] = PANEL_TITLES) -> str:
130 s = STYLES[style] 148 s = STYLES[style]
131 top = s["tl"] + s["h"] * (width - 2) + s["tr"] 149 h, v = s["h"], s["v"]
132 sep = s["ls"] + s["h"] * (width - 2) + s["rs"] 150 inner = width - 4 # single-column content width (1 space padding)
133 bot = s["bl"] + s["h"] * (width - 2) + s["br"] 151 cl = (width - 7) // 2 # left panel content width
152 cr = (width - 7) - cl # right panel content width
153 rfill = width - cl - 5 # h-run right of the divider in a rule
154
155 def hrule(left: str, right: str, joint: str | None = None) -> str:
156 if joint is None:
157 return left + h * (width - 2) + right
158 return left + h * (cl + 2) + joint + h * rfill + right
134 159
135 def line(content: str = "") -> str: 160 def line(content: str = "") -> str:
136 return f"{s['v']} " + content.ljust(inner) + f" {s['v']}" 161 return f"{v} " + content.ljust(inner) + f" {v}"
137 162
138 def center(content: str) -> str: 163 def center(content: str) -> str:
139 return f"{s['v']} " + content.center(inner) + f" {s['v']}" 164 return f"{v} " + content.center(inner) + f" {v}"
165
166 def prow(left: str, right: str, centered: bool = False) -> str:
167 fn = str.center if centered else str.ljust
168 return (f"{v} " + fn(_clip(left, cl), cl)
169 + f" {v} " + fn(_clip(right, cr), cr) + f" {v}")
170
171 out: list[str] = []
172
173 # logo art above the box, block-centered to the full width
174 if logo:
175 art = logo.rstrip("\n").split("\n")
176 pad = max((width - max((len(a) for a in art), default=0)) // 2, 0)
177 out.extend((" " * pad + a).rstrip() for a in art)
178 out.append("")
140 179
141 out: list[str] = [top] 180 out.append(hrule(s["tl"], s["tr"]))
142 181
143 # header: spaced title, then group tag + site, optional presents banner 182 # header: spaced title, then group tag + site, optional presents banner
144 title = str(data.get("title") or "UNTITLED").upper() 183 title = str(data.get("title") or "UNTITLED").upper()
@@ -149,24 +188,46 @@ def render(data: dict, group: str, site: str, width: int,
149 if presents: 188 if presents:
150 out.append(center(_clip(f"-={{ {group.upper()} proudly presents }}=-", inner))) 189 out.append(center(_clip(f"-={{ {group.upper()} proudly presents }}=-", inner)))
151 190
152 # info rows 191 line_fields = [(k, lbl, col) for k, lbl, _, kind, col in FIELDS
153 rows = [(label, str(data[key])) 192 if kind == "line" and k != "title" and data.get(k)]
154 for key, label, _, _ in FIELDS 193 prev_panel = False
155 if key in INFO_KEYS and data.get(key)] 194
156 if rows: 195 def sep_before() -> str:
157 out.append(sep) 196 nonlocal prev_panel
158 lw = max(len(label) for label, _ in rows) 197 joint = s["jb"] if prev_panel else None
198 prev_panel = False
199 return hrule(s["ls"], s["rs"], joint)
200
201 # info: two-column panel or single-column rows
202 if layout == "panel" and line_fields:
203 left = [(lbl, str(data[k])) for k, lbl, col in line_fields if col == "L"]
204 right = [(lbl, str(data[k])) for k, lbl, col in line_fields if col == "R"]
205 lw_l = max((len(lbl) for lbl, _ in left), default=0)
206 lw_r = max((len(lbl) for lbl, _ in right), default=0)
207
208 def cell(pair, lw):
209 return f" {pair[0]:<{lw}} : {pair[1]}" if pair else ""
210
211 out.append(hrule(s["ls"], s["rs"], s["jt"]))
212 out.append(prow(panel_titles[0], panel_titles[1], centered=True))
213 out.append(hrule(s["ls"], s["rs"], s["jm"]))
214 for lp, rp in zip_longest(left, right):
215 out.append(prow(cell(lp, lw_l), cell(rp, lw_r)))
216 prev_panel = True
217 elif line_fields:
218 out.append(hrule(s["ls"], s["rs"]))
219 lw = max(len(lbl) for _, lbl, _ in line_fields)
159 avail = inner - (2 + lw + 2) 220 avail = inner - (2 + lw + 2)
160 for label, value in rows: 221 for k, lbl, _ in line_fields:
161 wrapped = textwrap.wrap(value, avail) or [""] 222 wrapped = textwrap.wrap(str(data[k]), avail) or [""]
162 out.append(line(f" {label:<{lw}} {wrapped[0]}")) 223 out.append(line(f" {lbl:<{lw}} {wrapped[0]}"))
163 for cont in wrapped[1:]: 224 for cont in wrapped[1:]:
164 out.append(line(" " * (2 + lw + 2) + cont)) 225 out.append(line(" " * (2 + lw + 2) + cont))
165 226
166 # notes block 227 # notes block
167 notes = str(data.get("notes") or "").strip() 228 notes = str(data.get("notes") or "").strip()
168 if notes: 229 if notes:
169 out.append(sep) 230 out.append(sep_before())
170 out.append(line(" NOTES")) 231 out.append(line(" NOTES"))
171 out.append(line()) 232 out.append(line())
172 for para in notes.splitlines() or [""]: 233 for para in notes.splitlines() or [""]:
@@ -176,7 +237,7 @@ def render(data: dict, group: str, site: str, width: int,
176 # greets block 237 # greets block
177 greets = _split_list(data.get("greets") or []) 238 greets = _split_list(data.get("greets") or [])
178 if greets: 239 if greets:
179 out.append(sep) 240 out.append(sep_before())
180 out.append(line(" GREETS")) 241 out.append(line(" GREETS"))
181 out.append(line()) 242 out.append(line())
182 for wrapped in textwrap.wrap(" ".join(greets), inner - 4): 243 for wrapped in textwrap.wrap(" ".join(greets), inner - 4):
@@ -184,14 +245,14 @@ def render(data: dict, group: str, site: str, width: int,
184 245
185 # footer disclaimer, centered 246 # footer disclaimer, centered
186 if footer: 247 if footer:
187 out.append(sep) 248 out.append(sep_before())
188 out.append(line()) 249 out.append(line())
189 for para in footer.splitlines(): 250 for para in footer.splitlines():
190 for wrapped in (textwrap.wrap(para, inner - 4) or [""]): 251 for wrapped in (textwrap.wrap(para, inner - 4) or [""]):
191 out.append(center(wrapped)) 252 out.append(center(wrapped))
192 out.append(line()) 253 out.append(line())
193 254
194 out.append(bot) 255 out.append(hrule(s["bl"], s["br"], s["jb"] if prev_panel else None))
195 return "\n".join(out) + "\n" 256 return "\n".join(out) + "\n"
196 257
197 258
@@ -215,6 +276,12 @@ def build_parser() -> argparse.ArgumentParser:
215 help=f"box width in chars (default {DEFAULT_WIDTH})") 276 help=f"box width in chars (default {DEFAULT_WIDTH})")
216 p.add_argument("--style", choices=sorted(STYLES), 277 p.add_argument("--style", choices=sorted(STYLES),
217 help=f"box-drawing style (default {DEFAULT_STYLE})") 278 help=f"box-drawing style (default {DEFAULT_STYLE})")
279 p.add_argument("--layout", choices=["rows", "panel"],
280 help=f"info layout (default {DEFAULT_LAYOUT})")
281 p.add_argument("--logo", metavar="PATH",
282 help="art file to place above the box")
283 p.add_argument("--logo-encoding", choices=["cp437", "utf8"],
284 help="encoding of the logo file (default cp437)")
218 p.add_argument("--encoding", choices=["utf8", "cp437"], default="utf8", 285 p.add_argument("--encoding", choices=["utf8", "cp437"], default="utf8",
219 help="output encoding (default utf8; cp437 for true DOS nfos)") 286 help="output encoding (default utf8; cp437 for true DOS nfos)")
220 p.add_argument("--presents", action=argparse.BooleanOptionalAction, 287 p.add_argument("--presents", action=argparse.BooleanOptionalAction,
@@ -226,7 +293,7 @@ def build_parser() -> argparse.ArgumentParser:
226 help="prompt for every field") 293 help="prompt for every field")
227 p.add_argument("--no-input", action="store_true", 294 p.add_argument("--no-input", action="store_true",
228 help="never prompt, even if fields are missing") 295 help="never prompt, even if fields are missing")
229 for key, _, help_text, _ in FIELDS: 296 for key, _, help_text, _, _ in FIELDS:
230 p.add_argument(f"--{key}", help=help_text) 297 p.add_argument(f"--{key}", help=help_text)
231 return p 298 return p
232 299
@@ -258,6 +325,19 @@ def main(argv: list[str] | None = None) -> int:
258 footer_text = args.footer_text or config.get("footer_text") or DEFAULT_FOOTER 325 footer_text = args.footer_text or config.get("footer_text") or DEFAULT_FOOTER
259 footer = footer_text if show_footer else None 326 footer = footer_text if show_footer else None
260 327
328 layout = args.layout or config.get("layout") or DEFAULT_LAYOUT
329 panel_titles = (config.get("panel_left") or PANEL_TITLES[0],
330 config.get("panel_right") or PANEL_TITLES[1])
331
332 logo = None
333 logo_path = args.logo or config.get("logo")
334 if logo_path:
335 enc = args.logo_encoding or config.get("logo_encoding") or "cp437"
336 try:
337 logo = Path(logo_path).read_text(encoding=enc, errors="replace")
338 except FileNotFoundError:
339 sys.exit(f"nfogen: logo not found: {logo_path}")
340
261 interactive = sys.stdin.isatty() and not args.no_input 341 interactive = sys.stdin.isatty() and not args.no_input
262 if args.interactive: 342 if args.interactive:
263 prompt_missing(data, force_all=True) 343 prompt_missing(data, force_all=True)
@@ -268,7 +348,8 @@ def main(argv: list[str] | None = None) -> int:
268 sys.exit("nfogen: a title is required (use --title, a config, or -i)") 348 sys.exit("nfogen: a title is required (use --title, a config, or -i)")
269 349
270 text = render(data, group=group, site=site, width=width, style=style, 350 text = render(data, group=group, site=site, width=width, style=style,
271 presents=presents, footer=footer) 351 presents=presents, footer=footer, layout=layout,
352 logo=logo, panel_titles=panel_titles)
272 353
273 if args.encoding == "cp437": 354 if args.encoding == "cp437":
274 raw = text.encode("cp437", errors="replace") 355 raw = text.encode("cp437", errors="replace")
nfogen.toml +9 −6
@@ -28,17 +28,20 @@ CONFIG: a .toml or .json auto-loaded from
28default plus group, site, width. Or use -c PATH. 28default plus group, site, width. Or use -c PATH.
29 29
30FIELDS (all optional but title): 30FIELDS (all optional but title):
31core: title date type source size files 31release: cracker supplier packager protection
32url notes greets 32 install rating date source
33media: format video audio resolution 33program: type publisher format disks size files url
34language genre runtime 34media: video audio resolution language genre runtime
35text: notes greets
35 36
37LAYOUT: rows (default) or panel (2-col, --layout)
36STYLES: single, double, block (--style) 38STYLES: single, double, block (--style)
37ENCODING: utf8 or cp437 (--encoding cp437 = DOS) 39ENCODING: utf8 or cp437 (--encoding cp437 = DOS)
40LOGO: --logo art.asc places art above the box
38BANNERS: --presents --footer scene boilerplate 41BANNERS: --presents --footer scene boilerplate
39 42
40OPTIONS: -o -c -g -s -w --style --encoding 43OPTIONS: -o -c -g -s -w --style --layout --logo
41 --presents --footer -i --no-input 44 --encoding --presents --footer -i --no-input
42 45
43Generated by nfogen itself. 46Generated by nfogen itself.
44""" 47"""