joeylib2/scripts/lib/pspKeyboardPath.py

170 lines
5.7 KiB
Python

#!/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()