"""Assemble the Apple II boot/viewer with `xa` (origin $0800, raw bytes).""" from __future__ import annotations import os import shutil import subprocess import tempfile VIEWER_DIR = os.path.dirname(os.path.abspath(__file__)) SOURCES = {"hgr": "hgr.s", "dhgr": "dhgr.s"} _cache: dict[tuple, bytes] = {} # How long the viewer holds the picture (see apple/viewer/awyt.i). WAIT_MODES = {"forever": 0, "key": 1, "seconds": 2} SS_WAITMODE = {"key": 1, "seconds": 2, "both": 3} def build_slideshow_stub(n_images: int, advance: str = "both", seconds: int = 10, loop: bool = True) -> bytes: """Assemble the Apple HGR slideshow boot loader (one 256-byte boot sector). Reads NIMAGES * 32 sectors into the $4000 buffer and cycles them; must fit a single boot sector since the Disk II ROM only loads sector 0. """ import shutil as _sh if not _sh.which("xa"): raise AssemblerError("The 'xa' assembler was not found on PATH.") end_page = 0x40 + n_images * 0x20 wrapper = (f"#define WAITMODE {SS_WAITMODE[advance]}\n" f"#define WAITSECS {max(0, min(255, int(seconds)))}\n" f"#define NIMAGES {n_images}\n" f"#define LOOPFLAG {1 if loop else 0}\n" f"#define ENDPAGE ${end_page:02X}\n" '#include "slideshow.s"\n') with tempfile.TemporaryDirectory() as td: out = os.path.join(td, "v.bin") fd, wrap = tempfile.mkstemp(suffix=".s", prefix="_wrap_", dir=VIEWER_DIR) try: with os.fdopen(fd, "w") as f: f.write(wrapper) proc = subprocess.run(["xa", "-o", out, os.path.basename(wrap)], capture_output=True, text=True, cwd=VIEWER_DIR) if proc.returncode != 0: raise AssemblerError(f"xa failed:\n{proc.stdout}{proc.stderr}") with open(out, "rb") as f: raw = f.read() finally: os.unlink(wrap) if len(raw) > 256: raise AssemblerError( f"Apple slideshow boot loader is {len(raw)} bytes, over the 256-byte " "boot sector") return raw class AssemblerError(RuntimeError): pass def have_xa() -> bool: return shutil.which("xa") is not None def assemble_stub(viewer_key: str, display: str = "forever", seconds: int = 0) -> bytes: waitmode = WAIT_MODES.get(display, 0) secs = max(0, min(255, int(seconds))) # 8-bit delay counter key = (viewer_key, waitmode, secs) if key in _cache: return _cache[key] if not have_xa(): raise AssemblerError("The 'xa' assembler was not found on PATH.") if not os.path.exists(os.path.join(VIEWER_DIR, SOURCES[viewer_key])): raise AssemblerError(f"viewer source missing: {SOURCES[viewer_key]}") # Wrapper sets options then includes the real source; run from VIEWER_DIR so # the source's #include "awyt.i" resolves (xa looks relative to cwd). wrapper = (f"#define WAITMODE {waitmode}\n" f"#define WAITSECS {secs}\n" f'#include "{SOURCES[viewer_key]}"\n') with tempfile.TemporaryDirectory() as td: out = os.path.join(td, "v.bin") fd, wrap = tempfile.mkstemp(suffix=".s", prefix="_wrap_", dir=VIEWER_DIR) try: with os.fdopen(fd, "w") as f: f.write(wrapper) proc = subprocess.run(["xa", "-o", out, os.path.basename(wrap)], capture_output=True, text=True, cwd=VIEWER_DIR) if proc.returncode != 0: raise AssemblerError( f"xa failed for {viewer_key}:\n{proc.stdout}{proc.stderr}") with open(out, "rb") as f: raw = f.read() finally: os.unlink(wrap) _cache[key] = raw return raw