Files
hermes-agent/scripts/generate_icons.py
ethernet f5b56a2746 feat(icons): no-frame wordmarks, squircle BrandMarks, on-demand generation
- website wordmarks (logo.png/logo-dark.png) drop the black/white frame:
  the girl alone on transparency, black for light navbar / white for dark
  (docusaurus srcDark stays)
- BrandMark marks are now the app-icon squircle itself (transparent
  corners, 824px safe-zone composition) instead of plain tiles; the
  components no longer paint a tile/rounding
- generated icon outputs are NOT committed anymore: all 35 targets are
  gitignored and regenerated on demand by the consuming pipelines via the
  new scripts/generate-icons.mjs (website prebuild, desktop prebuild+dev,
  installer prebuild, web prebuild)
- freshness lane switched from byte-compare to structural verification
  (sizes, squircle corner transparency, ICO frame sets via header parse) —
  no more windows/ubuntu byte drift dance
- marks saved as 8-bit palette PNGs (FASTOCTREE quantize keeps per-index
  alpha tRNS -> 2.5KB, AA edges preserved); compose_svg renders in the
  background's native space so resvg scales per output size
2026-09-06 21:49:26 -04:00

516 lines
23 KiB
Python

#!/usr/bin/env python3
"""Generate every app icon in the repo from the nous-girl art + platform backgrounds.
Usage (from repo root):
.venv/Scripts/python.exe scripts/generate_icons.py # write
.venv/Scripts/python.exe scripts/generate_icons.py --check # verify structure
Sources of truth — two axes, composed per target:
Girl art (vector): assets/nous-girl-black.svg (black positive space)
assets/nous-girl-white.svg (white positive space)
straight from the Nous brand kit (Inkscape exports,
5487^2 viewBox, one path each).
Backgrounds (per platform surface, light/dark):
assets/backgrounds/squircle-light.svg white rounded
assets/backgrounds/squircle-dark.svg #0d1117 rounded
The master SVGs (assets/icon-master.svg light, assets/icon-master-dark.svg
dark) are GENERATED artifacts — squircle background + girl nested into the
824px HIG content safe zone. The light master drives every squircle target;
the dark master drives the dark-appearance targets.
The girl is nested via its art bbox as viewBox, so it always lands centered in
the box (824 safe zone for squircles / height-fitted for the marks) without
distortion. The girl art corners sit ~185px from the squircle arc centers vs
the 245px radius, so no art touches the rounded corners on any OS mask.
GENERATED OUTPUTS ARE NOT COMMITTED. Everything this script writes is
gitignored and regenerated on demand by the consuming pipelines (website
prebuild, desktop prebuild/predev, installer prebuild, web prebuild — via
scripts/generate-icons.mjs). The freshness lane (icons-freshness-check.yml)
runs --check, which regenerates in memory and asserts structural invariants
(sizes, transparency, container frame sets) — there are no committed bytes to
byte-compare against.
Rendering: resvg (resvg-py) for SVG -> PNG fidelity at every size.
Containers: Pillow for multi-size .ico and .icns.
Deps:
Pillow (core dependency), resvg-py (dev extra):
uv sync --extra dev
Outputs (30 files):
assets/icon-master.svg generated light master
assets/icon-master-dark.svg generated dark master
apps/desktop/assets/icon.png 1024x1024 squircle (light)
apps/desktop/assets/icon.ico 16,24,32,48,64,128,256
apps/desktop/assets/icon.icns 16..1024 (real ICNS)
apps/desktop/assets/icon-dark.png 1024x1024 squircle (dark)
apps/desktop/assets/icon-dark.ico 16,24,32,48,64,128,256
apps/desktop/assets/icon-dark.icns 16..1024 (real ICNS)
apps/desktop/assets/appx/Wide310x150Logo.png 310x150, squircle 100 centered
apps/desktop/assets/appx/StoreLogo.png 50x50 squircle
apps/desktop/assets/appx/Square44x44Logo.png 44x44 squircle
apps/desktop/assets/appx/Square150x150Logo.png 150x150 squircle
apps/desktop/assets/appx/*-dark.png dark-appearance logos
apps/desktop/public/apple-touch-icon.png 1024x1024 squircle
apps/desktop/public/nous-girl.png 256x256 squircle, black girl (light mark)
apps/desktop/public/nous-girl-dark.png 256x256 squircle, white girl (dark mark)
apps/bootstrap-installer/src-tauri/icons/32x32.png 32x32
apps/bootstrap-installer/src-tauri/icons/128x128.png 128x128
apps/bootstrap-installer/src-tauri/icons/128x128@2x.png 256x256
apps/bootstrap-installer/src-tauri/icons/icon.ico 16,32,64,128,256
apps/bootstrap-installer/src-tauri/icons/icon.icns 16..1024
apps/bootstrap-installer/public/nous-girl.png 256x256 squircle mark (light)
website/static/img/logo.png 1772x1799 girl alone, transparent (light)
website/static/img/logo-dark.png 1772x1799 girl alone, transparent (dark)
website/static/img/nous-logo.png 150x150 on white (opaque)
website/static/img/nous-logo-dark.png 150x150 on #0d1117 (opaque)
website/static/img/favicon-16x16.png 16x16
website/static/img/favicon-32x32.png 32x32
website/static/img/apple-touch-icon.png 180x180
website/static/img/favicon.ico 16,32,48
website/static/img/favicon.svg copy of the light master
web/public/favicon.ico 16,32,48
"""
from __future__ import annotations
import io
import re
import sys
from pathlib import Path
from PIL import Image
try:
import resvg_py
except ImportError:
sys.exit(
"resvg-py is missing. Install it with:\n"
" uv sync --extra dev"
)
ROOT = Path(__file__).resolve().parent.parent
ASSETS = ROOT / "assets"
BACKGROUNDS = ASSETS / "backgrounds"
GIRL_BLACK = ASSETS / "nous-girl-black.svg"
GIRL_WHITE = ASSETS / "nous-girl-white.svg"
GIRLS = {"black": GIRL_BLACK, "white": GIRL_WHITE}
MASTER = ASSETS / "icon-master.svg"
MASTER_DARK = ASSETS / "icon-master-dark.svg"
# The squircle corner radius measured on the real 1024 icon (~245px).
SQUIRCLE_RADIUS = 245
# The nous dark background (#0d1117) — fixed dark tile/background everywhere.
DARK_HEX = "#0d1117"
DARK_RGB = (13, 17, 23)
# Girl placement per background: (x, y, w, h) in that background's coordinate
# space. Squircle backgrounds put the girl in the 824px HIG content safe zone
# (centered, 100px pad on a 1024 canvas). Marks reuse the same squircles.
GIRL_BOXES = {
"squircle-light.svg": (100, 100, 824, 824),
"squircle-dark.svg": (100, 100, 824, 824),
}
# The brand-kit SVG canvas (both girl svgs share this viewBox).
GIRL_VIEWBOX = 5487.0615
# Target sizes for --check's structural verification: relpath -> (format, size)
CHECK_SIZES: dict[str, tuple[str, tuple[int, int]]] = {
"apps/desktop/assets/icon.png": ("PNG", (1024, 1024)),
"apps/desktop/assets/icon-dark.png": ("PNG", (1024, 1024)),
"apps/desktop/assets/appx/Wide310x150Logo.png": ("PNG", (310, 150)),
"apps/desktop/assets/appx/Wide310x150Logo-dark.png": ("PNG", (310, 150)),
"apps/desktop/assets/appx/StoreLogo.png": ("PNG", (50, 50)),
"apps/desktop/assets/appx/StoreLogo-dark.png": ("PNG", (50, 50)),
"apps/desktop/assets/appx/Square44x44Logo.png": ("PNG", (44, 44)),
"apps/desktop/assets/appx/Square44x44Logo-dark.png": ("PNG", (44, 44)),
"apps/desktop/assets/appx/Square150x150Logo.png": ("PNG", (150, 150)),
"apps/desktop/assets/appx/Square150x150Logo-dark.png": ("PNG", (150, 150)),
"apps/desktop/public/apple-touch-icon.png": ("PNG", (1024, 1024)),
"apps/desktop/public/nous-girl.png": ("PNG", (256, 256)),
"apps/desktop/public/nous-girl-dark.png": ("PNG", (256, 256)),
"apps/bootstrap-installer/src-tauri/icons/32x32.png": ("PNG", (32, 32)),
"apps/bootstrap-installer/src-tauri/icons/128x128.png": ("PNG", (128, 128)),
"apps/bootstrap-installer/src-tauri/icons/128x128@2x.png": ("PNG", (256, 256)),
"apps/bootstrap-installer/public/nous-girl.png": ("PNG", (256, 256)),
"website/static/img/logo.png": ("PNG", (1772, 1799)),
"website/static/img/logo-dark.png": ("PNG", (1772, 1799)),
"website/static/img/nous-logo.png": ("PNG", (150, 150)),
"website/static/img/nous-logo-dark.png": ("PNG", (150, 150)),
"website/static/img/favicon-16x16.png": ("PNG", (16, 16)),
"website/static/img/favicon-32x32.png": ("PNG", (32, 32)),
"website/static/img/apple-touch-icon.png": ("PNG", (180, 180)),
}
# (relpath, kind, arg)
TARGETS: list[tuple[str, str, object]] = [
("assets/icon-master.svg", "svg", None),
("assets/icon-master-dark.svg", "svg_dark", None),
("apps/desktop/assets/icon.png", "png", 1024),
("apps/desktop/assets/icon.ico", "ico", [16, 24, 32, 48, 64, 128, 256]),
("apps/desktop/assets/icon.icns", "icns", None),
("apps/desktop/assets/icon-dark.png", "png_dark", 1024),
("apps/desktop/assets/icon-dark.ico", "ico_dark", [16, 24, 32, 48, 64, 128, 256]),
("apps/desktop/assets/icon-dark.icns", "icns_dark", None),
("apps/desktop/assets/appx/Wide310x150Logo.png", "wide", (310, 150)),
("apps/desktop/assets/appx/StoreLogo.png", "png", 50),
("apps/desktop/assets/appx/Square44x44Logo.png", "png", 44),
("apps/desktop/assets/appx/Square150x150Logo.png", "png", 150),
("apps/desktop/assets/appx/Wide310x150Logo-dark.png", "wide_dark", (310, 150)),
("apps/desktop/assets/appx/StoreLogo-dark.png", "png_dark", 50),
("apps/desktop/assets/appx/Square44x44Logo-dark.png", "png_dark", 44),
("apps/desktop/assets/appx/Square150x150Logo-dark.png", "png_dark", 150),
("apps/desktop/public/apple-touch-icon.png", "png", 1024),
("apps/desktop/public/nous-girl.png", "girl_light", 256),
("apps/desktop/public/nous-girl-dark.png", "girl_dark", 256),
("apps/bootstrap-installer/src-tauri/icons/32x32.png", "png", 32),
("apps/bootstrap-installer/src-tauri/icons/128x128.png", "png", 128),
("apps/bootstrap-installer/src-tauri/icons/128x128@2x.png", "png", 256),
("apps/bootstrap-installer/src-tauri/icons/icon.ico", "ico", [16, 32, 64, 128, 256]),
("apps/bootstrap-installer/src-tauri/icons/icon.icns", "icns", None),
("apps/bootstrap-installer/public/nous-girl.png", "girl_light", 256),
("website/static/img/logo.png", "logo", None),
("website/static/img/logo-dark.png", "logo_dark", None),
("website/static/img/nous-logo.png", "png_white", 150),
("website/static/img/nous-logo-dark.png", "png_dark_white", 150),
("website/static/img/favicon-16x16.png", "png", 16),
("website/static/img/favicon-32x32.png", "png", 32),
("website/static/img/apple-touch-icon.png", "png", 180),
("website/static/img/favicon.ico", "ico", [16, 32, 48]),
("website/static/img/favicon.svg", "svg_copy", None),
("web/public/favicon.ico", "ico", [16, 32, 48]),
]
# ─── girl art extraction ────────────────────────────────────────────────────
_path_cache: dict[str, str] = {}
_bbox_cache: dict[str, tuple[float, float, float, float]] = {}
def girl_path(girl: str) -> str:
"""The girl `<path>` element with editor metadata stripped (resvg rejects
undeclared inkscape/sodipodi prefixes)."""
if girl not in _path_cache:
src = GIRLS[girl].read_text(encoding="utf-8")
m = re.search(r"<path\b.*?/>", src, re.S)
assert m, f"no <path> found in {GIRLS[girl].name}"
path = re.sub(r'\s+(inkscape|sodipodi):[a-zA-Z-]+="[^"]*"', "", m.group(0))
_path_cache[girl] = path
return _path_cache[girl]
def girl_bbox(girl: str) -> tuple[float, float, float, float]:
"""Art bounding box in the girl SVG's coordinate space, measured by
rendering once and taking the alpha bbox (robust to art changes)."""
if girl not in _bbox_cache:
data = resvg_py.svg_to_bytes(svg_path=str(GIRLS[girl]), width=512, height=512)
im = Image.open(io.BytesIO(data))
bx, by, bx2, by2 = im.getchannel("A").point(lambda v: 255 if v > 0 else 0).getbbox()
s = GIRL_VIEWBOX / 512.0
_bbox_cache[girl] = (bx * s, by * s, (bx2 - bx) * s, (by2 - by) * s)
return _bbox_cache[girl]
def girl_layer(girl: str, box: tuple[float, float, float, float]) -> str:
"""Nested-svg layer: girl art (bbox as viewBox) placed into `box` — the
box's aspect is preserved via 'meet', so the girl never distorts."""
bx, by, bw, bh = girl_bbox(girl)
x, y, w, h = box
return (
f'<svg x="{x}" y="{y}" width="{w}" height="{h}" viewBox="{bx} {by} {bw} {bh}">\n'
f" {girl_path(girl)}\n"
" </svg>"
)
def background_inner(name: str) -> tuple[str, int, int]:
"""Inner content + (width, height) of a background SVG asset."""
text = (BACKGROUNDS / name).read_text(encoding="utf-8")
m = re.search(r'<svg\b[^>]*viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"[^>]*>', text)
assert m, f"cannot parse viewBox of {name}"
w, h = float(m.group(1)), float(m.group(2))
inner = re.sub(r"^.*?>\s*", "", text, count=1, flags=re.S)
inner = re.sub(r"\s*</svg>\s*$", "", inner, flags=re.S)
return inner, int(w), int(h)
def compose_svg(girl: str, bg: str) -> str:
"""Full svg text: background + girl layer, in the background's native
coordinate space (resvg scales to whatever output size is requested, so
the composition is size-agnostic — no manual box scaling)."""
inner, w, h = background_inner(bg)
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}">\n'
f" {inner.strip()}\n"
f" {girl_layer(girl, GIRL_BOXES[bg])}\n"
"</svg>\n"
)
def ensure_masters(check: bool = False) -> None:
"""Write the generated light/dark masters; placeholder fallback if the
girl art is missing."""
missing = [p for p in (GIRL_BLACK, GIRL_WHITE) if not p.exists()]
if missing:
if check:
sys.exit(f"[check] girl art missing: {[p.name for p in missing]}")
placeholder = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<rect x="0" y="0" width="1024" height="1024" rx="{SQUIRCLE_RADIUS}" fill="#ffffff"/>
<circle cx="512" cy="512" r="420" fill="#e34c4c"/>
</svg>
'''
for path in (MASTER, MASTER_DARK):
path.write_text(placeholder, encoding="utf-8")
print(f"[master] wrote placeholder masters ({[p.name for p in missing]})")
return
MASTER.write_text(compose_svg("black", "squircle-light.svg"), encoding="utf-8")
MASTER_DARK.write_text(compose_svg("white", "squircle-dark.svg"), encoding="utf-8")
# ─── rendering ──────────────────────────────────────────────────────────────
def render(master: Path, size: int, *, background: str | None = None) -> Image.Image:
"""Render a master to an RGBA PNG of `size`x`size`."""
data = resvg_py.svg_to_bytes(
svg_path=str(master), width=size, height=size, background=background
)
return Image.open(io.BytesIO(data)).convert("RGBA")
def render_svg(svg: str, size: int | tuple[int, int]) -> Image.Image:
if isinstance(size, int):
w = h = size
else:
w, h = size
data = resvg_py.svg_to_bytes(svg_string=svg, width=w, height=h)
return Image.open(io.BytesIO(data)).convert("RGBA")
def paste_centered(canvas: Image.Image, img: Image.Image) -> None:
x = (canvas.width - img.width) // 2
y = (canvas.height - img.height) // 2
canvas.alpha_composite(img, (x, y))
def save_png(img: Image.Image, buf: io.BytesIO) -> None:
"""Save with alpha preserved. Flat art quantizes losslessly to an 8-bit
palette (tRNS per-index alpha keeps the AA edges), so use that when the
palette round-trips pixel-identically; fall back to RGBA otherwise."""
if img.mode != "RGBA":
img.convert("RGBA").save(buf, "PNG", optimize=True)
return
quantized = img.quantize(colors=256, method=Image.FASTOCTREE, dither=Image.NONE)
if quantized.convert("RGBA").tobytes() == img.tobytes():
quantized.save(buf, "PNG", optimize=True)
else:
img.save(buf, "PNG", optimize=True)
def girl_mark(kind: str, size: int) -> Image.Image:
"""The girl in the app-icon squircle (BrandMark asset) — the mark IS the
icon shape. girl_light: black girl on white squircle. girl_dark: white
girl on #0d1117 squircle."""
if kind == "girl_light":
girl, bg = "black", "squircle-light.svg"
else:
girl, bg = "white", "squircle-dark.svg"
if not GIRLS[girl].exists():
print(f" [girl] {GIRLS[girl].name} missing — placeholder fallback")
return render(MASTER if kind == "girl_light" else MASTER_DARK, size)
return render_svg(compose_svg(girl, bg), size)
def build_logo_image(master: Path, dark: bool = False) -> Image.Image:
"""1772x1799 wordmark: the girl alone on transparency (no frame), centered.
Light = black girl, dark = white girl — the consuming surface's background
(navbar light/dark) shows through."""
W, H = 1772, 1799
svg = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}">\n {girl_layer("white" if dark else "black", (0, 0, W, H))}\n</svg>\n'
return render_svg(svg, (W, H))
def target_bytes(kind: str, arg: object) -> bytes:
"""Produce the exact bytes for one target. Shared by write + check."""
if kind == "svg":
return MASTER.read_bytes()
if kind == "svg_dark":
return MASTER_DARK.read_bytes()
if kind == "svg_copy":
return MASTER.read_bytes()
buf = io.BytesIO()
if kind == "png":
save_png(render(MASTER, arg), buf)
elif kind == "png_dark":
save_png(render(MASTER_DARK, arg), buf)
elif kind == "png_white":
render(MASTER, arg, background="#ffffff").convert("RGB").save(buf, "PNG", optimize=True)
elif kind == "png_dark_white":
render(MASTER_DARK, arg, background=DARK_HEX).convert("RGB").save(buf, "PNG", optimize=True)
elif kind in ("girl_light", "girl_dark"):
save_png(girl_mark(kind, arg), buf)
elif kind == "ico":
img = render(MASTER, max(arg))
img.save(buf, format="ICO", sizes=[(s, s) for s in arg])
elif kind == "ico_dark":
img = render(MASTER_DARK, max(arg))
img.save(buf, format="ICO", sizes=[(s, s) for s in arg])
elif kind == "icns":
img = render(MASTER, 1024)
frames = [img.resize((s, s), Image.LANCZOS) for s in (16, 32, 64, 128, 256, 512, 1024)]
img.save(buf, format="ICNS", append_images=frames[1:])
elif kind == "icns_dark":
img = render(MASTER_DARK, 1024)
frames = [img.resize((s, s), Image.LANCZOS) for s in (16, 32, 64, 128, 256, 512, 1024)]
img.save(buf, format="ICNS", append_images=frames[1:])
elif kind == "wide":
w, h = arg
canvas = Image.new("RGBA", (w, h), (0, 0, 0, 0))
paste_centered(canvas, render(MASTER, 100))
canvas.save(buf, "PNG")
elif kind == "wide_dark":
w, h = arg
canvas = Image.new("RGBA", (w, h), (0, 0, 0, 0))
paste_centered(canvas, render(MASTER_DARK, 100))
canvas.save(buf, "PNG")
elif kind == "logo":
build_logo_image(MASTER, dark=False).save(buf, "PNG")
elif kind == "logo_dark":
build_logo_image(MASTER_DARK, dark=True).save(buf, "PNG")
else:
raise ValueError(f"unknown kind {kind!r}")
return buf.getvalue()
def cmd_write() -> None:
ensure_masters()
written = 0
for rel, kind, arg in TARGETS:
path = ROOT / rel
path.parent.mkdir(parents=True, exist_ok=True)
try:
path.write_bytes(target_bytes(kind, arg))
written += 1
except Exception as exc: # noqa: BLE001 - report and keep going
print(f" !! {rel}: FAILED ({exc})")
print(f"[ok] wrote {written}/{len(TARGETS)} files")
print("\n[verify]")
for rel, kind, arg in TARGETS:
path = ROOT / rel
try:
if kind in ("svg", "svg_dark", "svg_copy"):
print(f" {rel}: {path.stat().st_size} bytes SVG")
continue
im = Image.open(path)
if kind in ("ico", "ico_dark"):
sizes = []
try:
for i in range(im.n_frames):
im.seek(i)
sizes.append(im.size)
except Exception:
sizes = [im.size]
print(f" {rel}: ICO {sorted(set(sizes))}")
elif kind in ("icns", "icns_dark"):
print(f" {rel}: ICNS {im.size} (container)")
else:
print(f" {rel}: {im.format} {im.size}")
except Exception as exc:
print(f" {rel}: VERIFY FAILED ({exc})")
def cmd_check() -> int:
"""Structural verification: regenerate every target in memory and assert
the invariants that actually matter (there are no committed bytes to
byte-compare — outputs are generated on demand)."""
ensure_masters(check=True)
problems: list[str] = []
# every target must generate without error
for rel, kind, arg in TARGETS:
try:
target_bytes(kind, arg)
except Exception as exc: # noqa: BLE001
problems.append(f"{rel}: REGENERATE FAILED ({exc})")
# PNG targets must have the expected format + size
for rel, (fmt, size) in CHECK_SIZES.items():
path = ROOT / rel
if not path.exists():
problems.append(f"{rel}: MISSING (expected generated file)")
continue
try:
im = Image.open(path)
if im.format != fmt or im.size != size:
problems.append(f"{rel}: got {im.format} {im.size}, expected {fmt} {size}")
except Exception as exc: # noqa: BLE001
problems.append(f"{rel}: UNREADABLE ({exc})")
# squircles must keep transparent corners (alpha extrema include 0)
for rel in (
"apps/desktop/assets/icon.png",
"apps/desktop/assets/icon-dark.png",
"apps/desktop/public/nous-girl.png",
"apps/desktop/public/nous-girl-dark.png",
"apps/desktop/public/apple-touch-icon.png",
):
path = ROOT / rel
if not path.exists():
continue
alpha = Image.open(path).convert("RGBA").getchannel("A")
lo, hi = alpha.getextrema()
if lo != 0 or hi != 255:
problems.append(f"{rel}: alpha extrema {alpha.getextrema()}, expected (0, 255) transparent corners")
# containers must have the right frame sets (parse ICO headers directly —
# PIL's ICO n_frames is unreliable across versions)
def ico_sizes(path: Path) -> list[int]:
data = path.read_bytes()
count = int.from_bytes(data[4:6], "little")
sizes = []
for i in range(count):
entry = data[6 + i * 16 : 6 + (i + 1) * 16]
w = entry[0] or 256
h = entry[1] or 256
sizes.append(w)
return sorted(set(sizes))
for rel, sizes in (
("apps/desktop/assets/icon.ico", [16, 24, 32, 48, 64, 128, 256]),
("apps/desktop/assets/icon-dark.ico", [16, 24, 32, 48, 64, 128, 256]),
("apps/bootstrap-installer/src-tauri/icons/icon.ico", [16, 32, 64, 128, 256]),
):
path = ROOT / rel
if not path.exists():
problems.append(f"{rel}: MISSING")
continue
try:
got = ico_sizes(path)
if got != sizes:
problems.append(f"{rel}: ICO frames {got}, expected {sizes}")
except Exception as exc: # noqa: BLE001
problems.append(f"{rel}: UNREADABLE ({exc})")
if not problems:
print(f"[ok] all {len(TARGETS)} targets generate and pass structural checks")
return 0
print(f"[check] {len(problems)} problem(s):")
for line in problems:
print(f" - {line}")
return 1
def main() -> None:
if "--check" in sys.argv:
sys.exit(cmd_check())
cmd_write()
if __name__ == "__main__":
main()