First public commit.

This commit is contained in:
The Dust Council 2026-07-03 19:35:35 -07:00
parent 2a48f52979
commit 4bac9d83ed
288 changed files with 18417 additions and 1076 deletions

120
lenser/basicgen.py Normal file
View file

@ -0,0 +1,120 @@
"""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)