mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 12:49:03 -05:00
chore: merge pad-aware interface hints
This commit is contained in:
commit
a8fef3fbbd
10 files changed
+6462
-54
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "ea2777500114b796bb4a23ea9e0dd0a15bc4499bb1289e6e426943beece48865"
|
||||
$defaultInstallSha256 = "e2cc5f073a8226fe26b0513ebd25f00de6f3f613d8c9851473c2d30b53356f48"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+274
-43
@@ -9675,6 +9675,9 @@ class TuiState:
|
||||
def __init__(self, os_type: str = "linux", home: Path | None = None) -> None:
|
||||
self.os_type = os_type
|
||||
self.home = home or Path("/home/user")
|
||||
# The buttons the bar names: a pad route (TUI_PAD_ROUTES), "keys"
|
||||
# for a keyboard alone, "both" where the code cannot tell.
|
||||
self.pad: str = "both"
|
||||
self.stack: list = ["setup"]
|
||||
self.focus: dict = {}
|
||||
self.filters: dict = {}
|
||||
@@ -9778,6 +9781,36 @@ def plain_requested(args: argparse.Namespace, env: Mapping[str, str] | None = No
|
||||
return bool(args.plain) or _env_on(env, "RETROBIOS_PLAIN") or _env_on(env, "ACCESSIBLE")
|
||||
|
||||
|
||||
# Each handheld launcher names the buttons that reach the interface
|
||||
# (ports/shared/tui.txt exports RB_PAD as this), so the bar names those
|
||||
# buttons and no others.
|
||||
TUI_PAD_ENV = "RETROBIOS_PAD"
|
||||
TUI_PAD_ROUTES = ("gptokeyb", "muxterm", "evmapy", "miyoo")
|
||||
# Set by Steam for what it starts, and by gamescope: a controller may be
|
||||
# all there is, mapped by Steam Input to keys nobody here can name.
|
||||
_STEAM_MARKERS = ("SteamGamepadUI", "SteamDeck", "SteamGameId", "GAMESCOPE_WAYLAND_DISPLAY")
|
||||
|
||||
|
||||
def tui_pad_route(os_type: str, env: Mapping[str, str] | None = None) -> str:
|
||||
"""The keys the hints name: a launcher's pad route, "keys" at a desktop,
|
||||
"both" (button and key) where nothing says which one is held."""
|
||||
env = os.environ if env is None else env
|
||||
named = env.get(TUI_PAD_ENV, "").strip().lower()
|
||||
if named:
|
||||
return named if named in TUI_PAD_ROUTES else "both"
|
||||
if any(env.get(name) for name in _STEAM_MARKERS):
|
||||
return "both"
|
||||
if os_type in ("windows", "darwin", "wsl"):
|
||||
return "keys"
|
||||
# A Linux console is where a handheld draws: a desktop or a remote
|
||||
# shell is a keyboard's.
|
||||
if os_type == "linux" and env.get("TERM") != "linux" and any(
|
||||
env.get(name) for name in ("DISPLAY", "WAYLAND_DISPLAY", "SSH_CONNECTION",
|
||||
"SSH_TTY")):
|
||||
return "keys"
|
||||
return "both"
|
||||
|
||||
|
||||
def _terminal_size(stream: TextIO) -> tuple[int, int]:
|
||||
try:
|
||||
size = os.get_terminal_size(stream.fileno())
|
||||
@@ -9939,7 +9972,10 @@ def _filter_row(state: TuiState) -> TuiRow:
|
||||
return TuiRow("filter", f"Filter: {text}_")
|
||||
if text:
|
||||
return TuiRow("filter", f"Filter: {text}")
|
||||
return TuiRow("filter", "Filter (/ to type)", style="dim")
|
||||
# A pad has no / and no letters: muxterm's own keyboard types.
|
||||
how = {"muxterm": "Select shows a keyboard", "gptokeyb": "with a keyboard",
|
||||
"evmapy": "with a keyboard", "miyoo": "with a keyboard"}.get(state.pad, "/ to type")
|
||||
return TuiRow("filter", f"Filter ({how})", style="dim")
|
||||
|
||||
|
||||
def _system_titles(state: TuiState) -> dict:
|
||||
@@ -10696,6 +10732,8 @@ def tui_update(state: TuiState, key: TuiKey) -> list[tuple]:
|
||||
return _path_key(state, key)
|
||||
if state.editing:
|
||||
return _filter_key(state, key)
|
||||
if state.pad == "miyoo":
|
||||
key = _miyoo_key(state, key)
|
||||
char = key.text if key.name == "char" else ""
|
||||
if key.name == "f1" or char == "?":
|
||||
state.help = True
|
||||
@@ -10777,6 +10815,29 @@ def tui_update(state: TuiState, key: TuiKey) -> list[tuple]:
|
||||
return []
|
||||
|
||||
|
||||
# A Miyoo's L1 and R1 are the keys E and T (codes 18 and 20).
|
||||
_MIYOO_PAGES = {"e": "pgup", "t": "pgdn"}
|
||||
|
||||
|
||||
def _miyoo_key(state: TuiState, key: TuiKey) -> TuiKey:
|
||||
"""A Miyoo's buttons as the keys they stand for, outside a text field:
|
||||
its A is Space, the button that chooses, so it acts as Enter but where
|
||||
it ticks a system or a core."""
|
||||
if key.name != "char":
|
||||
return key
|
||||
if key.text in _MIYOO_PAGES:
|
||||
return TuiKey(_MIYOO_PAGES[key.text])
|
||||
if key.text != " ":
|
||||
return key
|
||||
if state.screen in _CHECKLISTS:
|
||||
rows = tui_rows(state)
|
||||
row = rows[max(0, min(state.focus.get(state.screen, 0), len(rows) - 1))] \
|
||||
if rows else None
|
||||
if row is not None and row.action in ("system", "core"):
|
||||
return key
|
||||
return TuiKey("enter")
|
||||
|
||||
|
||||
# Rows Space leaves alone: it is X (tick) on PortMaster and Y on muxterm, a
|
||||
# key pressed for something else, and these end the run or write.
|
||||
_SPACE_INERT = ("install", "close", "quit")
|
||||
@@ -10891,6 +10952,55 @@ _SHORT_KEYS = (("A/Enter ", "A "), ("B/Esc ", "B "), ("X/Space ", "X "), ("Y/o "
|
||||
("Left/Right ", "<> "), ("Up/Down ", "^v "))
|
||||
|
||||
|
||||
# The key of a hint as each route names it, None for a key it lacks. The
|
||||
# bar is written in the "both" form ("A/Enter Install"), the route's own
|
||||
# names replace it: a PC user read "A/Enter", a muOS player was told a
|
||||
# Start+Select that muxterm does not have.
|
||||
_ROUTE_KEYS: dict[str, dict[str, str | None]] = {
|
||||
"keys": {"A/Enter": "Enter", "B/Esc": "Esc", "X/Space": "Space", "Y/o": "o"},
|
||||
# retrobios.gptk: A Enter, B Esc, X Space, Y Tab, L1 and R1 a page,
|
||||
# Start Enter, Select Esc, Start+Select the kill.
|
||||
"gptokeyb": {"A/Enter": "A", "B/Esc": "B", "X/Space": "X", "Y/o": "Y", "?": None,
|
||||
"/": None, "Ctrl-C": "Start+Select", "Enter": "A", "Esc": "B",
|
||||
"Ctrl-U": None},
|
||||
# RetroBIOS.sh.keys: the same buttons, and no combo that ends the run.
|
||||
"evmapy": {"A/Enter": "A", "B/Esc": "B", "X/Space": "X", "Y/o": "Y", "?": None,
|
||||
"/": None, "Ctrl-C": None, "Enter": "A", "Esc": "B", "Ctrl-U": None},
|
||||
# muxterm's own keys: Start Enter, B Backspace, Y Space, Menu closes the
|
||||
# terminal. A, X, L1 and R1 send nothing, Select shows its keyboard.
|
||||
"muxterm": {"A/Enter": "Start", "B/Esc": "B", "X/Space": "Y", "Y/o": None, "?": None,
|
||||
"/": None, "Ctrl-C": "Menu", "Enter": "Start", "Esc": None,
|
||||
"Ctrl-U": None},
|
||||
# Buttons that are keys already: A Space, Start Enter, L2 Tab, R2
|
||||
# Backspace. B is Left Ctrl, a modifier: alone it sends no byte to a
|
||||
# terminal (the console's keyboard driver only records its state), so
|
||||
# the interface never sees it, nor X, Y and Select (Shift, Alt, Ctrl).
|
||||
"miyoo": {"A/Enter": "A", "B/Esc": "R2", "X/Space": "A", "Y/o": "L2", "?": None,
|
||||
"/": None, "Ctrl-C": None, "Enter": "Start", "Esc": None, "Ctrl-U": None},
|
||||
}
|
||||
|
||||
|
||||
def _route_hint(state: TuiState, parts: tuple) -> tuple:
|
||||
"""The bar's entries named as the run's input names them."""
|
||||
names = _ROUTE_KEYS.get(state.pad)
|
||||
if names is None or state.help:
|
||||
return parts
|
||||
named = []
|
||||
for part in parts:
|
||||
key, sep, what = part.partition(" ")
|
||||
if key in names:
|
||||
# On a Miyoo A is Space, which ticks in a checklist: Start continues.
|
||||
mine = ("Start" if state.pad == "miyoo" and key == "A/Enter"
|
||||
and state.screen in _CHECKLISTS else names[key])
|
||||
if mine is None:
|
||||
continue
|
||||
key = mine
|
||||
named.append(key + sep + what)
|
||||
if state.pad == "muxterm" and state.screen == "path":
|
||||
named.append("Select Keyboard")
|
||||
return tuple(named)
|
||||
|
||||
|
||||
def _hint(width: int, *parts: str) -> str:
|
||||
"""The keys that matter now, short names first given up, then whole
|
||||
entries from the right."""
|
||||
@@ -11407,6 +11517,17 @@ def _bar(share: float, width: int) -> str:
|
||||
return f"[{'#' * filled}{'-' * (room - filled)}] {int(share * 100):3d}%"
|
||||
|
||||
|
||||
# What stops an install a second time, and the first time, per route.
|
||||
_STOP_KEYS = {
|
||||
"both": ("B or Ctrl-C", "B, Ctrl-C or Start+Select"),
|
||||
"keys": ("Esc or Ctrl-C", "Esc or Ctrl-C"),
|
||||
"gptokeyb": ("B", "B or Start+Select"),
|
||||
"evmapy": ("B", "B"),
|
||||
"muxterm": ("B", "B"),
|
||||
"miyoo": ("R2", "R2"),
|
||||
}
|
||||
|
||||
|
||||
def _view_installing(state: TuiState, width: int, height: int) -> tuple:
|
||||
lines: list = []
|
||||
if state.download is None:
|
||||
@@ -11429,10 +11550,10 @@ def _view_installing(state: TuiState, width: int, height: int) -> tuple:
|
||||
lines.append(f"{files_done} of {_plural(state.download['files'], 'file')}")
|
||||
if files_done >= state.download["files"] and state.summary is None:
|
||||
lines.append("Finishing...")
|
||||
lines.append(_styled("Stopping, finishing the files in flight. Press B or Ctrl-C "
|
||||
stops = _STOP_KEYS.get(state.pad, _STOP_KEYS["both"])
|
||||
lines.append(_styled(f"Stopping, finishing the files in flight. Press {stops[0]} "
|
||||
"again to stop now." if state.stopping
|
||||
else "B, Ctrl-C or Start+Select stops. What is done stays done.",
|
||||
"dim"))
|
||||
else f"{stops[1]} stops. What is done stays done.", "dim"))
|
||||
room = height - len(lines) - 1
|
||||
failed = [item for item in reversed(state.finished) if not item[1]]
|
||||
good = [item for item in reversed(state.finished) if item[1]]
|
||||
@@ -11637,21 +11758,68 @@ _TUI_VIEWS: dict[str, Callable[[TuiState, int, int], tuple]] = {
|
||||
}
|
||||
|
||||
|
||||
# The help's names for each action, per route: the buttons first, then the
|
||||
# keys a keyboard plugged in also has. None for an action the route lacks.
|
||||
_HELP_KEYS: dict[str, dict[str, str | None]] = {
|
||||
"both": {"move": "Up/Down, k/j", "page": "PgUp/PgDn, L1/R1", "ok": "A/Enter, Start",
|
||||
"tick": "X, Right/Left", "options": "Y, o, Tab", "back": "B/Esc, Backspace",
|
||||
"stop": "B/Esc, Ctrl-C", "kill": "Start+Select", "enter": "Enter",
|
||||
"esc": "Esc"},
|
||||
"keys": {"move": "Up/Down, k/j", "page": "PgUp/PgDn", "ok": "Enter",
|
||||
"tick": "Space, Right/Left", "options": "o, Tab", "back": "Esc, Backspace",
|
||||
"stop": "Esc, Ctrl-C", "kill": None, "enter": "Enter", "esc": "Esc"},
|
||||
"gptokeyb": {"move": "D-pad, Up/Down", "page": "L1/R1, PgUp/PgDn",
|
||||
"ok": "A, Start, Enter", "tick": "X, Space, Right/Left",
|
||||
"options": "Y, Tab, o", "back": "B, Select, Esc", "stop": "B, Esc, Ctrl-C",
|
||||
"kill": "Select, then Start", "enter": "A, Enter", "esc": "B, Esc"},
|
||||
"evmapy": {"move": "D-pad, Up/Down", "page": "L1/R1, PgUp/PgDn", "ok": "A, Start, Enter",
|
||||
"tick": "X, Space, Right/Left", "options": "Y, Tab, o",
|
||||
"back": "B, Select, Esc", "stop": "B, Esc, Ctrl-C", "kill": None,
|
||||
"enter": "A, Enter", "esc": "B, Esc"},
|
||||
"muxterm": {"move": "D-pad, Up/Down", "page": "PgUp/PgDn", "ok": "Start, Enter",
|
||||
"tick": "Y, Space, Right/Left", "options": "o, Tab",
|
||||
"back": "B, Esc, Backspace", "stop": "B, Esc, Ctrl-C", "kill": None,
|
||||
"enter": "Start, Enter", "esc": "Esc"},
|
||||
"miyoo": {"move": "D-pad, Up/Down", "page": "L1/R1, PgUp/PgDn", "ok": "A, Start, Enter",
|
||||
"tick": "A, Space, Right/Left", "options": "L2, Tab, o",
|
||||
"back": "R2, Esc, Backspace", "stop": "R2, Esc, Ctrl-C", "kill": None,
|
||||
"enter": "Start, Enter", "esc": "Esc"},
|
||||
}
|
||||
|
||||
|
||||
def _route_extras(state: TuiState) -> list[tuple[str, str]]:
|
||||
"""What a route's own buttons do that no key of the table says."""
|
||||
if state.pad == "muxterm":
|
||||
return [("Select", "Keyboard on or off"), ("Menu", "Close the terminal")]
|
||||
if state.pad == "miyoo":
|
||||
return [("B", "Sends no key here")]
|
||||
return []
|
||||
|
||||
|
||||
def _help_lines(state: TuiState) -> list[tuple[str, str]]:
|
||||
screen = state.screen
|
||||
names = _HELP_KEYS.get(state.pad, _HELP_KEYS["both"])
|
||||
# gptokeyb's kill: Start pressed first also chooses the row in focus.
|
||||
kill = ([(names["kill"], "Quit at once")]
|
||||
if state.pad == "gptokeyb" else [])
|
||||
if screen == "installing":
|
||||
return [("B/Esc, Ctrl-C", "Stop: what is done stays done"),
|
||||
("Start+Select", "Stop"), ("?, F1", "This help")]
|
||||
return [(names["stop"], "Stop: what is done stays done"),
|
||||
*([(names["kill"], "Stop")] if names["kill"] else []),
|
||||
*_route_extras(state), ("?, F1", "This help")]
|
||||
if screen == "path":
|
||||
return [("Enter", "Use this folder"), ("Backspace", "Delete a character"),
|
||||
("Ctrl-U", "Clear"), ("Esc", "Back"), ("Ctrl-C", "Quit")]
|
||||
keys = [("Up/Down, k/j", "Move")]
|
||||
return [(names["enter"], "Use this folder"), ("Backspace", "Delete a character"),
|
||||
("Ctrl-U", "Clear"), (names["esc"], "Back"), ("Ctrl-C", "Quit"),
|
||||
*_route_extras(state)]
|
||||
keys = [(names["move"], "Move")]
|
||||
if len(tui_rows(state)) > 9 or screen in _PAGERS:
|
||||
keys += [("PgUp/PgDn, L1/R1", "Page"), ("Home/End, g/G", "First, last")]
|
||||
keys += [(names["page"], "Page"), ("Home/End, g/G", "First, last")]
|
||||
primary = _primary(state) or _TUI_VIEWS[screen](state, 80, 24)[2][0]
|
||||
keys.append(("A/Enter, Start", primary.split(" ", 1)[-1]))
|
||||
ok = names["ok"]
|
||||
if state.pad == "miyoo" and screen in _CHECKLISTS:
|
||||
ok = names["enter"]
|
||||
keys.append((ok, primary.split(" ", 1)[-1]))
|
||||
if screen in _CHECKLISTS:
|
||||
keys.append(("X, Right/Left", "Tick, untick"))
|
||||
keys.append((names["tick"], "Tick, untick"))
|
||||
if screen == "systems":
|
||||
keys.append(("a, n", "All, none"))
|
||||
if screen in ("platforms", "systems", "cores"):
|
||||
@@ -11661,9 +11829,10 @@ def _help_lines(state: TuiState) -> list[tuple[str, str]]:
|
||||
if screen == "options":
|
||||
keys.append(("Left/Right", "Change a value"))
|
||||
if screen in ("plan", "systems", "confirm"):
|
||||
keys.append(("Y, o, Tab", "Options"))
|
||||
keys.append((names["options"], "Options"))
|
||||
back = "Quit" if len(state.stack) == 1 else "Back"
|
||||
keys += [("B/Esc, Backspace", back), ("q, Ctrl-C", "Quit"), ("?, F1", "This help")]
|
||||
keys += [(names["back"], back), ("q, Ctrl-C", "Quit"), *kill, *_route_extras(state),
|
||||
("?, F1", "This help")]
|
||||
return keys
|
||||
|
||||
|
||||
@@ -11705,6 +11874,7 @@ def tui_view(state: TuiState, cols: int, rows: int) -> list[str]:
|
||||
primary = _primary(state) if not state.help and hint[0].startswith("A/Enter") else None
|
||||
if primary:
|
||||
hint = (primary, *hint[1:])
|
||||
hint = _route_hint(state, hint)
|
||||
lines = [_styled(fit(f" RetroBIOS: {title}", cols), "title"), _styled("")]
|
||||
lines += [_styled(fit(" " + line, cols), line.style) for line in body[:height]]
|
||||
lines += [_styled("")] * (rows - 1 - len(lines))
|
||||
@@ -11771,6 +11941,7 @@ def tui_start(
|
||||
) -> tuple[TuiState, list[tuple]]:
|
||||
"""The first screen, from what is found here and what the command named."""
|
||||
state = TuiState(os_type=os_type, home=Path.home())
|
||||
state.pad = tui_pad_route(os_type)
|
||||
state.cli_systems = list(args.system or [])
|
||||
state.regions = resolve_regions(args.region) if args.region else []
|
||||
state.cli_regions = list(state.regions)
|
||||
@@ -11986,6 +12157,7 @@ class _TuiTerminal:
|
||||
self.ended = False
|
||||
self.heard = 0.0
|
||||
self.frame: list[tuple[str, str]] = []
|
||||
self.entered = False
|
||||
|
||||
def enter(self) -> None:
|
||||
if os.name != "nt":
|
||||
@@ -12002,13 +12174,18 @@ class _TuiTerminal:
|
||||
mode[6][termios.VMIN] = 1
|
||||
mode[6][termios.VTIME] = 0
|
||||
termios.tcsetattr(self.fd, termios.TCSANOW, mode)
|
||||
self.entered = True
|
||||
self.write(_SCREEN_ON)
|
||||
|
||||
def leave(self) -> None:
|
||||
"""The main screen and the mode found, the mode even when the write
|
||||
fails: a closed window answers EIO."""
|
||||
try:
|
||||
self.write(_SCREEN_OFF)
|
||||
# Never entered (stopped while it started): the main screen and
|
||||
# the cursor are as they were, nothing is written over them.
|
||||
if getattr(self, "entered", True):
|
||||
self.entered = False
|
||||
self.write(_SCREEN_OFF)
|
||||
finally:
|
||||
if self.saved is not None:
|
||||
import termios # noqa: PLC0415
|
||||
@@ -12173,24 +12350,31 @@ def _farewell_lines(state: TuiState) -> list[str]:
|
||||
|
||||
def run_tui(args: argparse.Namespace) -> int:
|
||||
"""The full-screen interface, until it closes: the exit code it ends with."""
|
||||
os_type = _INVOCATION.os_type or detect_os()
|
||||
if os_type == "wsl":
|
||||
_enter_wsl()
|
||||
# The --from folders are read by each check, not here: indexing a large
|
||||
# card held the first frame back. Only whether each can be read is asked.
|
||||
folders = getattr(args, "from") or []
|
||||
setattr(args, "from", None)
|
||||
flags = {"interrupt": False, "resized": False, "suspend": False, "resume": False,
|
||||
"term": 0}
|
||||
handlers: dict[int, object] = {}
|
||||
|
||||
def on_stop(signum: int, _frame: object) -> None:
|
||||
flags["term"] = flags["term"] or signum
|
||||
|
||||
# Before the start, whose detection and pack opening take seconds on a
|
||||
# handheld: a Start+Select then ended on the default action, without a
|
||||
# word or the terminal put back.
|
||||
for name in ("SIGTERM", "SIGHUP"):
|
||||
number = getattr(signal, name, None)
|
||||
if number is not None:
|
||||
handlers[number] = signal.signal(number, on_stop)
|
||||
try:
|
||||
_open_local_sources(args)
|
||||
finally:
|
||||
setattr(args, "from", folders)
|
||||
for folder in folders:
|
||||
problem = from_folder_problem(str(folder))
|
||||
if problem is not None:
|
||||
print(f"Error: {folder} {problem[1]}: --from names the folder to copy BIOS "
|
||||
"files from.", file=sys.stderr)
|
||||
return 1
|
||||
state, effects = tui_start(args, os_type)
|
||||
started = _tui_startup(args)
|
||||
except BaseException:
|
||||
_restore_handlers(handlers)
|
||||
raise
|
||||
if started is None:
|
||||
_restore_handlers(handlers)
|
||||
return 1
|
||||
state, effects = started
|
||||
if flags["term"]:
|
||||
state.exit_code = 128 + flags["term"]
|
||||
work = Path(tempfile.mkdtemp(prefix=TUI_WORK_PREFIX))
|
||||
(work / TUI_OWNER).write_text(str(os.getpid()), encoding="utf-8")
|
||||
_prune_work_folders(work.parent, time.time())
|
||||
@@ -12198,8 +12382,6 @@ def run_tui(args: argparse.Namespace) -> int:
|
||||
inbox: queue.Queue = queue.Queue()
|
||||
current: list[_EngineRun] = []
|
||||
written: dict[str, Path] = {}
|
||||
flags = {"interrupt": False, "resized": False, "suspend": False, "resume": False,
|
||||
"term": 0}
|
||||
# Start+Select on a handheld is gptokeyb's pkill: SIGTERM, then SIGKILL
|
||||
# 3 s later. The install gets 2 s to say what it wrote.
|
||||
terminating = {"signal": 0, "deadline": 0.0}
|
||||
@@ -12207,7 +12389,6 @@ def run_tui(args: argparse.Namespace) -> int:
|
||||
terminal = _TuiTerminal(sys.stdin, sys.stdout)
|
||||
real_out, real_err = sys.stdout, sys.stderr
|
||||
buffer = io.StringIO()
|
||||
handlers: dict[int, object] = {}
|
||||
|
||||
def perform(todo: list[tuple], from_key: bool = True) -> None:
|
||||
for effect in todo:
|
||||
@@ -12251,9 +12432,6 @@ def run_tui(args: argparse.Namespace) -> int:
|
||||
def on_resize(_signum: int, _frame: object) -> None:
|
||||
flags["resized"] = True
|
||||
|
||||
def on_stop(signum: int, _frame: object) -> None:
|
||||
flags["term"] = flags["term"] or signum
|
||||
|
||||
def on_suspend(_signum: int, _frame: object) -> None:
|
||||
flags["suspend"] = True
|
||||
|
||||
@@ -12262,14 +12440,15 @@ def run_tui(args: argparse.Namespace) -> int:
|
||||
|
||||
try:
|
||||
for name, handler in (("SIGINT", on_interrupt), ("SIGWINCH", on_resize),
|
||||
("SIGTERM", on_stop), ("SIGHUP", on_stop),
|
||||
("SIGTSTP", on_suspend), ("SIGCONT", on_resume)):
|
||||
number = getattr(signal, name, None)
|
||||
if number is not None:
|
||||
handlers[number] = signal.signal(number, handler)
|
||||
sys.stdout = sys.stderr = buffer
|
||||
terminal.enter()
|
||||
perform(effects)
|
||||
if state.exit_code is None:
|
||||
# Stopped while it started: the screen never opens.
|
||||
terminal.enter()
|
||||
perform(effects)
|
||||
while state.exit_code is None:
|
||||
if flags["suspend"]:
|
||||
# Stopped from outside (kill -TSTP): the shell gets its
|
||||
@@ -12349,12 +12528,64 @@ def run_tui(args: argparse.Namespace) -> int:
|
||||
try:
|
||||
for line in lines:
|
||||
print(line, file=stream)
|
||||
stream.flush()
|
||||
except OSError:
|
||||
# The terminal is gone: the exit code is all that still reaches anyone.
|
||||
return state.exit_code if state.exit_code is not None else 1
|
||||
# The terminal is gone (a closed window answers EIO, a closed pipe
|
||||
# EPIPE): the exit code is all that still reaches anyone, and the
|
||||
# flush at exit would replace it with 120.
|
||||
_drop_closed_terminal()
|
||||
return state.exit_code if state.exit_code is not None else 1
|
||||
|
||||
|
||||
def _tui_startup(args: argparse.Namespace) -> tuple[TuiState, list[tuple]] | None:
|
||||
"""The local sources opened and the first screen chosen, None when a
|
||||
--from folder cannot be read (said on stderr)."""
|
||||
os_type = _INVOCATION.os_type or detect_os()
|
||||
if os_type == "wsl":
|
||||
_enter_wsl()
|
||||
# The --from folders are read by each check, not here: indexing a large
|
||||
# card held the first frame back. Only whether each can be read is asked.
|
||||
folders = getattr(args, "from") or []
|
||||
setattr(args, "from", None)
|
||||
try:
|
||||
_open_local_sources(args)
|
||||
finally:
|
||||
setattr(args, "from", folders)
|
||||
for folder in folders:
|
||||
problem = from_folder_problem(str(folder))
|
||||
if problem is not None:
|
||||
print(f"Error: {folder} {problem[1]}: --from names the folder to copy BIOS "
|
||||
"files from.", file=sys.stderr)
|
||||
return None
|
||||
return tui_start(args, os_type)
|
||||
|
||||
|
||||
def _restore_handlers(handlers: dict) -> None:
|
||||
for number, previous in handlers.items():
|
||||
signal.signal(number, previous)
|
||||
|
||||
|
||||
def _drop_closed_terminal() -> None:
|
||||
"""Point stdout and stderr at nothing once the terminal is gone, so what
|
||||
they still hold is flushed somewhere at exit."""
|
||||
try:
|
||||
null = os.open(os.devnull, os.O_WRONLY)
|
||||
except OSError:
|
||||
# No null device to point them at: the flush at exit fails as before.
|
||||
return
|
||||
try:
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
fd = stream.fileno()
|
||||
except (ValueError, AttributeError, io.UnsupportedOperation):
|
||||
# A stream with no descriptor (a test's StringIO) has no
|
||||
# terminal behind it to lose.
|
||||
continue
|
||||
os.dup2(null, fd)
|
||||
finally:
|
||||
os.close(null)
|
||||
|
||||
|
||||
def _tui_cleanup(
|
||||
terminal: _TuiTerminal, runs: list[_EngineRun], handlers: dict, state: TuiState,
|
||||
) -> list[str]:
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ esac
|
||||
TEMP_INSTALLER=""
|
||||
TEMP_DIRECTORY=""
|
||||
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
DEFAULT_INSTALL_SHA256="ea2777500114b796bb4a23ea9e0dd0a15bc4499bb1289e6e426943beece48865"
|
||||
DEFAULT_INSTALL_SHA256="e2cc5f073a8226fe26b0513ebd25f00de6f3f613d8c9851473c2d30b53356f48"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
Vendored
+1188
File diff suppressed because it is too large.
Load diff
+1188
File diff suppressed because it is too large.
Load diff
Vendored
+1134
File diff suppressed because it is too large.
Load diff
Vendored
+1188
File diff suppressed because it is too large.
Load diff
Vendored
+1188
File diff suppressed because it is too large.
Load diff
+289
-5
@@ -56,6 +56,8 @@ except ImportError:
|
||||
|
||||
GOLDEN = REPO_ROOT / "tests" / "fixtures" / "tui"
|
||||
SIZES = ((40, 12), (40, 15), (80, 24), (120, 40))
|
||||
# RETROBIOS_PAD's values, and "keys" for a desktop that sets none.
|
||||
ROUTES = ("keys", "gptokeyb", "evmapy", "muxterm", "miyoo")
|
||||
UP, DOWN, RIGHT, LEFT = "\x1b[A", "\x1b[B", "\x1b[C", "\x1b[D"
|
||||
Key = install.TuiKey
|
||||
|
||||
@@ -940,6 +942,31 @@ class Views(unittest.TestCase):
|
||||
path.write_text(text, encoding="utf-8")
|
||||
self.assertEqual(text, path.read_text(encoding="utf-8"))
|
||||
|
||||
def test_goldens_per_route(self):
|
||||
"""Each input route at the handheld sizes and 80x24, kept only where a
|
||||
screen reads differently from the both-names form."""
|
||||
update = os.environ.get("RETROBIOS_UPDATE_GOLDENS") == "1"
|
||||
for route in ROUTES:
|
||||
sections = []
|
||||
for name, state in golden_states().items():
|
||||
state.pad = route
|
||||
for cols, rows in SIZES[:3]:
|
||||
with self.subTest(route=route, screen=name, size=f"{cols}x{rows}"):
|
||||
state.cols, state.rows = cols, rows
|
||||
lines = install.tui_view(state, cols, rows)
|
||||
for line in lines:
|
||||
self.assertLessEqual(install.cell_width(line), cols, line)
|
||||
self.assertLess(install.cell_width(lines[-1]), cols)
|
||||
text = "\n".join(line.rstrip() for line in lines) + "\n"
|
||||
both = (GOLDEN / f"{name}-{cols}x{rows}.txt").read_text(encoding="utf-8")
|
||||
if text != both:
|
||||
sections.append(f"=== {name} {cols}x{rows}\n{text}")
|
||||
path = GOLDEN / f"route-{route}.txt"
|
||||
if update:
|
||||
path.write_text("".join(sections), encoding="utf-8")
|
||||
with self.subTest(route=route):
|
||||
self.assertEqual("".join(sections), path.read_text(encoding="utf-8"))
|
||||
|
||||
def test_the_hint_bar_names_the_primary_action_first(self):
|
||||
for name, state in golden_states().items():
|
||||
with self.subTest(screen=name):
|
||||
@@ -954,6 +981,163 @@ class Views(unittest.TestCase):
|
||||
self.assertTrue(focused[0].lstrip().startswith("> RetroArch"))
|
||||
|
||||
|
||||
def _bar_keys(line: str) -> set:
|
||||
"""The key each entry of a hint bar names: its first word."""
|
||||
return {part.split(" ", 1)[0] for part in line.strip().split(" ") if part}
|
||||
|
||||
|
||||
# The keys each route has, as the bar may name them. "Any" opens "Any key
|
||||
# closes this help", <> and ^v are the narrow forms of Left/Right, Up/Down.
|
||||
ROUTE_KEYS = {
|
||||
"keys": {"Enter", "Esc", "Space", "o", "?", "/", "Ctrl-C", "Ctrl-U"},
|
||||
"gptokeyb": {"A", "B", "X", "Y", "Start+Select"},
|
||||
"evmapy": {"A", "B", "X", "Y"},
|
||||
"muxterm": {"Start", "B", "Y", "Menu", "Select"},
|
||||
"miyoo": {"A", "Start", "R2", "L2"},
|
||||
}
|
||||
EVERY_ROUTE_HAS = {"Left/Right", "Up/Down", "<>", "^v", "Any"}
|
||||
|
||||
|
||||
class PadRoutes(unittest.TestCase):
|
||||
"""The bar names the buttons the launcher maps, and a desktop's keys."""
|
||||
|
||||
def test_the_route_from_the_environment(self):
|
||||
route = install.tui_pad_route
|
||||
for value in install.TUI_PAD_ROUTES:
|
||||
self.assertEqual(route("linux", {"RETROBIOS_PAD": value, "DISPLAY": ":0"}), value)
|
||||
self.assertEqual(route("linux", {"RETROBIOS_PAD": " MiYoo "}), "miyoo")
|
||||
self.assertEqual(route("linux", {"RETROBIOS_PAD": "dualshock"}), "both")
|
||||
for os_type in ("windows", "darwin", "wsl"):
|
||||
self.assertEqual(route(os_type, {}), "keys", os_type)
|
||||
for env in ({"DISPLAY": ":0", "TERM": "xterm-256color"}, {"WAYLAND_DISPLAY": "w-0"},
|
||||
{"SSH_CONNECTION": "10.0.0.2 5 10.0.0.3 22"}, {"SSH_TTY": "/dev/pts/1"}):
|
||||
self.assertEqual(route("linux", env), "keys", env)
|
||||
# SteamOS game mode and anything Steam starts: Steam Input may be
|
||||
# all there is. A console with nothing named, or Android: unknown.
|
||||
for os_type, env in (("linux", {"SteamGamepadUI": "1", "DISPLAY": ":0"}),
|
||||
("linux", {"GAMESCOPE_WAYLAND_DISPLAY": "gamescope-0",
|
||||
"WAYLAND_DISPLAY": "w-0"}),
|
||||
("windows", {"SteamGameId": "123"}),
|
||||
("linux", {"SteamDeck": "1", "DISPLAY": ":0"}),
|
||||
("linux", {"TERM": "linux", "SSH_TTY": "/dev/pts/0"}),
|
||||
("linux", {"TERM": "linux"}), ("linux", {}),
|
||||
("android", {"DISPLAY": ":0"})):
|
||||
self.assertEqual(route(os_type, env), "both", env)
|
||||
|
||||
def test_the_launchers_values_are_the_routes(self):
|
||||
tui = (REPO_ROOT / "ports" / "shared" / "tui.txt").read_text(encoding="utf-8")
|
||||
named = set(re.findall(r'RB_PAD="(\w+)"', tui))
|
||||
for launcher in REPO_ROOT.glob("ports/*/*.sh"):
|
||||
named |= set(re.findall(r'RB_PAD="(\w+)"', launcher.read_text(encoding="utf-8")))
|
||||
self.assertLessEqual(named, set(install.TUI_PAD_ROUTES))
|
||||
self.assertEqual(set(ROUTES) - {"keys"}, set(install.TUI_PAD_ROUTES))
|
||||
|
||||
def test_the_plan_bar_of_each_route(self):
|
||||
expected = {
|
||||
"both": "A/Enter Install B/Esc Back Y/o Options ? Help",
|
||||
"keys": "Enter Install Esc Back o Options ? Help",
|
||||
"gptokeyb": "A Install B Back Y Options",
|
||||
"evmapy": "A Install B Back Y Options",
|
||||
"muxterm": "Start Install B Back",
|
||||
"miyoo": "A Install R2 Back L2 Options",
|
||||
}
|
||||
for route, bar in expected.items():
|
||||
with self.subTest(route=route):
|
||||
state = checked_state()
|
||||
state.pad = route
|
||||
self.assertEqual(install.tui_view(state, 80, 24)[-1].strip(), bar)
|
||||
|
||||
def test_no_bar_names_a_key_its_route_lacks(self):
|
||||
for route, have in ROUTE_KEYS.items():
|
||||
for name, state in golden_states().items():
|
||||
state.pad = route
|
||||
for cols, rows in SIZES:
|
||||
with self.subTest(route=route, screen=name, size=f"{cols}x{rows}"):
|
||||
state.cols, state.rows = cols, rows
|
||||
bar = install.tui_view(state, cols, rows)[-1]
|
||||
self.assertLessEqual(_bar_keys(bar) - EVERY_ROUTE_HAS, have, bar)
|
||||
|
||||
def test_the_install_says_what_stops_it_there(self):
|
||||
expected = {
|
||||
"both": "B, Ctrl-C or Start+Select stops.", "keys": "Esc or Ctrl-C stops.",
|
||||
"gptokeyb": "B or Start+Select stops.", "evmapy": "B stops.",
|
||||
"muxterm": "B stops.", "miyoo": "R2 stops.",
|
||||
}
|
||||
for route, words in expected.items():
|
||||
with self.subTest(route=route):
|
||||
state = golden_states()["installing"]
|
||||
state.pad = route
|
||||
text = "\n".join(install.tui_view(state, 80, 24))
|
||||
self.assertIn(f"{words} What is done stays done.", text)
|
||||
state.stopping = True
|
||||
again = {"both": "B or Ctrl-C", "keys": "Esc or Ctrl-C", "miyoo": "R2"}
|
||||
self.assertIn(f"Press {again.get(route, 'B')} again to stop now.",
|
||||
" ".join(" ".join(install.tui_view(state, 80, 24)).split()))
|
||||
|
||||
def test_the_help_lists_the_buttons_and_the_keys(self):
|
||||
def help_text(route: str, screen: str = "plan") -> str:
|
||||
state = golden_states()[screen]
|
||||
state.pad, state.help = route, True
|
||||
return "\n".join(line.rstrip() for line in install.tui_view(state, 80, 24))
|
||||
|
||||
gptk = help_text("gptokeyb")
|
||||
for words in ("D-pad, Up/Down", "A, Start, Enter", "B, Select, Esc", "Y, Tab, o",
|
||||
"Select, then Start", "Quit at once", "q, Ctrl-C"):
|
||||
self.assertIn(words, gptk)
|
||||
self.assertNotIn("Start+Select", help_text("evmapy"))
|
||||
keys = help_text("keys")
|
||||
for words in ("Enter", "Esc, Backspace", "o, Tab"):
|
||||
self.assertIn(words, keys)
|
||||
for button in ("A/Enter", "B/Esc", "L1", "Start", "Y,"):
|
||||
self.assertNotIn(button, keys)
|
||||
muxterm = help_text("muxterm")
|
||||
for words in ("Start, Enter", "B, Esc, Backspace", "Keyboard on or off",
|
||||
"Close the terminal"):
|
||||
self.assertIn(words, muxterm)
|
||||
self.assertNotIn("Start+Select", muxterm)
|
||||
miyoo = help_text("miyoo", "systems")
|
||||
for words in ("Start, Enter", "A, Space, Right/Left", "R2, Esc, Backspace",
|
||||
"L2, Tab, o", "Sends no key here"):
|
||||
self.assertIn(words, miyoo)
|
||||
# The both-names form is the one it was.
|
||||
self.assertEqual(help_text("both"), (GOLDEN / "help-80x24.txt").read_text().rstrip("\n"))
|
||||
|
||||
def test_a_miyoo_a_chooses_and_ticks(self):
|
||||
"""A is Space on a Miyoo: it chooses where Enter does, ticks where X
|
||||
would, and L1 and R1 (E and T) page."""
|
||||
state = checked_state()
|
||||
state.pad = "miyoo"
|
||||
self.assertEqual(press(state, " "), [("run", "install")])
|
||||
state = checked_state()
|
||||
state.pad = "miyoo"
|
||||
state.stack.append("systems")
|
||||
rows = install.tui_rows(state)
|
||||
first = next(i for i, row in enumerate(rows) if row.action == "system")
|
||||
state.focus["systems"] = first
|
||||
before = set(state.ticked)
|
||||
press(state, " ")
|
||||
self.assertNotEqual(state.ticked, before)
|
||||
state.focus["systems"] = next(i for i, row in enumerate(rows) if row.action == "all")
|
||||
press(state, " ")
|
||||
self.assertEqual(state.ticked, {row["system"] for row in state.systems})
|
||||
state.focus["systems"] = first
|
||||
press(state, "t")
|
||||
self.assertGreater(state.focus["systems"], first)
|
||||
press(state, "e")
|
||||
self.assertEqual(state.focus["systems"], 0)
|
||||
# In a text field the letters are letters.
|
||||
state.editing = "systems"
|
||||
press(state, "t")
|
||||
self.assertEqual(state.filters["systems"], "t")
|
||||
|
||||
def test_space_stays_x_on_the_other_routes(self):
|
||||
for route in ("both", "keys", "gptokeyb", "evmapy", "muxterm"):
|
||||
with self.subTest(route=route):
|
||||
state = checked_state()
|
||||
state.pad = route
|
||||
self.assertEqual(press(state, " "), [("run", "confirm")])
|
||||
|
||||
|
||||
class Rendering(unittest.TestCase):
|
||||
def test_only_changed_lines_are_written(self):
|
||||
out = io.StringIO()
|
||||
@@ -1234,8 +1418,13 @@ class CleanupSurvivesABrokenTerminal(unittest.TestCase):
|
||||
self.addCleanup(os.close, slave)
|
||||
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0))
|
||||
before = termios.tcgetattr(slave)
|
||||
handlers = {n: signal.getsignal(n) for n in (signal.SIGINT, signal.SIGTERM)}
|
||||
tty = self.Tty(slave)
|
||||
handlers = {n: signal.getsignal(n) for n in (signal.SIGINT, signal.SIGTERM,
|
||||
signal.SIGHUP)}
|
||||
# Keys come in on the terminal itself, the screen goes out on a
|
||||
# descriptor of its own, as fd 0 and fd 1 are.
|
||||
tty, screen_fd = self.Tty(slave), os.dup(slave)
|
||||
self.addCleanup(os.close, screen_fd)
|
||||
out = self.Tty(screen_fd)
|
||||
real_draw = install._TuiTerminal.draw
|
||||
|
||||
def breaking_draw(terminal, lines, cols):
|
||||
@@ -1251,11 +1440,14 @@ class CleanupSurvivesABrokenTerminal(unittest.TestCase):
|
||||
mock.patch.object(install, "tui_start", return_value=(fresh_state(), [])), \
|
||||
mock.patch.object(install, "_open_local_sources"), \
|
||||
mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
mock.patch.object(sys, "stdin", tty), mock.patch.object(sys, "stdout", tty), \
|
||||
mock.patch.object(sys, "stderr", tty):
|
||||
mock.patch.object(sys, "stdin", tty), mock.patch.object(sys, "stdout", out), \
|
||||
mock.patch.object(sys, "stderr", out):
|
||||
code = install.run_tui(_args())
|
||||
self.assertIs(sys.stdout, tty)
|
||||
self.assertIs(sys.stdout, out)
|
||||
self.assertEqual(code, 1)
|
||||
# The farewell found no terminal: what still flushes at exit goes to
|
||||
# the null device, and the exit code stays the one returned.
|
||||
self.assertEqual(os.fstat(screen_fd).st_rdev, os.stat(os.devnull).st_rdev)
|
||||
# The crash keeps its log, marked kept, and nothing else of the run.
|
||||
self.assertEqual(sorted(path.name for path in work.iterdir()),
|
||||
["error.log", "kept", "owner"])
|
||||
@@ -1608,6 +1800,50 @@ class WorkFolders(unittest.TestCase):
|
||||
work, out = self.run_to(130)
|
||||
self.assertFalse(work.exists())
|
||||
|
||||
def test_a_stop_while_it_starts_ends_on_the_farewell(self):
|
||||
"""Start+Select during the detection: the signal is the interface's,
|
||||
never the default action that ended Python without a word."""
|
||||
import signal
|
||||
|
||||
for number in (signal.SIGTERM, signal.SIGHUP):
|
||||
with self.subTest(signal=number):
|
||||
heard = []
|
||||
|
||||
def recorder(signum, _frame, heard=heard):
|
||||
heard.append(signum)
|
||||
|
||||
previous = signal.signal(number, recorder)
|
||||
self.addCleanup(signal.signal, number, previous)
|
||||
interrupt = signal.getsignal(signal.SIGINT)
|
||||
|
||||
def starting(*_args, **_kwargs):
|
||||
os.kill(os.getpid(), number)
|
||||
return fresh_state(), [("fetch", "retroarch")]
|
||||
|
||||
entered = []
|
||||
WorkFolders.Terminal.state_to = lambda _t: None
|
||||
out = io.StringIO()
|
||||
work = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, work, True)
|
||||
with mock.patch.object(install, "_TuiTerminal", self.Terminal), \
|
||||
mock.patch.object(install.tempfile, "mkdtemp", return_value=str(work)), \
|
||||
mock.patch.object(self.Terminal, "enter",
|
||||
lambda _t: entered.append(True)), \
|
||||
mock.patch.object(install, "tui_start", side_effect=starting), \
|
||||
mock.patch.object(install, "_open_local_sources"), \
|
||||
mock.patch.object(install, "_tui_fetch") as fetch, \
|
||||
mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
mock.patch.object(sys, "stdout", out), \
|
||||
mock.patch.object(sys, "stderr", out):
|
||||
code = install.run_tui(_args())
|
||||
self.assertEqual(code, 128 + number)
|
||||
self.assertEqual(heard, [])
|
||||
self.assertEqual(entered, [])
|
||||
fetch.assert_not_called()
|
||||
self.assertEqual(out.getvalue(), "RetroBIOS: stopped. Nothing was installed.\n")
|
||||
self.assertIs(signal.getsignal(number), recorder)
|
||||
self.assertIs(signal.getsignal(signal.SIGINT), interrupt)
|
||||
|
||||
|
||||
def found(dest: str, folder: str, reason: str = "") -> dict:
|
||||
return {"event": "found", "platform": "retroarch", "dest": dest, "folder": folder,
|
||||
@@ -2402,6 +2638,11 @@ class EndToEnd(unittest.TestCase):
|
||||
self.env = {**repo.env(), "HOME": str(home), "TERM": "xterm",
|
||||
"RETROBIOS_OS": "linux", "XDG_CONFIG_HOME": str(home / ".config")}
|
||||
self.env.pop("NO_COLOR", None)
|
||||
# Whatever the shell running the tests has, the input is not known:
|
||||
# the bar names both the button and the key. Each test names its own.
|
||||
for name in ("DISPLAY", "WAYLAND_DISPLAY", "SSH_CONNECTION", "SSH_TTY",
|
||||
install.TUI_PAD_ENV, *install._STEAM_MARKERS):
|
||||
self.env.pop(name, None)
|
||||
return repo
|
||||
|
||||
def start(self, *args: str, cols: int = 80, rows: int = 24) -> Pty:
|
||||
@@ -2592,6 +2833,49 @@ class EndToEnd(unittest.TestCase):
|
||||
term.send("q")
|
||||
self.assertEqual(term.close(), 130)
|
||||
|
||||
def test_closing_the_window_ends_with_its_signal(self):
|
||||
"""The terminal gone, the farewell has nowhere to go: the code is
|
||||
129 (SIGHUP), not 120 from the flush failing at exit."""
|
||||
self.repo()
|
||||
for screen_name, keys_first in (("Where do you play?", ()),
|
||||
("files to download", ("\r",))):
|
||||
with self.subTest(screen=screen_name):
|
||||
term = self.start()
|
||||
term.wait("Where do you play?")
|
||||
term.send(*keys_first)
|
||||
term.wait(screen_name)
|
||||
os.close(term.fd)
|
||||
end = time.monotonic() + 20
|
||||
status = None
|
||||
while time.monotonic() < end:
|
||||
pid, status = os.waitpid(term.pid, os.WNOHANG)
|
||||
if pid:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
os.kill(term.pid, 9)
|
||||
self.fail("still running once its window closed")
|
||||
term.status = os.WEXITSTATUS(status)
|
||||
self.assertTrue(os.WIFEXITED(status))
|
||||
self.assertEqual(term.status, 129)
|
||||
|
||||
def test_a_desktop_names_its_keys_and_a_pad_its_buttons(self):
|
||||
self.repo()
|
||||
for env, bar in (({"DISPLAY": ":0"}, "Enter Continue Esc Quit ? Help"),
|
||||
({"RETROBIOS_PAD": "gptokeyb", "DISPLAY": ":0"},
|
||||
"A Continue B Quit"),
|
||||
({"RETROBIOS_PAD": "muxterm"}, "Start Continue B Quit")):
|
||||
with self.subTest(env=env):
|
||||
self.env.update(env)
|
||||
term = self.start(cols=40, rows=12)
|
||||
term.wait("Where do you play?")
|
||||
screen = term.wait(bar)
|
||||
self.assertEqual(screen.splitlines()[-1].strip(), bar)
|
||||
term.send("q")
|
||||
self.assertEqual(term.close(), 130)
|
||||
for name in env:
|
||||
self.env.pop(name)
|
||||
|
||||
def test_resize_redraws(self):
|
||||
self.repo()
|
||||
term = self.start(cols=40, rows=15)
|
||||
|
||||
+11
-4
@@ -169,8 +169,12 @@ install starts.
|
||||
| Quit | `q`, Ctrl+C | B on the first screen |
|
||||
|
||||
The bottom line of each screen names what A does on the row under the cursor,
|
||||
with both spellings (`A/Enter`, `B/Esc`), since A and B swap places between
|
||||
controller layouts. A narrow screen shortens them to `A`, `B` and `X`. On the
|
||||
in the names of what is held. On a computer it names the keys (`Enter Install`,
|
||||
`Esc Back`). Started by a handheld launcher, it names the buttons of that
|
||||
launcher (`A Install`, `B Back`, see `RETROBIOS_PAD` below), and `?` shows
|
||||
both the buttons and the keys. Where neither is known, in SteamOS game mode or
|
||||
on a console no launcher named, it gives both spellings (`A/Enter`, `B/Esc`),
|
||||
and a narrow screen shortens them to `A`, `B` and `X`. On the
|
||||
regions screen, Left and Right move the region under the cursor up and down the
|
||||
order of preference. During the download, a stop waits for the files in
|
||||
progress, and a second one stops at once. Start+Select on a handheld ends the
|
||||
@@ -1072,9 +1076,12 @@ Each launcher tells the interface which buttons reach it, in
|
||||
| `gptokeyb` | gptokeyb or gptokeyb2 with `retrobios.gptk`, the table above | the PortMaster port, the Tools entry, the muOS app with PortMaster |
|
||||
| `muxterm` | muxterm's own keys: D-pad, Start as Enter, B as Backspace, Y as Space | the muOS app without PortMaster |
|
||||
| `evmapy` | `RetroBIOS.sh.keys`, the same buttons as the table | the Batocera and Knulli port |
|
||||
| `miyoo` | buttons that are already keys, A as Space and B as Left Ctrl | no launcher yet |
|
||||
| `miyoo` | buttons that are already keys: A as Space, Start as Enter, L1 and R1 as E and T, L2 as Tab, R2 as Backspace | no launcher yet |
|
||||
|
||||
A launcher with no mapper at all (a keyboard) leaves it unset.
|
||||
A launcher with no mapper at all (a keyboard) leaves it unset. On a Miyoo, B,
|
||||
X, Y and Select are Left Ctrl, Shift, Alt and Right Ctrl: pressed alone, a
|
||||
modifier sends nothing to a terminal, so the interface goes back with R2
|
||||
instead, and the bottom line says so.
|
||||
|
||||
**Without Wi-Fi.** On a computer, download the BIOS pack that serves your
|
||||
system (every part, for a pack in parts): RetroArch's on muOS, ArkOS,
|
||||
|
||||
Reference in new issue
Block a user