#!/usr/bin/env python3 # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT """pspKeyboardPath.py - the PSP keyboard's d-pad moves and presses for some typing, for a gate to send the PSP. The keyboard (src/hosted/psp/pspKeyboard.c) is read from its own source: its rows of KEY_* entries, each key's width in half keys, what it types and its legend, and where the cursor starts. Its cursor is followed as the keyboard moves it -- along a row wrapping round, and up or down a row onto the key nearest the middle of the one it was on (ties: down-right going down, up-left going up). Each step is printed on a line of its own: move left|right|up|down one d-pad press press LEGEND cross on the key with that legend pspKeyboardPath.py SOURCE TOKEN... A TOKEN is "key:LEGEND" (a key by its legend: ESC, F5, SHIFT, ENTER...) or "type:TEXT" (each character of TEXT, Shift pressed first for a shifted one, which the keyboard lets go after the next key). """ import re import sys def chars(text): """A C character literal's character ('\\'' and '\\\\' unescaped).""" text = text.strip() match = re.fullmatch(r"\(uint8_t\)\((.*)\)", text) if match: text = match.group(1) if text.startswith("'") and text.endswith("'"): body = text[1:-1] return body[1] if body.startswith("\\") else body return None def args(call): """The arguments of a KEY_*(...) entry, split at top-level commas.""" out = [] depth = 0 quote = None current = "" i = 0 while i < len(call): ch = call[i] if quote: current += ch if ch == "\\": current += call[i + 1] i += 1 elif ch == quote: quote = None elif ch in "'\"": quote = ch current += ch elif ch == "(": depth += 1 current += ch elif ch == ")": depth -= 1 current += ch elif ch == "," and depth == 0: out.append(current.strip()) current = "" else: current += ch i += 1 out.append(current.strip()) return out def width(text): return int(text.rstrip("u")) def keyboard(source): """The rows (each a list of keys: halves, legend, plain, shifted) and the cursor's starting row and column.""" text = open(source).read() defines = dict(re.findall(r"#define (CURSOR_(?:ROW|COL)_START)\s+(\d+)u", text)) rows = [] for body in re.findall(r"static const BoardKeyT gRow\d+\[\] = \{(.*?)\n\};", text, re.S): row = [] for kind, call in re.findall(r"(KEY_[A-Z]+)\((.*)\),?\s*$", body, re.M): a = args(call) if kind == "KEY_LETTER": row.append((2, a[0], chars(a[1]), a[0])) elif kind == "KEY_DIGIT": row.append((2, a[0], chars(a[1]), chars(a[2]))) elif kind == "KEY_PUNCT": row.append((width(a[0]), chars(a[1]), chars(a[1]), chars(a[2]))) elif kind == "KEY_NAMED": plain = chars(a[2]) row.append((width(a[0]), a[3].strip('"'), plain, plain)) elif kind == "KEY_LATCH": row.append((width(a[0]), a[3].strip('"'), None, None)) rows.append(row) return rows, int(defines["CURSOR_ROW_START"]), int(defines["CURSOR_COL_START"]) def center(row, col): """Twice the middle of a key, in half keys.""" start = sum(k[0] for k in row[:col]) return 2 * start + row[col][0] def path(rows, start, target): """Moves from start (row, col) to target, and the cursor's place after each, as the keyboard makes them: rows first, then along the row.""" moves = [] r, c = start tr, tc = target while r != tr: middle = center(rows[r], c) down = (tr - r) % len(rows) <= (r - tr) % len(rows) r = (r + 1) % len(rows) if down else (r - 1) % len(rows) best = None for col in range(len(rows[r])): d = abs(center(rows[r], col) - middle) if best is None or d < best or (down and d == best): best, c = d, col moves.append("move " + ("down" if down else "up")) count = len(rows[r]) right = (tc - c) % count left = (c - tc) % count moves += ["move right"] * right if right <= left else ["move left"] * left return moves, (r, tc) def find(rows, want, byLegend): """(row, col) of the key with legend want, or else of the key that types want; and whether that is its shifted character.""" for r, row in enumerate(rows): for c, key in enumerate(row): if byLegend and key[1] == want: return (r, c), False if not byLegend and key[2] == want: return (r, c), False for r, row in enumerate(rows): for c, key in enumerate(row): if not byLegend and key[3] == want: return (r, c), True raise SystemExit("pspKeyboardPath: no key shows or types %r" % want) def main(): if len(sys.argv) < 3: raise SystemExit(__doc__) rows, r0, c0 = keyboard(sys.argv[1]) at = (r0, c0) for token in sys.argv[2:]: kind, _, value = token.partition(":") wants = [value] if kind == "key" else list(value) for want in wants: spot, shifted = find(rows, want, kind == "key") if shifted: shiftSpot, _ = find(rows, "SHIFT", True) moves, at = path(rows, at, shiftSpot) print("\n".join(moves + ["press SHIFT"])) moves, at = path(rows, at, spot) print("\n".join(moves + ["press " + rows[at[0]][at[1]][1]])) if __name__ == "__main__": main()