120 lines
4.4 KiB
Python
120 lines
4.4 KiB
Python
"""Generate a small, colourful Commodore 64 BASIC program (tokenised .PRG) that
|
|
prints image metadata. We emit the tokenised bytes directly -- token bytes for
|
|
the few keywords used (PRINT, POKE) and PETSCII for everything else -- so there
|
|
is no dependency on an external BASIC tokeniser.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
# BASIC V2 keyword tokens.
|
|
_PRINT = 0x99
|
|
_POKE = 0x97
|
|
_CHR = 0xC7 # CHR$( function token
|
|
_QUOTE = 0x22
|
|
|
|
# PETSCII control codes.
|
|
CLR = 0x93
|
|
RVON = 0x12
|
|
RVOFF = 0x92
|
|
# colours
|
|
WHITE, RED, GREEN, BLUE = 0x05, 0x1c, 0x1e, 0x1f
|
|
BLACK, ORANGE, BROWN = 0x90, 0x81, 0x95
|
|
LT_RED, DK_GREY, GREY, LT_GREEN = 0x96, 0x97, 0x98, 0x99
|
|
LT_BLUE, LT_GREY, PURPLE, YELLOW, CYAN = 0x9a, 0x9b, 0x9c, 0x9e, 0x9f
|
|
|
|
# label colours cycled down the screen (none black, all readable on black).
|
|
_LABEL_COLOURS = [CYAN, YELLOW, LT_GREEN, LT_RED, LT_BLUE, PURPLE, ORANGE, GREEN]
|
|
# per-character rainbow for the title (bright, distinct, no black).
|
|
_RAINBOW = [RED, ORANGE, YELLOW, LT_GREEN, GREEN, CYAN, LT_BLUE, BLUE, PURPLE,
|
|
LT_RED, WHITE]
|
|
|
|
_SCREEN_W = 40
|
|
_VALUE_COL = 11 # column where a value starts (= label field width)
|
|
_LINE_MAX = _SCREEN_W - 1 # keep lines < 40 so the screen never auto-wraps
|
|
_VALUE_W = _LINE_MAX - _VALUE_COL # printable value chars per line
|
|
|
|
|
|
def _petscii(text: str) -> bytes:
|
|
"""Map an ASCII string to printable PETSCII (upper-case glyph range)."""
|
|
out = bytearray()
|
|
for ch in str(text).upper():
|
|
b = ord(ch)
|
|
if b == _QUOTE:
|
|
b = 0x27 # avoid closing the BASIC string
|
|
if 0x20 <= b <= 0x5F:
|
|
out.append(b)
|
|
else:
|
|
out.append(0x2E) # '.'
|
|
return bytes(out)
|
|
|
|
|
|
def _print_str(inner: bytes) -> bytes:
|
|
return bytes([_PRINT, _QUOTE]) + inner + bytes([_QUOTE])
|
|
|
|
|
|
def _assemble(lines: list[tuple[int, bytes]]) -> bytes:
|
|
"""Link tokenised lines into a PRG (load address $0801)."""
|
|
cur = 0x0801
|
|
pieces = []
|
|
for num, toks in lines:
|
|
body = bytes([num & 0xFF, (num >> 8) & 0xFF]) + toks + b"\x00"
|
|
nxt = cur + 2 + len(body)
|
|
pieces.append(bytes([nxt & 0xFF, (nxt >> 8) & 0xFF]) + body)
|
|
cur = nxt
|
|
return bytes([0x01, 0x08]) + b"".join(pieces) + b"\x00\x00"
|
|
|
|
|
|
def _rainbow_title(text: str) -> bytes:
|
|
"""Centred, per-character rainbow title (control codes don't take columns)."""
|
|
pad = max(0, (_SCREEN_W - len(text)) // 2)
|
|
out = bytes([CLR]) + _petscii(" " * pad)
|
|
for k, ch in enumerate(text):
|
|
out += bytes([_RAINBOW[k % len(_RAINBOW)]]) + _petscii(ch)
|
|
return bytes([_PRINT, _QUOTE]) + out + bytes([_QUOTE])
|
|
|
|
|
|
def _field_lines(label: str, value: str, colour: int) -> list[bytes]:
|
|
"""Word-/width-wrapped PRINT lines for one field; continuations are indented
|
|
to the value's start column so a long value lines up under itself."""
|
|
label_p = _petscii((label + ":").ljust(_VALUE_COL))
|
|
value = str(value)[:_VALUE_W * 4] # cap at four screen lines
|
|
chunks = [value[i:i + _VALUE_W] for i in range(0, len(value), _VALUE_W)] or [""]
|
|
|
|
out = [_print_str(bytes([colour]) + label_p + bytes([WHITE]) + _petscii(chunks[0]))]
|
|
indent = _petscii(" " * _VALUE_COL)
|
|
for chunk in chunks[1:]:
|
|
out.append(_print_str(bytes([WHITE]) + indent + _petscii(chunk)))
|
|
return out
|
|
|
|
|
|
def build_info_prg(fields: list[tuple[str, str]]) -> bytes:
|
|
"""Return a tokenised BASIC PRG that prints ``fields`` (label, value)."""
|
|
lines: list[tuple[int, bytes]] = []
|
|
num = 0
|
|
|
|
def add(toks: bytes):
|
|
nonlocal num
|
|
num += 10
|
|
lines.append((num, toks))
|
|
|
|
# border and background both black
|
|
add(bytes([_POKE]) + b"53280,0:" + bytes([_POKE]) + b"53281,0")
|
|
add(_rainbow_title("8 Bit Lenser picture info"))
|
|
add(bytes([_PRINT])) # blank line
|
|
|
|
for i, (label, value) in enumerate(fields):
|
|
col = _LABEL_COLOURS[i % len(_LABEL_COLOURS)]
|
|
for line in _field_lines(label, value, col):
|
|
add(line)
|
|
|
|
add(bytes([_PRINT]))
|
|
# PRINT " load "CHR$(34)"*"CHR$(34)",8,1 to view picture" -- CHR$(34) is the
|
|
# double-quote, which can't appear literally inside a BASIC string.
|
|
q = bytes([_CHR]) + _petscii("(34)")
|
|
add(bytes([_PRINT])
|
|
+ bytes([_QUOTE]) + bytes([GREY]) + _petscii(" load ") + bytes([_QUOTE])
|
|
+ q
|
|
+ bytes([_QUOTE]) + _petscii("*") + bytes([_QUOTE])
|
|
+ q
|
|
+ bytes([_QUOTE]) + _petscii(",8,1 to view picture") + bytes([_QUOTE]))
|
|
return _assemble(lines)
|