#!/usr/bin/env bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT # # verify-dos-input.sh - Headless typed-character acceptance gate for the # DOS port. Runs KEYS.EXE in DOSBox Staging under Xvfb, types the # RetroNet definition-of-done string with xdotool (real X key events, # shift and all, so ':' exercises the INT 9 shift path), takes a RAW # native-resolution screenshot, and decodes the example's verification # scanline (row 198) from the exact VGA DAC colors. KEYS gives all 16 # palette entries distinct colors precisely so this decode is exact. # Then the named keys a second key gives (scripts/lib/keysGate.sh): the # keypad's arrows and Enter, and the right Ctrl and Alt (the INT 9 # handler's 0xE0-prefixed codes). A mapper file binds just these keys and # puts the screenshot on F11 (scripts/lib/dosboxKeys.sh): DOSBox's Ctrl+F5 # would hand KEYS a Ctrl press. # # Usage: scripts/verify-dos-input.sh # Requires: DOSBox Staging (install.sh install emulator:dos), Xvfb, xdotool, python3 + PIL; # build/dos/bin/KEYS.EXE (make dos). set -euo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" source "$repo/scripts/lib/dosboxKeys.sh" source "$repo/scripts/lib/keysGate.sh" joeyNeed DOSBOX dosbox bin_dir=$repo/build/dos/bin conf=$repo/scripts/dosbox-386sx16.conf target="192.168.1.10:6510" [ -f "$bin_dir/KEYS.EXE" ] || { echo "verify-dos-input: build KEYS.EXE first (make dos)" >&2; exit 2; } work=$(mktemp -d -t joeylib-dos-input.XXXXXX) display_num=$((RANDOM % 500 + 300)) cleanup() { joeyStop "${dosbox_pid:-}" [ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null rm -rf "$work" } trap cleanup EXIT mkdir -p "$work/shots" dosboxKeysMapper "$work/mapper.txt" "${DOSBOX_KEYS_TYPING[@]}" "${DOSBOX_KEYS_NAMED[@]}" Xvfb ":$display_num" -screen 0 1024x768x24 >/dev/null 2>&1 & xvfb_pid=$! sleep 1 XDG_CONFIG_HOME="$work/xdg" DISPLAY=":$display_num" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ --conf "$conf" \ --set "capture_dir=$work/shots" \ --set "mapperfile=$work/mapper.txt" \ -c "MOUNT C $bin_dir" -c "C:" -c "KEYS.EXE" \ >"$work/dosbox.log" 2>&1 & dosbox_pid=$! # Let DOS boot and KEYS reach its poll loop on the simulated 386SX-16. sleep 12 # Staging titles its window " - cycles/ms"; it is the only # client on this private display, so match any named window. win=$(joeyFindWindow ":$display_num" "$dosbox_pid") if [ -z "$win" ]; then echo "verify-dos-input: FAIL - DOSBox window not found" >&2 exit 1 fi DISPLAY=":$display_num" xdotool windowactivate --sync "$win" 2>/dev/null || true DISPLAY=":$display_num" xdotool type --window "$win" --delay 150 -- "$target" sleep 2 png="$work/typed.png" if ! dosboxShot ":$display_num" "$win" "$work/shots" "$png"; then echo "verify-dos-input: FAIL - no screenshot captured" >&2 exit 1 fi python3 - "$png" "$target" <<'PY' import sys from PIL import Image png, target = sys.argv[1], sys.argv[2] img = Image.open(png).convert("RGB") w, h = img.size # Raw capture is native 320x200; tolerate an integer upscale. sx, sy = w // 320, h // 200 if sx < 1 or sy < 1 or w % 320 or h % 200: print(f"verify-dos-input: FAIL - unexpected screenshot size {w}x{h}") sys.exit(1) def dac8(c4): v6 = ((c4 << 2) | (c4 >> 2)) & 0x3F return (v6 << 2) | (v6 >> 4) # KEYS palette 0 (see buildPalette): gray ramp with the four demo # colors and red at 15. 12-bit 0x0RGB -> exact 8-bit via the port's # 6-bit DAC expansion. pal12 = [(i << 8) | (i << 4) | i for i in range(16)] pal12[0], pal12[1], pal12[2], pal12[3] = 0x000, 0x333, 0x0F0, 0xFFF pal12[15] = 0xF00 # Nearest-color match: emulators differ in how they scale the 6-bit # DAC to 8-bit (bit-replication vs rounding), but the palette entries # sit ~17 units apart per channel, so nearest is unambiguous. palRgb = [(dac8((v >> 8) & 0xF), dac8((v >> 4) & 0xF), dac8(v & 0xF)) for v in pal12] def pix(px): r, g, b = img.getpixel((px * sx, 198 * sy)) best, bestDist = 0, 1 << 30 for n, (pr, pg, pb) in enumerate(palRgb): d = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2 if d < bestDist: best, bestDist = n, d if bestDist > 3 * 8 ** 2: print(f"verify-dos-input: FAIL - pixel {px} color {(r, g, b)} too far from any palette entry") sys.exit(1) return best row = bytes((pix(2 * i) << 4) | pix(2 * i + 1) for i in range(40)) expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii") got = row[: len(expect)] print("VERIFY-DOS-INPUT row=" + row.hex().upper()) if got != expect: print(f"verify-dos-input: FAIL\n expected: {expect.hex().upper()}\n got: {got.hex().upper()}") sys.exit(1) PY # The stage in the raw screenshot: KEYS's row 198 found where it is. found=$(python3 "$repo/scripts/lib/keysRow.py" "$png" "$target") || true if [[ ! "$found" =~ row\ at\ \(([0-9]+),([0-9]+)\)\ at\ ([0-9]+)x ]]; then echo "verify-dos-input: FAIL - KEYS's stage is not in the screenshot: $found" >&2 exit 1 fi scale=${BASH_REMATCH[3]} stageAt="${BASH_REMATCH[1]},$((BASH_REMATCH[2] - 198 * scale))" # X's names for the keys (scripts/lib/keysGate.sh's). declare -A xKey=([up]=Up [kp8]=KP_Up [kp2]=KP_Down [kp4]=KP_Left [kp6]=KP_Right [kpenter]=KP_Enter [rctrl]=Control_R [ralt]=Alt_R) KEYS_LABEL=verify-dos-input keysPress() { DISPLAY=":$display_num" xdotool keydown --window "$win" "${xKey[$1]}" } keysRelease() { DISPLAY=":$display_num" xdotool keyup --window "$win" "${xKey[$1]}" } keysRead() { if dosboxShot ":$display_num" "$win" "$work/shots" "$work/cells.png"; then python3 "$repo/scripts/lib/keysCells.py" "$work/cells.png" --at "$stageAt" --scale "$scale" else echo "no screenshot" fi } keysRunSteps kp8 kp2 kp4 kp6 kpenter rctrl ralt if [ -n "$KEYS_FAILS" ]; then echo "verify-dos-input: FAIL - $KEYS_FAILS" >&2 exit 1 fi echo "verify-dos-input: PASS (typed \"$target\", all ${#target} chars arrived via jlInputGetChar; $KEYS_STEPS named-key steps: the keypad's arrows and Enter, right Ctrl and Alt, Up held with its second key)"