"""PETSCII -> display-string conversion for the debug trace. Data bytes the C64 sends to a printer are PETSCII, not ASCII. For the upper- case/graphics character set (the C64 power-on default) the printable sub-ranges line up with ASCII for letters, digits and most punctuation, with a handful of Commodore-specific glyphs (``£ ↑ ←``). Control codes (RETURN, RVS, colour changes, cursor moves) are rendered with short symbolic names so the trace stays readable. This is deliberately a *display* mapping for logging, not a font renderer. The goal (PLAN.md §8) is: printable PETSCII -> its glyph; control code -> ````; anything else (graphics characters) -> ``.``. """ from __future__ import annotations from dataclasses import dataclass # Symbolic names for the C64 control codes that actually turn up in a printer # byte stream. Anything not in here renders as ``<$xx>``. _CONTROL_NAMES = { 0x03: "STOP", 0x05: "WHT", 0x08: "SHIFT-DIS", 0x09: "SHIFT-EN", 0x0A: "LF", 0x0D: "CR", 0x0E: "LOWER", 0x11: "DOWN", 0x12: "RVS-ON", 0x13: "HOME", 0x14: "DEL", 0x1C: "RED", 0x1D: "RIGHT", 0x1E: "GRN", 0x1F: "BLU", 0x81: "ORG", 0x8D: "SHIFT-CR", 0x90: "BLK", 0x91: "UP", 0x92: "RVS-OFF", 0x93: "CLR", 0x94: "INST", 0x95: "BRN", 0x99: "GRY", 0x9D: "LEFT", 0x9E: "YEL", 0x9F: "CYN", } # Printable glyphs that differ from plain ASCII in the upper-case/graphics set. _GLYPH_OVERRIDES = { 0x5C: "£", # £ 0x5E: "↑", # ↑ 0x5F: "←", # ← 0xA0: " ", # shifted space } CAT_PRINTABLE = "printable" CAT_CONTROL = "control" CAT_GRAPHIC = "graphic" @dataclass(frozen=True) class Glyph: """The rendered form of one PETSCII byte.""" text: str # what to show in the trace category: str # CAT_PRINTABLE | CAT_CONTROL | CAT_GRAPHIC @property def printable(self) -> bool: return self.category == CAT_PRINTABLE def to_glyph(byte: int) -> Glyph: """Map one PETSCII byte to a :class:`Glyph` for the debug trace.""" if not 0 <= byte <= 0xFF: raise ValueError(f"byte out of range: {byte!r}") if byte in _GLYPH_OVERRIDES: return Glyph(_GLYPH_OVERRIDES[byte], CAT_PRINTABLE) # Space through '_' (minus the overrides above) matches ASCII directly. if 0x20 <= byte <= 0x5F: return Glyph(chr(byte), CAT_PRINTABLE) # Control ranges: $00-$1F and $80-$9F. if byte <= 0x1F or 0x80 <= byte <= 0x9F: name = _CONTROL_NAMES.get(byte, f"${byte:02X}") return Glyph(f"<{name}>", CAT_CONTROL) # Everything else ($60-$7F, $A0-$FF) is graphics / shifted glyphs in the # upper-case set -- not meaningful as printer text, render as '.'. return Glyph(".", CAT_GRAPHIC)