"""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("c64view 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)