170 lines
5.7 KiB
Python
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()
|