mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
chore: merge the last installer items
# Conflicts: # install.ps1 # install.py # install.sh # install/launchers/index.json
This commit is contained in:
commit
dc225acb57
18 files changed
+770
-106
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "c63a84da36e10657838dc359f4258db75e098d4902ee27312296ccd22a6994f4"
|
||||
$defaultInstallSha256 = "000b7e9e965f6d81c254d7ee7967077f9bbae8aca2286179f31d4f4f7b555d06"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+313
-64
@@ -15,6 +15,7 @@ Usage:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import bisect
|
||||
import codecs
|
||||
import concurrent.futures
|
||||
import contextlib
|
||||
@@ -543,6 +544,10 @@ EVENT_KEYS: dict[str, tuple[str, ...]] = {
|
||||
# kind of its own, since schema 1 never adds a key to the file event.
|
||||
"declared": ("platform", "files", "present", "missing", "wrong_hash",
|
||||
"bytes_needed", "dests"),
|
||||
# Right after declared: the files of the data trees to fetch (check's
|
||||
# data_missing) and the bytes they write. A kind of its own, since
|
||||
# schema 1 never adds a key to check.
|
||||
"data": ("platform", "files", "bytes"),
|
||||
# A file to install that this machine already holds: misplaced in the
|
||||
# BIOS folder (folder is its bios_dir, reason says why in words) or in
|
||||
# a folder named with --from (reason empty). member names the ZIP member
|
||||
@@ -642,7 +647,7 @@ def _os_name() -> str:
|
||||
def _emit_check(
|
||||
plat: str, manifest: dict, bios_path: Path, files: list[dict], up_to_date: list[dict],
|
||||
mismatched: list[dict], data_missing: int, not_installable: int,
|
||||
chosen: list[str] | None = None,
|
||||
chosen: list[str] | None = None, data_bytes: int = 0,
|
||||
) -> None:
|
||||
"""One event per file, per system and per platform after a check, then
|
||||
one for the files of the platform's own list.
|
||||
@@ -705,6 +710,7 @@ def _emit_check(
|
||||
bytes_needed=sum(size for _d, size, state in own_states if state != "present"),
|
||||
dests=[dest for dest, _s, _state in own_states],
|
||||
)
|
||||
EVENTS.emit("data", platform=plat, files=data_missing, bytes=data_bytes)
|
||||
|
||||
|
||||
def _declared_state(copy: dict, entry: dict, bios_path: Path) -> str:
|
||||
@@ -9549,6 +9555,74 @@ def _check_card_root(plat: str, dest: Path) -> None:
|
||||
f"{display_name(plat)} file list was asked for.")
|
||||
|
||||
|
||||
def card_bound(named: str, plat: str) -> tuple[str, str] | None:
|
||||
"""(system name, BIOS folder below the card root) when what --platform
|
||||
named runs from an SD card, else None.
|
||||
|
||||
`--platform onion` gets RetroArch's list, and on a computer that list's
|
||||
default folder is the computer's own RetroArch: the files went there,
|
||||
which Onion never reads. --platform retroarch names a list, not a card.
|
||||
"""
|
||||
if plat in CARD_PLATFORMS:
|
||||
return display_name(plat), ""
|
||||
key = _name_key(named)
|
||||
rule = next((rule for rule in _card_rules()
|
||||
if key and rule.manifest == plat and key == _name_key(rule.name)), None)
|
||||
return (rule.name, rule.bios) if rule else None
|
||||
|
||||
|
||||
def mounted_cards_of(system: str, plat: str) -> list[Path]:
|
||||
"""The mounted cards whose own files say they run system."""
|
||||
if plat in CARD_PLATFORMS:
|
||||
return [mount for mount in _mounted_cards() if card_platform(mount) == plat]
|
||||
return [mount for mount in _mounted_cards()
|
||||
if any(rule.name == system for rule in _card_matches(mount))]
|
||||
|
||||
|
||||
def card_install_dest(named: str, plat: str, interactive: bool) -> Path:
|
||||
"""The folder for a card-bound system named without --dest, on a machine
|
||||
that is not the device.
|
||||
|
||||
One mounted card that runs it is taken. At a terminal the card's root is
|
||||
asked for, never this machine's own folder. Without questions the run
|
||||
stops on the --dest that names the card.
|
||||
"""
|
||||
system, below = card_bound(named, plat) or (display_name(plat), "")
|
||||
cards = mounted_cards_of(system, plat)
|
||||
if len(cards) == 1:
|
||||
dest = cards[0] / below if below else cards[0]
|
||||
print(f" {system} card found at {cards[0]}: {display_name(plat)} files, in {dest}")
|
||||
return dest
|
||||
example = (f"--platform {named.strip() or plat} --dest /media/$USER/<card> "
|
||||
"(Windows: --dest E:\\)")
|
||||
if len(cards) > 1:
|
||||
print(f" {len(cards)} {system} cards are mounted: "
|
||||
+ ", ".join(str(card) for card in cards) + ".", file=sys.stderr)
|
||||
if not interactive:
|
||||
EVENTS.emit("error", stage="detect", reason="not_found", platform=plat,
|
||||
message=f"{system} keeps its BIOS files on its SD card, and no "
|
||||
"card was named with --dest")
|
||||
print(f" {system} keeps its BIOS files on its SD card, not on this machine. "
|
||||
f"Mount the card and name its root with --dest, for example: {example}",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f" {system} keeps its BIOS files on its SD card, not on this machine. "
|
||||
"Mount the card, then type its root folder.")
|
||||
raw = ""
|
||||
while not raw:
|
||||
raw = _ask("Card root (Ctrl+C to stop): ")
|
||||
root = _typed_folder(raw)
|
||||
if plat in CARD_PLATFORMS:
|
||||
_check_card_root(plat, root)
|
||||
return root
|
||||
folder = card_dest(named, plat, root)[1]
|
||||
if folder == root and below and not _ends_with(root, below):
|
||||
# A KNULLI share or a ROCKNIX card read as a list's name: its
|
||||
# folder below the root, as the device reads it.
|
||||
return root / below
|
||||
return folder
|
||||
|
||||
|
||||
def _platform_for_dest(os_type: str, dest: Path, interactive: bool) -> str:
|
||||
"""The file list for a --dest given without --platform.
|
||||
|
||||
@@ -9670,7 +9744,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
elif args.platform:
|
||||
print("Detecting platform...")
|
||||
detected = detect_platforms(os_type)
|
||||
matched = [(n, p) for n, p in detected if n == args.platform]
|
||||
bound = card_bound(named, args.platform)
|
||||
# A system that runs from its card is found only on the device:
|
||||
# Onion named on a PC matched the PC's own RetroArch.
|
||||
matched = [(n, p) for n, p in detected if n == args.platform
|
||||
and (bound is None or detected_os(n, p) == bound[0])]
|
||||
if matched:
|
||||
platforms = matched
|
||||
elif listing:
|
||||
@@ -9678,20 +9756,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
# needed, and asking BizHawk's for one refused a question asked
|
||||
# before downloading BizHawk.
|
||||
platforms = [(args.platform, _default_dest(os_type, args.platform) or Path.cwd())]
|
||||
elif args.platform in CARD_PLATFORMS:
|
||||
elif bound is not None:
|
||||
# Its files go on its SD card, which is never this machine's
|
||||
# own folder: the card's mount point is the user's to name.
|
||||
EVENTS.emit("error", stage="detect", reason="not_found", platform=args.platform,
|
||||
message=f"{display_name(args.platform)} keeps its BIOS files on "
|
||||
"its SD card, and no card was named with --dest")
|
||||
print(
|
||||
f" {display_name(args.platform)} keeps its BIOS files on its SD card. "
|
||||
f"Mount the card and name its root with --dest, for example: "
|
||||
f"--platform {args.platform} --dest /media/$USER/<card> "
|
||||
f"(Windows: --dest E:\\)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
# own folder.
|
||||
platforms = [(args.platform, card_install_dest(named, args.platform,
|
||||
interactive))]
|
||||
elif args.platform in PORTABLE_ONLY:
|
||||
# A portable emulator reads the folder beside its executable:
|
||||
# any default would be a folder it never opens.
|
||||
@@ -10163,6 +10232,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
plat_name, manifest, bios_path, files, up_to_date, mismatched,
|
||||
sum(len(state[1]) for _entry, state in data_states), len(omitted_files),
|
||||
chosen=selection[0] if selection else None,
|
||||
data_bytes=sum(meta.get("size", 0) for _entry, state in data_states
|
||||
for meta in state[1].values()),
|
||||
)
|
||||
emit_copies(plat_name, to_download, copies, bios_index, bios_path, files)
|
||||
emit_unreadable(plat_name, unread)
|
||||
@@ -10740,6 +10811,11 @@ class TuiState:
|
||||
# "chosen" (the run's folder, from Change folder) or "closed" (the
|
||||
# one chosen could not be opened, none is offered).
|
||||
self.folder_kind: str = "usual"
|
||||
# (system, BIOS folder below the card root) when the setup asked for
|
||||
# runs from an SD card: the folder screen offers the mounted cards.
|
||||
self.card_bound: tuple[str, str] | None = None
|
||||
# (width and text, lines) of the last pager wrapped.
|
||||
self.pager_cache: tuple | None = None
|
||||
self.path_text: str = ""
|
||||
self.browse_dir: Path | None = None
|
||||
self.browse_rows: list = []
|
||||
@@ -11141,13 +11217,20 @@ def tui_rows(state: TuiState) -> list[TuiRow]:
|
||||
if screen == "platforms":
|
||||
needle = state.filters.get(screen, "")
|
||||
systems = _systems_reading()
|
||||
return [_filter_row(state)] + [
|
||||
TuiRow("platform", display_name(plat), ", ".join(systems.get(plat, [])), plat)
|
||||
for plat in AVAILABLE_PLATFORMS
|
||||
if _matches(" ".join([plat, display_name(plat), *systems.get(plat, [])]), needle)
|
||||
]
|
||||
rows = [_filter_row(state)]
|
||||
for plat in AVAILABLE_PLATFORMS:
|
||||
named = _setup_matches(plat, systems.get(plat, []), needle)
|
||||
if named is not None:
|
||||
# The systems the filter named, those a device typed runs:
|
||||
# "miyoo" on RetroArch read "muOS, JELOS, KOLOS...".
|
||||
rows.append(TuiRow("platform", display_name(plat),
|
||||
", ".join(named or systems.get(plat, [])), plat))
|
||||
return rows
|
||||
if screen == "folder":
|
||||
rows = [TuiRow("default", "Use this folder")] if state.folder_default else []
|
||||
if _tui_card_bound(state):
|
||||
rows += [TuiRow("card", f"{_mount_name(mount)} ({_card_what(mount)})", value=mount)
|
||||
for mount in _mounted_cards()]
|
||||
return rows + [TuiRow("browse", "Browse for a folder"), TuiRow("type", "Type a path")]
|
||||
if screen == "browse":
|
||||
return list(state.browse_rows)
|
||||
@@ -11529,6 +11612,19 @@ def _mounted_cards() -> list[Path]:
|
||||
return found
|
||||
|
||||
|
||||
def _tui_card_bound(state: TuiState) -> tuple[str, str] | None:
|
||||
"""The card the chosen setup runs from, when it runs from one."""
|
||||
if state.card_bound:
|
||||
return state.card_bound
|
||||
return card_bound(state.platform, state.platform) if state.platform in CARD_PLATFORMS else None
|
||||
|
||||
|
||||
def _card_what(mount: Path) -> str:
|
||||
"""What a mounted card is, as its own files say."""
|
||||
system = card_system(mount)
|
||||
return f"{system[2]} card" if system else "card or drive"
|
||||
|
||||
|
||||
def _browse_rows(folder: Path | None, zips: bool = False) -> tuple[list[TuiRow], str]:
|
||||
"""A directory's rows for the browser, cards first, and what went wrong.
|
||||
With zips, the ZIP files too: a backup is often one archive."""
|
||||
@@ -11539,9 +11635,7 @@ def _browse_rows(folder: Path | None, zips: bool = False) -> tuple[list[TuiRow],
|
||||
rows = [TuiRow("use", "Use this folder"), TuiRow("up", "..", value=folder.parent)]
|
||||
for card in _mounted_cards():
|
||||
if card != folder:
|
||||
system = card_system(card)
|
||||
what = f"{system[2]} card" if system else "card or drive"
|
||||
rows.append(TuiRow("dir", f"{card} ({what})", value=card))
|
||||
rows.append(TuiRow("dir", f"{card} ({_card_what(card)})", value=card))
|
||||
try:
|
||||
entries = list(os.scandir(folder))
|
||||
except OSError as exc:
|
||||
@@ -11651,7 +11745,8 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
|
||||
if state.dest_given is not None:
|
||||
return _choose(state, row.value, state.dest_given)
|
||||
state.platform = row.value
|
||||
if row.value in CARD_PLATFORMS:
|
||||
state.card_bound = card_bound(row.value, row.value)
|
||||
if state.card_bound:
|
||||
return _open_folder_screen(state, None)
|
||||
# The folder detected here before the built-in one: the built-in
|
||||
# default was offered as where RetroArch reads, when the
|
||||
@@ -11663,6 +11758,9 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
|
||||
if screen == "folder":
|
||||
if action == "default":
|
||||
return _choose(state, state.platform, state.folder_default)
|
||||
if action == "card":
|
||||
below = (_tui_card_bound(state) or ("", ""))[1]
|
||||
return _choose(state, state.platform, row.value / below if below else row.value)
|
||||
if action == "browse":
|
||||
state.browse_for = "dest"
|
||||
_push(state, "browse")
|
||||
@@ -11719,6 +11817,9 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
|
||||
return _open_from_browser(state)
|
||||
if action in ("misplaced", "unreadable"):
|
||||
return _push(state, action)
|
||||
if action == "systems" and len(state.systems) > TUI_LONG_LIST:
|
||||
# On None: picking one system of 66 began with Up, Up, A.
|
||||
return _push(state, action, 1)
|
||||
# On the first system, so A continues and the D-pad ticks.
|
||||
return _push(state, action, 3 if action == "systems" else 0)
|
||||
if screen == "systems":
|
||||
@@ -11726,7 +11827,14 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
|
||||
state.ticked = {row["system"] for row in state.systems}
|
||||
elif action == "none":
|
||||
state.ticked = set()
|
||||
# On the first system: the next A takes the one in focus alone.
|
||||
state.focus["systems"] = next(
|
||||
(i for i, item in enumerate(tui_rows(state)) if item.action == "system"),
|
||||
state.focus.get("systems", 0))
|
||||
else:
|
||||
if action == "system" and not state.ticked:
|
||||
# Nothing ticked: A takes the system in focus, and only it.
|
||||
state.ticked = {row.value}
|
||||
return _begin_install(state)
|
||||
return []
|
||||
if screen == "options":
|
||||
@@ -11770,6 +11878,94 @@ def _systems_reading() -> dict[str, list[str]]:
|
||||
return names
|
||||
|
||||
|
||||
# The devices people type in the setup filter instead of the setup they run,
|
||||
# beside the setups a first-party page says each one runs. Keys are platform
|
||||
# ids or the system names of _EMBEDDED_RULES, and a device that runs several
|
||||
# setups is under each. The handheld systems' lists are those of
|
||||
# platforms/_guides.yml (handheld_guide, each with its devices_source), and a
|
||||
# test holds the two equal.
|
||||
_SETUP_DEVICES: dict[str, tuple[str, ...]] = {
|
||||
"ArkOS": ("RG353M", "RG353V", "RG503", "RGB30", "RK2023", "RG351MP", "RG351V", "R35S",
|
||||
"RGB10"),
|
||||
"muOS": ("RG35XX H", "RG35XX Plus", "RG35XX SP", "RG35XX 2024", "RG34XX H", "RG40XX H",
|
||||
"RG40XX V", "RG28XX H", "RGCUBEXX", "TrimUI Brick", "TrimUI Smart Pro"),
|
||||
"Onion": ("Miyoo Mini", "Miyoo Mini+"),
|
||||
"spruceOS": ("Miyoo A30", "Miyoo Flip", "Miyoo Mini", "Miyoo Mini Plus",
|
||||
"Miyoo Mini Flip", "TrimUI Smart Pro", "TrimUI Brick",
|
||||
"Anbernic RG35XX Plus", "RG40XX H", "Powkiddy RGB30"),
|
||||
"Allium": ("Miyoo Mini", "Miyoo Mini Plus", "Miyoo Mini Flip"),
|
||||
"ArchR": ("R36S", "R33S", "R36S Soysauce", "Odroid Go Advance", "Odroid Go Super",
|
||||
"RG351V", "RG351M", "GameForce Chi", "MagicX XU10", "Powkiddy RGB10",
|
||||
"Powkiddy RGB10X", "K36"),
|
||||
"CrossMix-OS": ("TrimUI Smart Pro",),
|
||||
"dArkOS": ("Odroid Go Advance", "RGB10", "RG351MP", "G350"),
|
||||
"KOLOS": ("RG353P", "RG353M", "RG353V", "RG353VS", "RG503", "Powkiddy RK2023", "RGB30",
|
||||
"RGB10 Max 3"),
|
||||
"Koriki": ("Miyoo Mini",),
|
||||
"MiyooCFW": ("BittBoy", "Pocket Go", "PowKiddy Q90", "PowKiddy V90", "PowKiddy Q20 Mini",
|
||||
"Sup M3", "XYC Q8"),
|
||||
"plumOS": ("MagicX XU20 V32",),
|
||||
"RetroOZ": ("OGS", "OGA", "RGB10Max", "RGB10Max2"),
|
||||
"UnofficialOS": ("R35S", "R36S", "RG351M", "RG351P", "RG351V", "RG353M", "RG353P",
|
||||
"RG353V", "RG503", "RG552", "RGB30", "RGB20 Pro", "Odroid Go Advance",
|
||||
"Odroid Go Super"),
|
||||
# The Android tile of the handheld group (_guides.yml): goretroid.com
|
||||
# "OS: Android 13" for the Retroid Pocket 5, ayntec.com "Android 15" for
|
||||
# the AYN Odin 3. The guide runs RetroArch on Steam Deck (run: steamdeck)
|
||||
# and on Windows, which the ROG Ally runs (rog.asus.com spec,
|
||||
# "Windows 11 Home").
|
||||
"retroarch": ("Steam Deck", "ROG Ally", "Android", "Retroid Pocket", "Retroid Pocket 5",
|
||||
"AYN Odin", "AYN Odin 3"),
|
||||
"emudeck": ("Steam Deck", "ROG Ally"),
|
||||
"retrodeck": ("Steam Deck",),
|
||||
"retrobat": ("ROG Ally",),
|
||||
"bizhawk": ("ROG Ally",),
|
||||
# _guides.yml runs_on, each cited there.
|
||||
"batocera": ("Steam Deck",),
|
||||
"amberelec": ("Anbernic RG351", "RG552"),
|
||||
"emuelec": ("Odroid Go",),
|
||||
"minui": ("Anbernic", "Miyoo", "TrimUI"),
|
||||
"nextui": ("TrimUI Brick", "TrimUI Smart Pro"),
|
||||
"myminui": ("Anbernic", "Miyoo"),
|
||||
# rocknix.org/devices, read 2026-10-26.
|
||||
"rocknix": ("Anbernic RG351", "RG353", "RG503", "RG552", "RG 55G1", "RG ARC",
|
||||
"RG35XX 2024", "RG35XX Plus", "RG35XX Pro", "RG35XX H", "RG35XX SP", "RG28XX",
|
||||
"RG40XX V", "RG40XX H", "RG CubeXX", "RG34XX", "AYANEO Pocket", "Ayn Odin 2",
|
||||
"Ayn Odin 3", "Ayn Thor", "GameForce Ace", "Odroid Go Advance",
|
||||
"Odroid Go Super", "Odroid Go Ultra", "MagicX XU Mini M", "Mangmi Air X",
|
||||
"Powkiddy RGB10", "RGB10X", "RK2023", "RGB20SX", "RGB20 Pro", "RGB30", "X35S",
|
||||
"X35H", "X55", "XU10", "Retroid Pocket 5", "Retroid Pocket 6",
|
||||
"Retroid Pocket Flip2", "Retroid Pocket Mini", "Retroid Pocket Nova", "R33S",
|
||||
"R35S", "R36S", "K36"),
|
||||
# knulli.org, read 2026-10-26.
|
||||
"knulli": ("Anbernic RG-Arc-S", "RG28XX", "RG34XX", "RG35XX Original", "RG35XX Plus",
|
||||
"RG35XX H", "RG35XX 2024", "RG35XX SP", "RG40XX H", "RG40XX V", "RGCubeXX",
|
||||
"Retroid Pocket 5", "Retroid Pocket Flip 2", "Retroid Pocket Mini",
|
||||
"Miyoo Flip", "Orange Pi Zero 2w", "Powkiddy RGB30", "X55", "TrimUI Brick",
|
||||
"TrimUI Smart Pro", "BattleXP G350", "GKD Pixel 2"),
|
||||
}
|
||||
|
||||
|
||||
def _device_needle(needle: str) -> str:
|
||||
"""The filter text with a model's short form spelled out: owners write
|
||||
RP5 for the Retroid Pocket 5, as the store names it."""
|
||||
short = re.fullmatch(r"\s*rp\s*(\d+)\s*", needle, re.IGNORECASE)
|
||||
return f"Retroid Pocket {short.group(1)}" if short else needle
|
||||
|
||||
|
||||
def _setup_matches(plat: str, systems: list[str], needle: str) -> list[str] | None:
|
||||
"""What the filter names for plat: its devices and systems that match,
|
||||
[] when plat's own name does, None when nothing does. A device counts
|
||||
for each setup it runs."""
|
||||
if not needle or _matches(f"{plat} {display_name(plat)}", needle):
|
||||
return []
|
||||
needle = _device_needle(needle)
|
||||
devices = [device for device in _SETUP_DEVICES.get(plat, ()) if _matches(device, needle)]
|
||||
named = [system for system in systems
|
||||
if _matches(" ".join([system, *_SETUP_DEVICES.get(system, ())]), needle)]
|
||||
return devices + named or None
|
||||
|
||||
|
||||
def _platform_focus(state: TuiState, plat: str) -> int:
|
||||
"""The platform list's row for plat, below its filter row."""
|
||||
return 1 + AVAILABLE_PLATFORMS.index(plat) if plat in AVAILABLE_PLATFORMS else 1
|
||||
@@ -11982,18 +12178,11 @@ def _pager_key(state: TuiState, key: TuiKey) -> list[tuple]:
|
||||
|
||||
|
||||
def _pager_last(state: TuiState, items: list) -> int:
|
||||
"""The first item shown when the end of a pager is in view, found from
|
||||
the end: only the last screenful is wrapped."""
|
||||
width = min(state.cols, TUI_MAX_WIDTH) - 2
|
||||
room = max(1, state.rows - 4)
|
||||
used, index = 0, len(items)
|
||||
while index > 0:
|
||||
size = len(_text_lines([items[index - 1]], width))
|
||||
if used + size > room:
|
||||
break
|
||||
used += size
|
||||
index -= 1
|
||||
return index
|
||||
"""The first line shown when the end of a pager is in view: the view's
|
||||
own height (tui_view), less the line that counts."""
|
||||
total = _pager_starts(state, items, min(state.cols, TUI_MAX_WIDTH) - 2)[-1]
|
||||
height = state.rows - 3
|
||||
return 0 if total <= height else max(0, total - max(1, height - 1))
|
||||
|
||||
|
||||
def tui_update(state: TuiState, key: TuiKey) -> list[tuple]:
|
||||
@@ -12016,6 +12205,12 @@ def tui_update(state: TuiState, key: TuiKey) -> list[tuple]:
|
||||
return _filter_key(state, key)
|
||||
if state.pad == "miyoo":
|
||||
key = _miyoo_key(state, key)
|
||||
if (state.pad in _NO_PAGE_PADS and key.name in ("left", "right")
|
||||
and screen not in ("regions", "options")):
|
||||
# muxterm keeps PgUp and PgDn for its scrollback: the D-pad's Left
|
||||
# and Right page instead (X or Y still ticks). One system of 66 took
|
||||
# about 60 Down presses.
|
||||
key = TuiKey("pgup" if key.name == "left" else "pgdn")
|
||||
char = key.text if key.name == "char" else ""
|
||||
if key.name == "f1" or char == "?":
|
||||
state.help = True
|
||||
@@ -12099,6 +12294,10 @@ def tui_update(state: TuiState, key: TuiKey) -> list[tuple]:
|
||||
return []
|
||||
|
||||
|
||||
# The routes inside muxterm, where no button sends PgUp or PgDn.
|
||||
_NO_PAGE_PADS = ("muxterm", "gptokeyb_muxterm")
|
||||
|
||||
|
||||
# A Miyoo's L1 and R1 are the keys E and T (codes 18 and 20).
|
||||
_MIYOO_PAGES = {"e": "pgup", "t": "pgdn"}
|
||||
|
||||
@@ -12163,6 +12362,10 @@ def _take_event(state: TuiState, event: dict) -> None:
|
||||
state.done_saves = event
|
||||
elif kind == "found" and state.run == "install":
|
||||
state.local_dests[event["dest"]] = (event["folder"], event["reason"])
|
||||
elif kind == "data" and state.run == "check" and state.check is not None:
|
||||
state.check["data_bytes"] = event["bytes"]
|
||||
elif kind == "data" and state.run in ("confirm", "preview") and state.dry is not None:
|
||||
state.dry["data_bytes"] = event["bytes"]
|
||||
elif state.run == "check":
|
||||
if kind == "check":
|
||||
state.check = event
|
||||
@@ -12388,6 +12591,9 @@ def _scrolled(lines: list[TuiLine], first: int, height: int, lead: int = 0) -> l
|
||||
|
||||
# The rows a screen keeps for its list under pinned lines.
|
||||
TUI_PINNED_ROWS = 4
|
||||
# A list longer than this opens its systems on None, and its help names a
|
||||
# page key.
|
||||
TUI_LONG_LIST = 9
|
||||
|
||||
|
||||
def _compose(
|
||||
@@ -12433,24 +12639,46 @@ def _compose(
|
||||
def _pager(
|
||||
state: TuiState, width: int, height: int, items: list, lead: int = 0,
|
||||
) -> list[TuiLine]:
|
||||
"""The items in view, from the first shown on, each wrapped alone: the
|
||||
8,000 lines of a full preview were wrapped again at every key."""
|
||||
first = max(0, min(state.focus.get(state.screen, 0), _pager_last(state, items)))
|
||||
if len(items) <= height and first == 0:
|
||||
lines = _text_lines(items, width)
|
||||
if len(lines) <= height:
|
||||
return lines
|
||||
"""The lines in view, scrolled line by line, the last one saying which
|
||||
lines these are. The lead items (a count above a list) are not counted.
|
||||
|
||||
It scrolled by item and counted items: at 40x12 the result read
|
||||
"1-2 of 5" over eight lines, its last paragraph cut, and a paragraph
|
||||
longer than the screen could never be read to its end. Only the items
|
||||
in view are wrapped: the 8,000 lines of a full preview were wrapped
|
||||
again at every key.
|
||||
"""
|
||||
starts = _pager_starts(state, items, width)
|
||||
total = starts[-1]
|
||||
if total <= height:
|
||||
return _text_lines(items, width)
|
||||
room = max(1, height - 1)
|
||||
first = max(0, min(state.focus.get(state.screen, 0), total - room))
|
||||
index = bisect.bisect_right(starts, first) - 1
|
||||
offset = first - starts[index]
|
||||
lines: list[TuiLine] = []
|
||||
index = first
|
||||
while index < len(items) and len(lines) < room:
|
||||
while index < len(items) and starts[index] < first + room:
|
||||
lines += _text_lines([items[index]], width)
|
||||
index += 1
|
||||
whole = index if len(lines) <= room else index - 1
|
||||
lines = lines[:room]
|
||||
start = max(1, first + 1 - lead)
|
||||
return lines + [_styled(f"{start}-{max(start, whole - lead)} of {len(items) - lead}",
|
||||
"dim")]
|
||||
skip = starts[lead] if lead else 0
|
||||
start = max(1, first + 1 - skip)
|
||||
end = max(start, first + room - skip)
|
||||
return lines[offset:offset + room] + [_styled(f"{start}-{end} of {total - skip}", "dim")]
|
||||
|
||||
|
||||
def _pager_starts(state: TuiState, items: list, width: int) -> list[int]:
|
||||
"""The line each item starts on once wrapped at width, and the total
|
||||
last, kept while the items stay the same. An item that fits is one line
|
||||
without being wrapped."""
|
||||
key = (width, [(str(item), getattr(item, "style", "")) for item in items])
|
||||
if state.pager_cache is not None and state.pager_cache[0] == key:
|
||||
return state.pager_cache[1]
|
||||
starts = [0]
|
||||
for item in items:
|
||||
fits = getattr(item, "style", "") == "path" or cell_width(str(item)) <= width
|
||||
starts.append(starts[-1] + (1 if fits else len(_text_lines([item], width))))
|
||||
state.pager_cache = (key, starts)
|
||||
return starts
|
||||
|
||||
|
||||
def _tui_misplaced(state: TuiState) -> list[dict]:
|
||||
@@ -12521,11 +12749,15 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
|
||||
headline = f"{_plural(fetch, 'file')} to download, {format_size(fetch_bytes)}"
|
||||
elif need:
|
||||
headline = f"{_plural(len(copied), 'file')} to copy from this machine"
|
||||
# The data trees' size comes from their own event: "2636 data files"
|
||||
# beside "6.6 GB" said nothing of what they weigh.
|
||||
data_size = (f" ({format_size(check['data_bytes'])})" if data and "data_bytes" in check
|
||||
else "")
|
||||
if need:
|
||||
if data:
|
||||
headline += f", and {_plural(data, 'data file')}"
|
||||
headline += f", and {_plural(data, 'data file')}{data_size}"
|
||||
if not need:
|
||||
headline = (f"{_plural(data, 'data file')} to fetch" if data
|
||||
headline = (f"{_plural(data, 'data file')} to fetch{data_size}" if data
|
||||
else "Everything is in place")
|
||||
wrong = check["wrong_hash"]
|
||||
offline_lines = (["Copy them from your old card or a folder, or run the installer "
|
||||
@@ -12570,7 +12802,9 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
|
||||
f"{'is' if len(misplaced) == 1 else 'are'} in the wrong place, "
|
||||
f"{'it' if len(misplaced) == 1 else 'they'} will be copied where the "
|
||||
"emulators read "
|
||||
f"{'it' if len(misplaced) == 1 else 'them'}.")
|
||||
f"{'it' if len(misplaced) == 1 else 'them'}, your "
|
||||
f"{'copy' if len(misplaced) == 1 else 'copies'} left as "
|
||||
f"{'it is' if len(misplaced) == 1 else 'they are'}.")
|
||||
if state.facts.get("writable") is False:
|
||||
if state.os_type == "windows":
|
||||
lines.append("Or run the installer as an administrator.")
|
||||
@@ -12654,10 +12888,13 @@ def _view_platforms(state: TuiState, width: int, height: int) -> tuple:
|
||||
|
||||
def _view_folder(state: TuiState, width: int, height: int) -> tuple:
|
||||
name = display_name(state.platform)
|
||||
if state.platform in CARD_PLATFORMS:
|
||||
bound = _tui_card_bound(state)
|
||||
if bound:
|
||||
title = "Where is the SD card?"
|
||||
before = [f"{name} keeps its BIOS files on its SD card. Choose the card's "
|
||||
"root folder, the one holding .system."]
|
||||
before = [f"{bound[0]} keeps its BIOS files on its SD card. Choose the card's "
|
||||
"root folder"
|
||||
+ (", the one holding .system." if state.platform in CARD_PLATFORMS
|
||||
else ".")]
|
||||
elif state.folder_default is not None:
|
||||
title = "Which folder?"
|
||||
lead = {"found": f"{name} reads its BIOS files from:",
|
||||
@@ -13119,6 +13356,10 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]:
|
||||
sources = _tui_sources(state, installed, len(failed))
|
||||
if sources:
|
||||
lines.append(sources)
|
||||
ok = {dest for dest, good, _why in state.finished if good}
|
||||
if any(reason and dest in ok for dest, (_folder, reason) in state.local_dests.items()):
|
||||
# A fix copies: the misplaced files are the user's, never moved.
|
||||
lines.append("Your copies at the wrong place were left as they are.")
|
||||
lines.append(f"BIOS folder: {state.dest}")
|
||||
if installed + up_to_date:
|
||||
lines += _saves_lines(state, state.done_saves)
|
||||
@@ -13241,9 +13482,9 @@ _HELP_KEYS: dict[str, dict[str, str | None]] = {
|
||||
"back": "B, Select, Esc", "stop": "B, Esc, Ctrl-C", "kill": None,
|
||||
"enter": "A, Enter", "esc": "B, Esc"},
|
||||
# muxterm keeps PgUp and PgDn for its own scrollback, and its L1 and R1
|
||||
# send nothing: no key pages a list there.
|
||||
"muxterm": {"move": "D-pad, Up/Down", "page": None, "ok": "Start, Enter",
|
||||
"tick": "Y, Space, Right/Left", "options": "o, Tab",
|
||||
# send nothing: the D-pad's Left and Right page a list there.
|
||||
"muxterm": {"move": "D-pad, Up/Down", "page": "D-pad Left/Right", "ok": "Start, Enter",
|
||||
"tick": "Y, Space", "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",
|
||||
@@ -13252,8 +13493,9 @@ _HELP_KEYS: dict[str, dict[str, str | None]] = {
|
||||
"enter": "Start, Enter", "esc": "Esc"},
|
||||
}
|
||||
# muxterm keeps PgUp and PgDn for its scrollback even when gptokeyb2 owns the
|
||||
# pad, and the launcher sends nothing on L1 and R1 there.
|
||||
_HELP_KEYS["gptokeyb_muxterm"] = {**_HELP_KEYS["gptokeyb"], "page": None}
|
||||
# pad, and the launcher sends nothing on L1 and R1 there: Left and Right page.
|
||||
_HELP_KEYS["gptokeyb_muxterm"] = {**_HELP_KEYS["gptokeyb"], "page": "D-pad Left/Right",
|
||||
"tick": "X, Space"}
|
||||
|
||||
|
||||
def _route_extras(state: TuiState) -> list[tuple[str, str]]:
|
||||
@@ -13280,7 +13522,7 @@ def _help_lines(state: TuiState) -> list[tuple[str, str]]:
|
||||
("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:
|
||||
if len(tui_rows(state)) > TUI_LONG_LIST or screen in _PAGERS:
|
||||
keys += [*([(names["page"], "Page")] if names["page"] else []),
|
||||
("Home/End, g/G", "First, last")]
|
||||
primary = _primary(state) or _TUI_VIEWS[screen](state, 80, 24)[2][0]
|
||||
@@ -13319,6 +13561,8 @@ def _primary(state: TuiState) -> str | None:
|
||||
return None
|
||||
row = rows[max(0, min(state.focus.get(state.screen, 0), len(rows) - 1))]
|
||||
verb = _ROW_VERBS.get(row.action)
|
||||
if state.screen == "systems" and row.action == "system" and not state.ticked:
|
||||
verb = "Only this system"
|
||||
return f"A/Enter {verb}" if verb else None
|
||||
|
||||
|
||||
@@ -13465,7 +13709,11 @@ def tui_start(
|
||||
state.stack = []
|
||||
return state, _choose(state, plat, Path(args.dest))
|
||||
if plat:
|
||||
matched = [setup for setup in state.setups if setup[0] == plat]
|
||||
bound = card_bound(args.platform, plat)
|
||||
# A system that runs from its card is its card or the device, never
|
||||
# this machine's own folder for the same list.
|
||||
matched = [setup for setup in state.setups if setup[0] == plat
|
||||
and (bound is None or detected_os(setup[0], setup[1]) == bound[0])]
|
||||
if len(matched) == 1:
|
||||
state.stack = []
|
||||
return state, _choose(state, plat, matched[0][1])
|
||||
@@ -13473,8 +13721,9 @@ def tui_start(
|
||||
state.setups = matched
|
||||
return state, []
|
||||
state.platform = plat
|
||||
state.card_bound = bound
|
||||
state.stack = []
|
||||
default = None if plat in CARD_PLATFORMS else _default_dest(os_type, plat)
|
||||
default = None if bound else _default_dest(os_type, plat)
|
||||
return state, _open_folder_screen(state, default)
|
||||
if args.dest is not None:
|
||||
card = card_platform(Path(args.dest))
|
||||
|
||||
+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="c63a84da36e10657838dc359f4258db75e098d4902ee27312296ccd22a6994f4"
|
||||
DEFAULT_INSTALL_SHA256="000b7e9e965f6d81c254d7ee7967077f9bbae8aca2286179f31d4f4f7b555d06"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 622744,
|
||||
"sha256": "c63a84da36e10657838dc359f4258db75e098d4902ee27312296ccd22a6994f4",
|
||||
"size": 636292,
|
||||
"sha256": "000b7e9e965f6d81c254d7ee7967077f9bbae8aca2286179f31d4f4f7b555d06",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,15 +71,15 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 675244,
|
||||
"sha256": "447a2eb9863dd1dad2108d22132bbb8d553adca5ebf1b2bddfbce698df42cf3f"
|
||||
"size": 688841,
|
||||
"sha256": "65c0ff1debcc384c1a6da526e4c92505a1628036cc4c7f51928d5253fbd07484"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
"name": "RetroBIOS/README.md",
|
||||
"repo_path": "ports/muos/README.md",
|
||||
"size": 1127,
|
||||
"sha256": "c0cfafad5fae8e6764b263b14a06195b267f66c4fe042333941ae6bcc6dab265",
|
||||
"size": 1176,
|
||||
"sha256": "26b5da84f22ce3c652f16ed8335be11d99e3bfb8657eec7caa100cf505f68497",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 622744,
|
||||
"sha256": "c63a84da36e10657838dc359f4258db75e098d4902ee27312296ccd22a6994f4",
|
||||
"size": 636292,
|
||||
"sha256": "000b7e9e965f6d81c254d7ee7967077f9bbae8aca2286179f31d4f4f7b555d06",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 622744,
|
||||
"sha256": "c63a84da36e10657838dc359f4258db75e098d4902ee27312296ccd22a6994f4",
|
||||
"size": 636292,
|
||||
"sha256": "000b7e9e965f6d81c254d7ee7967077f9bbae8aca2286179f31d4f4f7b555d06",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,7 +11,8 @@ It runs in muxterm, muOS's terminal. With PortMaster installed, gptokeyb2
|
||||
turns every button into a key and muxterm is told to leave the pad alone, so
|
||||
each button is sent once: D-pad to move, A or Start to choose, B or Select to
|
||||
go back, X to tick a system, Y for the options. L1 and R1 do nothing in
|
||||
muxterm, which keeps Page Up and Page Down for its own scrollback. Hold
|
||||
muxterm, which keeps Page Up and Page Down for its own scrollback: Left and
|
||||
Right on the D-pad page a list instead. Hold
|
||||
Select, then press Start to quit. Without PortMaster, muxterm reads the pad
|
||||
itself (Y then ticks a system) and Menu closes it.
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"checking",
|
||||
"check",
|
||||
"declared",
|
||||
"data",
|
||||
"found",
|
||||
"unreadable",
|
||||
"saves",
|
||||
@@ -64,6 +65,9 @@
|
||||
{
|
||||
"$ref": "#/$defs/declared"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/data"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/found"
|
||||
},
|
||||
@@ -454,6 +458,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"event",
|
||||
"platform",
|
||||
"files",
|
||||
"bytes"
|
||||
],
|
||||
"properties": {
|
||||
"event": {
|
||||
"const": "data"
|
||||
},
|
||||
"platform": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"bytes": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"found": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
Vendored
+1
-1
@@ -8,5 +8,5 @@
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py
|
||||
1-4 of 8
|
||||
1-8 of 17
|
||||
A/Enter Exit Up/Down Scroll
|
||||
Vendored
+1
-1
@@ -11,5 +11,5 @@
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
1-5 of 8
|
||||
1-11 of 17
|
||||
A/Enter Exit Up/Down Scroll
|
||||
Vendored
+2
-2
@@ -981,7 +981,7 @@
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py
|
||||
1-4 of 8
|
||||
1-8 of 17
|
||||
A Exit Up/Down Scroll
|
||||
=== result 40x15
|
||||
RetroBIOS: Finished with errors
|
||||
@@ -997,7 +997,7 @@
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
1-5 of 8
|
||||
1-11 of 17
|
||||
A Exit Up/Down Scroll
|
||||
=== result 80x24
|
||||
RetroBIOS: Finished with errors
|
||||
|
||||
+2
-2
@@ -981,7 +981,7 @@
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py
|
||||
1-4 of 8
|
||||
1-8 of 17
|
||||
A Exit Up/Down Scroll
|
||||
=== result 40x15
|
||||
RetroBIOS: Finished with errors
|
||||
@@ -997,7 +997,7 @@
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
1-5 of 8
|
||||
1-11 of 17
|
||||
A Exit Up/Down Scroll
|
||||
=== result 80x24
|
||||
RetroBIOS: Finished with errors
|
||||
|
||||
+2
-2
@@ -981,7 +981,7 @@
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py
|
||||
1-4 of 8
|
||||
1-8 of 17
|
||||
A Exit Up/Down Scroll
|
||||
=== result 40x15
|
||||
RetroBIOS: Finished with errors
|
||||
@@ -997,7 +997,7 @@
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
1-5 of 8
|
||||
1-11 of 17
|
||||
A Exit Up/Down Scroll
|
||||
=== result 80x24
|
||||
RetroBIOS: Finished with errors
|
||||
|
||||
Vendored
+2
-2
@@ -927,7 +927,7 @@
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py
|
||||
1-4 of 8
|
||||
1-8 of 17
|
||||
Enter Exit Up/Down Scroll
|
||||
=== result 40x15
|
||||
RetroBIOS: Finished with errors
|
||||
@@ -943,7 +943,7 @@
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
1-5 of 8
|
||||
1-11 of 17
|
||||
Enter Exit Up/Down Scroll
|
||||
=== result 80x24
|
||||
RetroBIOS: Finished with errors
|
||||
|
||||
Vendored
+2
-2
@@ -981,7 +981,7 @@
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py
|
||||
1-4 of 8
|
||||
1-8 of 17
|
||||
A Exit Up/Down Scroll
|
||||
=== result 40x15
|
||||
RetroBIOS: Finished with errors
|
||||
@@ -997,7 +997,7 @@
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
1-5 of 8
|
||||
1-11 of 17
|
||||
A Exit Up/Down Scroll
|
||||
=== result 80x24
|
||||
RetroBIOS: Finished with errors
|
||||
|
||||
Vendored
+2
-2
@@ -981,7 +981,7 @@
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py
|
||||
1-4 of 8
|
||||
1-8 of 17
|
||||
Start Exit Up/Down Scroll
|
||||
=== result 40x15
|
||||
RetroBIOS: Finished with errors
|
||||
@@ -997,7 +997,7 @@
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
1-5 of 8
|
||||
1-11 of 17
|
||||
Start Exit Up/Down Scroll
|
||||
=== result 80x24
|
||||
RetroBIOS: Finished with errors
|
||||
|
||||
@@ -5167,6 +5167,8 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree):
|
||||
{"event": "declared", "platform": "retroarch", "files": 4, "present": 1,
|
||||
"missing": 2, "wrong_hash": 1, "bytes_needed": 245,
|
||||
"dests": ["a.bin", "b.bin", "c.bin", "d.bin"]},
|
||||
# No data tree in this list.
|
||||
{"event": "data", "platform": "retroarch", "files": 0, "bytes": 0},
|
||||
]
|
||||
|
||||
SKIPPED = [{"reason": "not_found", "label": "not found", "files": 1, "required": 1}]
|
||||
|
||||
@@ -473,6 +473,29 @@ class FromPackInstallsOffline(Scratch):
|
||||
for rel, data in tree.items():
|
||||
self.assertEqual((self.dest / "dolphin-emu" / rel).read_bytes(), data)
|
||||
|
||||
def test_the_data_trees_say_what_they_weigh(self):
|
||||
"""tui_audit_4 P3-10.8: the plan read "and 2636 data files" with no
|
||||
size. The data event carries the bytes check's data_missing counts."""
|
||||
tree = {"Sys/GC/font.bin": b"font bytes", "Sys/Wii/setting.txt": b"setting"}
|
||||
listed = manifest()
|
||||
listed["data_directories"] = [{
|
||||
"ref": "dolphin-sys", "dest": "dolphin-emu", "type": "zip",
|
||||
"url": "https://example.invalid/dolphin.zip", "systems": ["nintendo-gba"],
|
||||
"files": {rel: {"sha1": _sha1(data), "size": len(data)}
|
||||
for rel, data in tree.items()},
|
||||
}]
|
||||
self.list.write_text(json.dumps(listed))
|
||||
self.place("dolphin-emu/Sys/GC/font.bin", tree["Sys/GC/font.bin"])
|
||||
proc = self.run_installer("--check", "--events", "--manifest", str(self.list),
|
||||
"--from-pack", str(self.pack))
|
||||
events = [json.loads(line) for line in proc.stdout.splitlines()]
|
||||
kinds = [event["event"] for event in events]
|
||||
self.assertEqual(kinds.index("data"), kinds.index("declared") + 1)
|
||||
data = events[kinds.index("data")]
|
||||
self.assertEqual(data, {"event": "data", "platform": "retroarch", "files": 1,
|
||||
"bytes": len(tree["Sys/Wii/setting.txt"])})
|
||||
self.assertEqual(events[kinds.index("check")]["data_missing"], 1)
|
||||
|
||||
def test_a_cut_download_is_named(self):
|
||||
self.pack.write_bytes(self.pack.read_bytes()[:100])
|
||||
proc = self.run_installer("--from-pack", str(self.pack))
|
||||
|
||||
+366
-9
@@ -444,13 +444,12 @@ class Flow(unittest.TestCase):
|
||||
self.assertEqual(install.tui_rows(state)[state.focus["systems"]].action, "system")
|
||||
press(state, "n")
|
||||
self.assertEqual(state.ticked, set())
|
||||
self.assertEqual(press(state, "enter"), [])
|
||||
self.assertIn("No system is ticked", state.status)
|
||||
focus = next(i for i, row in enumerate(install.tui_rows(state))
|
||||
if row.value == "nintendo-gba")
|
||||
state.focus["systems"] = focus
|
||||
press(state, "right")
|
||||
# Nothing ticked: Enter takes the system in focus alone.
|
||||
self.assertEqual(press(state, "enter"), [("run", "confirm")])
|
||||
self.assertEqual(state.ticked, {"nintendo-gba"})
|
||||
self.assertEqual(state.screen, "confirm")
|
||||
self.assertIn("--system=nintendo-gba", install.tui_argv(state, "confirm", "py", "i.py"))
|
||||
self.assertIn("--dry-run", install.tui_argv(state, "confirm", "py", "i.py"))
|
||||
@@ -946,6 +945,12 @@ def golden_states() -> dict:
|
||||
class Views(unittest.TestCase):
|
||||
"""Every screen as text, at the sizes a handheld and a desktop show."""
|
||||
|
||||
def setUp(self):
|
||||
# The card screen lists the cards mounted here: none, on every machine.
|
||||
patcher = mock.patch.object(install, "_mounted_cards", return_value=[])
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def test_goldens(self):
|
||||
update = os.environ.get("RETROBIOS_UPDATE_GOLDENS") == "1"
|
||||
GOLDEN.mkdir(parents=True, exist_ok=True)
|
||||
@@ -1121,19 +1126,20 @@ class PadRoutes(unittest.TestCase):
|
||||
self.assertIn(words, muxterm)
|
||||
self.assertNotIn("Start+Select", muxterm)
|
||||
# gptokeyb2 owns the pad inside muxterm, which keeps PgUp and PgDn
|
||||
# for its scrollback: the gptokeyb buttons, and no page key.
|
||||
# for its scrollback: the gptokeyb buttons, Left and Right to page.
|
||||
in_muxterm = help_text("gptokeyb_muxterm", "preview")
|
||||
for words in ("A, Start, Enter", "Select, then Start"):
|
||||
self.assertIn(words, in_muxterm)
|
||||
self.assertNotIn("L1/R1", in_muxterm)
|
||||
self.assertIn("L1/R1", help_text("gptokeyb", "preview"))
|
||||
# muxterm keeps PgUp and PgDn for its scrollback, and L1 and R1
|
||||
# send nothing: no paging is offered there, on a list or a pager.
|
||||
# send nothing: the D-pad's Left and Right page there instead.
|
||||
for screen in ("systems", "result", "plan"):
|
||||
with self.subTest(screen=screen):
|
||||
text = help_text("muxterm", screen)
|
||||
for words in ("L1", "R1", "PgUp", "PgDn", "Page"):
|
||||
for words in ("L1", "R1", "PgUp", "PgDn"):
|
||||
self.assertNotIn(words, text)
|
||||
self.assertIn("D-pad Left/Right Page", help_text("muxterm", "result"))
|
||||
miyoo = help_text("miyoo", "systems")
|
||||
for words in ("Start, Enter", "A, Space, Right/Left", "R2, Esc, Backspace",
|
||||
"L2, Tab, o", "Sends no key here"):
|
||||
@@ -1998,8 +2004,10 @@ class CopyFromAnOldCard(unittest.TestCase):
|
||||
self.recheck(state, [found("scph5500.bin", str(SYSTEM_DIR),
|
||||
"nested folder: bios/scph5500.bin")])
|
||||
text = " ".join(line.strip() for line in install.tui_view(state, 120, 40))
|
||||
# tui_audit_4 P3-10.13: a fix copies, and the screen says the user's
|
||||
# copy stays.
|
||||
self.assertIn("1 file is in the wrong place, it will be copied where the emulators "
|
||||
"read it.", text)
|
||||
"read it, your copy left as it is.", text)
|
||||
row = next(i for i, r in enumerate(install.tui_rows(state)) if r.action == "misplaced")
|
||||
state.focus["plan"] = row
|
||||
press(state, "enter")
|
||||
@@ -2009,6 +2017,19 @@ class CopyFromAnOldCard(unittest.TestCase):
|
||||
press(state, "esc")
|
||||
self.assertEqual(state.screen, "plan")
|
||||
|
||||
def test_the_result_says_the_misplaced_copies_stay(self):
|
||||
state = checked_state()
|
||||
state.stack.append("result")
|
||||
state.code = 0
|
||||
state.summary = {"event": "summary", "mode": "install", "installed": 2,
|
||||
"up_to_date": 0, "pending": 0, "failed": [], "skipped": [],
|
||||
"left_out": [], "kept": [], "interrupted": False}
|
||||
state.local_dests = {"scph5500.bin": (str(SYSTEM_DIR), "nested folder: bios/a")}
|
||||
state.finished = [("scph5500.bin", True, ""), ("gba_bios.bin", True, "")]
|
||||
text = " ".join(" ".join(install.tui_outcome(state)[1]).split())
|
||||
self.assertIn("1 file fixed from copies already in the BIOS folder, 1 downloaded. "
|
||||
"Your copies at the wrong place were left as they are.", text)
|
||||
|
||||
def test_a_local_copy_is_no_download_speed(self):
|
||||
state = checked_state()
|
||||
press(state, "enter")
|
||||
@@ -2764,6 +2785,341 @@ class TheSaveFolderHasARow(unittest.TestCase):
|
||||
self.assertNotIn("savedir", [row.action for row in install._option_rows(checked_state())])
|
||||
|
||||
|
||||
class ACardSystemNamedOnAComputer(unittest.TestCase):
|
||||
"""`--platform onion` on a PC wrote RetroArch's files into the PC's own
|
||||
~/.config/retroarch/system, which Onion never reads: the system's list
|
||||
is RetroArch's, and so was the folder detection found for it."""
|
||||
|
||||
ONION = {".tmp_update/onionVersion/version.txt": "4.3.1\n"}
|
||||
|
||||
def card(self, files: dict) -> Path:
|
||||
root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, root, True)
|
||||
for name, text in files.items():
|
||||
(root / name).parent.mkdir(parents=True, exist_ok=True)
|
||||
(root / name).write_text(text, encoding="utf-8")
|
||||
return root
|
||||
|
||||
def test_the_systems_that_run_from_a_card(self):
|
||||
self.assertEqual(install.card_bound("onion", "retroarch"), ("Onion", "BIOS"))
|
||||
self.assertEqual(install.card_bound("muOS", "retroarch"), ("muOS", "MUOS/bios"))
|
||||
self.assertEqual(install.card_bound("spruceos", "retroarch"), ("spruceOS", "BIOS"))
|
||||
self.assertEqual(install.card_bound("knulli", "knulli"), ("KNULLI", "bios"))
|
||||
self.assertEqual(install.card_bound("nextui", "nextui"), ("NextUI", ""))
|
||||
self.assertIsNone(install.card_bound("retroarch", "retroarch"))
|
||||
self.assertIsNone(install.card_bound("batocera", "batocera"))
|
||||
|
||||
def test_without_questions_the_run_names_the_card_and_writes_nothing(self):
|
||||
home = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, home, True)
|
||||
system = home / ".config" / "retroarch" / "system"
|
||||
system.mkdir(parents=True)
|
||||
(home / ".config" / "retroarch" / "retroarch.cfg").write_text(
|
||||
f'system_directory = "{system}"\n', encoding="utf-8")
|
||||
with FakeRepository() as repo:
|
||||
proc = install_run("--platform", "onion", "--yes",
|
||||
env={**repo.env(), "HOME": str(home)})
|
||||
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
|
||||
self.assertIn("Onion keeps its BIOS files on its SD card, not on this machine.",
|
||||
proc.stderr)
|
||||
self.assertIn("--platform onion --dest /media/$USER/<card>", proc.stderr)
|
||||
self.assertEqual(list(system.iterdir()), [])
|
||||
|
||||
def test_one_mounted_card_of_that_system_is_taken(self):
|
||||
onion, other = self.card(self.ONION), self.card({"roms/a": ""})
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[other, onion]), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
dest = install.card_install_dest("onion", "retroarch", interactive=False)
|
||||
self.assertEqual(dest, onion / "BIOS")
|
||||
self.assertIn(f"Onion card found at {onion}", out.getvalue())
|
||||
|
||||
def test_at_a_terminal_the_card_root_is_asked(self):
|
||||
onion = self.card(self.ONION)
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
|
||||
mock.patch.object(install, "_ask", return_value=str(onion)) as ask, \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
dest = install.card_install_dest("onion", "retroarch", interactive=True)
|
||||
self.assertEqual(dest, onion / "BIOS")
|
||||
self.assertIn("Mount the card, then type its root folder.", out.getvalue())
|
||||
ask.assert_called_once()
|
||||
knulli = self.card({"roms/a": ""})
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
|
||||
mock.patch.object(install, "_ask", return_value=str(knulli)), \
|
||||
contextlib.redirect_stdout(io.StringIO()):
|
||||
self.assertEqual(install.card_install_dest("knulli", "knulli", True),
|
||||
knulli / "bios")
|
||||
|
||||
def test_the_interface_opens_the_card_screen_with_the_cards_first(self):
|
||||
blank = self.card({"roms/a": ""})
|
||||
(blank / "BIOS").mkdir()
|
||||
found = [("retroarch", SYSTEM_DIR)]
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[blank]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=found), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]):
|
||||
state, effects = install.tui_start(_args(platform="onion"), "linux")
|
||||
self.assertEqual((state.screen, effects), ("folder", []))
|
||||
rows = install.tui_rows(state)
|
||||
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
|
||||
effects = install.tui_update(state, Key("enter"))
|
||||
self.assertEqual([row.action for row in rows], ["card", "browse", "type"])
|
||||
self.assertIn("Where is the SD card?", text)
|
||||
self.assertIn("Onion keeps its BIOS files on its SD card", text)
|
||||
self.assertEqual(effects, [("fetch", "retroarch")])
|
||||
self.assertEqual(state.dest, blank / "BIOS")
|
||||
|
||||
def test_the_interface_takes_the_one_onion_card_mounted(self):
|
||||
onion = self.card(self.ONION)
|
||||
(onion / "BIOS").mkdir()
|
||||
found = [("retroarch", SYSTEM_DIR)]
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[onion]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=found), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]):
|
||||
state, effects = install.tui_start(_args(platform="onion"), "linux")
|
||||
self.assertEqual(effects, [("fetch", "retroarch")])
|
||||
self.assertEqual(state.dest, onion / "BIOS")
|
||||
|
||||
def test_on_the_device_its_own_folder_is_used(self):
|
||||
device = Path("/mnt/SDCARD/BIOS")
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
|
||||
mock.patch.object(install, "detect_platforms",
|
||||
return_value=[("retroarch", device)]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]), \
|
||||
mock.patch.dict(install._DETECTED_OS, {("retroarch", device): "Onion"}), \
|
||||
mock.patch.object(install.Path, "exists", return_value=True):
|
||||
state, effects = install.tui_start(_args(platform="onion"), "linux")
|
||||
self.assertEqual(effects, [("fetch", "retroarch")])
|
||||
self.assertEqual(state.dest, device)
|
||||
|
||||
|
||||
class TheSetupFilterKnowsDevices(unittest.TestCase):
|
||||
"""tui_audit_4 P3-10.5: the filter found nothing for steam deck, r36s,
|
||||
trimui or retroid, and miyoo found RetroArch with muOS, JELOS... beside it."""
|
||||
|
||||
TYPED = {
|
||||
"steam deck": {"retroarch", "batocera", "emudeck", "retrodeck"},
|
||||
"r36s": {"retroarch", "rocknix"},
|
||||
"rg35xx": {"retroarch", "rocknix", "knulli"},
|
||||
"trimui": {"retroarch", "knulli", "minui", "nextui"},
|
||||
"retroid": {"retroarch", "rocknix", "knulli"},
|
||||
"miyoo": {"retroarch", "knulli", "minui", "myminui"},
|
||||
"anbernic": {"retroarch", "rocknix", "amberelec", "knulli", "minui", "myminui"},
|
||||
"odin": {"retroarch", "rocknix"},
|
||||
"ayn": {"retroarch", "rocknix"},
|
||||
"rp5": {"retroarch", "rocknix", "knulli"},
|
||||
"rog ally": {"retroarch", "emudeck", "retrobat", "bizhawk"},
|
||||
"onion": {"retroarch"},
|
||||
}
|
||||
|
||||
def rows(self, needle: str) -> list:
|
||||
state = fresh_state()
|
||||
state.stack = ["platforms"]
|
||||
state.filters["platforms"] = needle
|
||||
return [row for row in install.tui_rows(state) if row.action == "platform"]
|
||||
|
||||
def test_each_device_finds_every_setup_it_runs(self):
|
||||
for needle, setups in self.TYPED.items():
|
||||
with self.subTest(needle=needle):
|
||||
self.assertEqual({row.value for row in self.rows(needle)}, setups)
|
||||
|
||||
def test_the_right_column_names_what_matched(self):
|
||||
retroarch = {row.value: row for row in self.rows("miyoo")}["retroarch"]
|
||||
self.assertEqual(retroarch.right.split(", "),
|
||||
["spruceOS", "Onion", "Allium", "Koriki", "MiyooCFW"])
|
||||
deck = {row.value: row for row in self.rows("steam deck")}
|
||||
self.assertEqual(deck["retroarch"].right, "Steam Deck")
|
||||
self.assertIn("muOS", {row.value: row for row in self.rows("")}["retroarch"].right)
|
||||
|
||||
def test_the_handheld_lists_are_the_guides(self):
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
self.skipTest("pyyaml is not installed")
|
||||
|
||||
guides = yaml.safe_load((REPO_ROOT / "platforms" / "_guides.yml").read_text())
|
||||
for system in guides["handheld_guide"]["systems"]:
|
||||
if system.get("devices"):
|
||||
with self.subTest(system=system["name"]):
|
||||
self.assertEqual(install._SETUP_DEVICES[system["name"]],
|
||||
tuple(str(name) for name in system["devices"]))
|
||||
|
||||
|
||||
class OneSystemInFewPresses(unittest.TestCase):
|
||||
"""tui_audit_4 P3-10.11: one system with a controller took 15 presses on
|
||||
the fixture list, about 60 on muxterm, which has no page key."""
|
||||
|
||||
# The buttons each route sends, as the keys the interface reads.
|
||||
PADS = {
|
||||
"gptokeyb": {"A": "enter", "X": " ", "Start": "enter", "R1": "pgdn"},
|
||||
"muxterm": {"A": "enter", "X": " ", "Start": "enter", "R1": "right"},
|
||||
}
|
||||
|
||||
def long_list(self, pad: str) -> install.TuiState:
|
||||
state = checked_state()
|
||||
state.pad = pad
|
||||
state.cols, state.rows = 40, 15
|
||||
state.systems = [{"event": "system", "platform": "retroarch", "system": f"sys-{n:02d}",
|
||||
"name": f"System {n:02d}", "files": 1, "present": 0, "missing": 1,
|
||||
"wrong_hash": 0, "bytes_needed": 10} for n in range(66)]
|
||||
state.ticked = {row["system"] for row in state.systems}
|
||||
state.focus["plan"] = next(i for i, row in enumerate(install.tui_rows(state))
|
||||
if row.action == "install")
|
||||
return state
|
||||
|
||||
def press_button(self, state, pad: str, button: str) -> list:
|
||||
name = self.PADS[pad].get(button, button)
|
||||
return install.tui_update(state, Key("char", name) if len(name) == 1 else Key(name))
|
||||
|
||||
def go_to(self, state, pad: str, target: str, paging: bool) -> int:
|
||||
"""Presses from the focus to the target system, as a person does."""
|
||||
count = 0
|
||||
while True:
|
||||
rows = install.tui_rows(state)
|
||||
focus = state.focus.get("systems", 0)
|
||||
index = next(i for i, row in enumerate(rows) if row.value == target)
|
||||
if index == focus:
|
||||
return count
|
||||
page = max(1, state.rows - 6)
|
||||
button = "R1" if paging and index - focus >= page else "down"
|
||||
self.press_button(state, pad, button)
|
||||
count += 1
|
||||
|
||||
def presses(self, pad: str, fast: bool) -> int:
|
||||
"""From the plan to the run that installs sys-45 alone, with the
|
||||
setup's A before it."""
|
||||
state = self.long_list(pad)
|
||||
count = 1
|
||||
for button in ("down", "A"):
|
||||
self.press_button(state, pad, button)
|
||||
count += 1
|
||||
self.assertEqual(state.screen, "systems")
|
||||
paging = pad != "muxterm" or fast
|
||||
if fast:
|
||||
self.assertEqual(install.tui_rows(state)[state.focus["systems"]].action, "none")
|
||||
self.press_button(state, pad, "A")
|
||||
count += 1
|
||||
count += self.go_to(state, pad, "sys-45", paging)
|
||||
effects = self.press_button(state, pad, "A")
|
||||
count += 1
|
||||
else:
|
||||
state.focus["systems"] = 3
|
||||
for button in ("up", "up", "A"):
|
||||
self.press_button(state, pad, button)
|
||||
count += 1
|
||||
count += self.go_to(state, pad, "sys-45", paging)
|
||||
self.press_button(state, pad, "X")
|
||||
effects = self.press_button(state, pad, "Start")
|
||||
count += 2
|
||||
self.assertEqual(state.ticked, {"sys-45"})
|
||||
self.assertEqual(effects, [("run", "confirm")])
|
||||
# Install on the confirm screen, then Exit on the result.
|
||||
return count + 2
|
||||
|
||||
def test_presses_before_and_after(self):
|
||||
# The audit's path, A Down A Up Up A R1 x5 X Start A A. Before the
|
||||
# fast path it took 17 presses here, 57 on muxterm (None kept the
|
||||
# focus, and muxterm had no page key).
|
||||
self.assertEqual(self.presses("gptokeyb", fast=False), 15)
|
||||
self.assertEqual(self.presses("gptokeyb", fast=True), 12)
|
||||
self.assertEqual(self.presses("muxterm", fast=False), 55)
|
||||
self.assertEqual(self.presses("muxterm", fast=True), 12)
|
||||
|
||||
def test_a_on_a_system_with_none_ticked_takes_it_alone(self):
|
||||
state = self.long_list("keys")
|
||||
state.stack.append("systems")
|
||||
state.ticked = set()
|
||||
state.focus["systems"] = 5
|
||||
text = install.tui_view(state, 80, 24)[-1]
|
||||
self.assertIn("Enter Only this system", text)
|
||||
focused = install.tui_rows(state)[5].value
|
||||
self.assertEqual(press(state, "enter"), [("run", "confirm")])
|
||||
self.assertEqual(state.ticked, {focused})
|
||||
|
||||
def test_enter_still_continues_with_the_ticks_as_they_are(self):
|
||||
state = self.long_list("keys")
|
||||
state.stack.append("systems")
|
||||
state.focus["systems"] = 5
|
||||
self.assertEqual(press(state, "enter"), [("run", "install")])
|
||||
self.assertEqual(len(state.ticked), 66)
|
||||
|
||||
def test_left_and_right_page_inside_muxterm_only(self):
|
||||
for pad in ("muxterm", "gptokeyb_muxterm", "gptokeyb"):
|
||||
with self.subTest(pad=pad):
|
||||
state = self.long_list(pad)
|
||||
state.stack.append("systems")
|
||||
state.focus["systems"] = 3
|
||||
press(state, "right")
|
||||
paged = pad != "gptokeyb"
|
||||
self.assertEqual(state.focus["systems"] > 4, paged)
|
||||
self.assertEqual(len(state.ticked), 66)
|
||||
state.help = True
|
||||
help_text = " ".join(install.tui_view(state, 80, 24))
|
||||
self.assertEqual("D-pad Left/Right" in help_text, paged)
|
||||
|
||||
def test_a_short_list_opens_on_its_first_system(self):
|
||||
state = checked_state()
|
||||
state.focus["plan"] = next(i for i, row in enumerate(install.tui_rows(state))
|
||||
if row.action == "systems")
|
||||
press(state, "enter")
|
||||
self.assertEqual(install.tui_rows(state)[state.focus["systems"]].action, "system")
|
||||
|
||||
|
||||
class DataFilesHaveASize(unittest.TestCase):
|
||||
"""tui_audit_4 P3-10.8: "8179 files to download, 6.6 GB, and 2636 data
|
||||
files", the data files with no size."""
|
||||
|
||||
def test_the_plan_gives_the_data_files_size(self):
|
||||
state = fresh_state()
|
||||
state.stack = ["setup"]
|
||||
with mock.patch.object(install.Path, "exists", return_value=True):
|
||||
install.tui_update(state, Key("enter"))
|
||||
install.tui_receive(state, ("manifest", "retroarch", MANIFEST))
|
||||
check = {**CHECK, "data_missing": 2636}
|
||||
data = {"event": "data", "platform": "retroarch", "files": 2636,
|
||||
"bytes": 412 * 1024 ** 2}
|
||||
for event in [*FILES, *SYSTEMS, check, data]:
|
||||
install.tui_receive(state, ("event", state.run_id, event))
|
||||
with mock.patch.object(install, "_tui_folder_facts", return_value={
|
||||
"free": 50 * 1024 ** 3, "writable": True, "cores": []}):
|
||||
install.tui_receive(state, ("ended", state.run_id, 1, []))
|
||||
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
|
||||
self.assertIn("and 2636 data files (412.0 MB)", text)
|
||||
|
||||
|
||||
class ThePagerCountsLines(unittest.TestCase):
|
||||
"""tui_audit_4 P3-9.4: at 40x12 the result read "1-2 of 5" over eight
|
||||
lines, counting paragraphs, and its last paragraph was cut."""
|
||||
|
||||
def result_state(self, cols: int, rows: int) -> install.TuiState:
|
||||
state = checked_state()
|
||||
state.stack.append("result")
|
||||
state.code, state.summary = 0, {"installed": 3}
|
||||
state.dest = Path("/media/" + "long-folder-name/" * 12)
|
||||
state.cols, state.rows = cols, rows
|
||||
return state
|
||||
|
||||
def test_the_last_line_is_reachable_at_every_size(self):
|
||||
for cols, rows in (*SIZES, (50, 13), (60, 14)):
|
||||
with self.subTest(size=f"{cols}x{rows}"):
|
||||
state = self.result_state(cols, rows)
|
||||
width = min(cols, install.TUI_MAX_WIDTH) - 2
|
||||
wrapped = install._text_lines(install._pager_items(state), width)
|
||||
press(state, "end")
|
||||
screen = install.tui_view(state, cols, rows)
|
||||
body = [line.rstrip() for line in screen[2:-1]]
|
||||
self.assertIn(" " + str(wrapped[-1]).rstrip(), body)
|
||||
if len(wrapped) > rows - 3:
|
||||
self.assertTrue(any(re.fullmatch(rf" \d+-{len(wrapped)} of {len(wrapped)}",
|
||||
line) for line in body), body)
|
||||
|
||||
def test_the_counter_moves_one_line_per_press(self):
|
||||
state = self.result_state(40, 12)
|
||||
first = [line for line in install.tui_view(state, 40, 12) if " of " in line][-1]
|
||||
self.assertRegex(first, r"^ 1-8 of \d+")
|
||||
press(state, "down")
|
||||
second = [line for line in install.tui_view(state, 40, 12) if " of " in line][-1]
|
||||
self.assertRegex(second, r"^ 2-9 of \d+")
|
||||
|
||||
|
||||
class OfflineInTheInterface(unittest.TestCase):
|
||||
"""--offline in the full screen: a "No connection" error offering a Try
|
||||
again that can never work, and a plan counting downloads the run
|
||||
@@ -3743,8 +4099,9 @@ class EndToEnd(unittest.TestCase):
|
||||
term.wait("> Install")
|
||||
term.send(DOWN, "\r")
|
||||
term.wait("Choose systems")
|
||||
# D-pad and A only: up to None, A, down to the second system, Right.
|
||||
term.send(UP, UP, "\r", DOWN, DOWN, DOWN, RIGHT)
|
||||
# D-pad and A only: up to None, A (on the first system), down to the
|
||||
# second system, Right.
|
||||
term.send(UP, UP, "\r", DOWN, RIGHT)
|
||||
screen = term.wait("1 of 2 systems")
|
||||
self.assertIn("> [x] sony-playstation", screen)
|
||||
term.send("\r")
|
||||
|
||||
+8
-6
@@ -131,7 +131,8 @@ how many files are missing, how much they weigh, how many are in place and how
|
||||
much space is free, before anything is written.
|
||||
|
||||
The other choices sit one press away. **Choose systems** ticks the consoles you
|
||||
play, with the total to download under the list. **Options** sets the
|
||||
play, with the total to download under the list. A long list opens on **None**:
|
||||
A there unticks every system, and A on a system then installs that one alone. **Options** sets the
|
||||
preferred regions, game data, the installed cores only, the cores, the parallel
|
||||
downloads, the folder and, for a list with files read from the frontend's save
|
||||
directory, that directory. Each row names the option it sets, and **Show the
|
||||
@@ -1073,8 +1074,8 @@ Start pressed first also chooses the line in focus: hold Select first. A
|
||||
running install is then stopped the way B stops it, and the last screen says
|
||||
what it wrote. On muOS without PortMaster, muxterm reads the pad itself: Y
|
||||
ticks a system, Select shows its keyboard, and Menu quits. L1 and R1 do not
|
||||
page there: muxterm keeps PgUp and PgDn for its own scrollback, so a list
|
||||
moves with the D-pad.
|
||||
page there: muxterm keeps PgUp and PgDn for its own scrollback, so Left and
|
||||
Right on the D-pad page a list instead, as they do with gptokeyb in muxterm.
|
||||
|
||||
Each launcher also sets `RETROBIOS_IDLE_EXIT` to 300: with no button pressed
|
||||
for 5 minutes since it opened, and no check or install running, the interface
|
||||
@@ -1094,8 +1095,8 @@ Each launcher tells the interface which buttons reach it, in
|
||||
| Value | Input | Set by |
|
||||
|-------|-------|--------|
|
||||
| `gptokeyb` | gptokeyb or gptokeyb2 with `retrobios.gptk`, the table above | the PortMaster port and the Tools entry |
|
||||
| `gptokeyb_muxterm` | the same, inside muxterm, which keeps L1 and R1 for its scrollback: no button pages | 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 |
|
||||
| `gptokeyb_muxterm` | the same, inside muxterm, which keeps L1 and R1 for its scrollback: Left and Right page | the muOS app with PortMaster |
|
||||
| `muxterm` | muxterm's own keys: D-pad (Left and Right page), 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, Start as Enter, L1 and R1 as E and T, L2 as Tab, R2 as Backspace | no launcher yet |
|
||||
|
||||
@@ -1332,6 +1333,7 @@ other schemas under `api/v1/schemas/`.
|
||||
| `system` | `platform`, `system`, `name`, `files`, `present`, `missing`, `wrong_hash`, `bytes_needed` | Each system, sorted by id, and with `--system` only the systems named. Files with no system count under `other`. `name` is the platform's own name for it |
|
||||
| `check` | `platform`, `name`, `bios_dir`, `files`, `present`, `missing`, `wrong_hash`, `bytes_needed`, `data_missing`, `not_installable` | The platform's totals. `data_missing` counts data-tree files to fetch, `not_installable` the files the collection cannot supply |
|
||||
| `declared` | `platform`, `files`, `present`, `missing`, `wrong_hash`, `bytes_needed`, `dests` | Right after `check`, the same counts over the files the platform's own list names (`cores: null`), and their `dests` in the order of the `file` events. A setup that checks its own list reads its answer here, with or without `--declared-only` |
|
||||
| `data` | `platform`, `files`, `bytes` | Right after `declared`: the data-tree files to fetch (the `data_missing` of `check`) and the bytes they write once in place |
|
||||
| `found` | `platform`, `dest`, `folder`, `path`, `member`, `reason` | Each file to install that this machine already holds, after `check` and before anything is written, in `--check` and `--dry-run` too. `folder` is the `--from` folder as given, or the `bios_dir` of the check when the BIOS folder holds the file at another place, and then `reason` says where in words (`nested folder: ...` for a misplaced file, `the same file is at ...` for one the list installs there too). `path` is the file, and `member` the ZIP member holding the bytes when `path` is an archive, an empty string otherwise. Its copy is reported by a `downloaded` event |
|
||||
| `unreadable` | `platform`, `folder`, `path`, `member`, `reason` | Each file in a `--from` folder that could not be read and may be one the run still needs, right after the `found` events: its size is that of a file no copy was found for, or unknown (a folder that could not be listed, a ZIP that would not open). `member` names the ZIP member, or is empty. `reason` says why in words (`Permission denied`, `it is encrypted, a password is needed`) |
|
||||
| `saves` | `platform`, `files`, `folder` | After `detect` and before `checking`, when the list holds files read from the frontend's save directory and that directory is known: how many, and the `folder` they go to, where `<system>` and `<core>` stand for one folder per system or per core. When it is not known, the files are counted in the summary's `skipped` instead (`no_save_directory`) |
|
||||
@@ -1343,7 +1345,7 @@ other schemas under `api/v1/schemas/`.
|
||||
| `app` | `system`, `folder`, `ok`, `reason`, `dests`, `written`, `kept` | After `summary`, with `--add-app` or a yes to its question: the RetroBIOS app on the card of `system` whose root is `folder`. `dests` lists the app's files below that root, `written` the ones this run wrote (empty when each was in place), `kept` the backup folders as in `summary`. `ok` is false, with `reason`, when the card takes no app (before anything is written, code 2), when the install did not finish, or when a file could not be fetched, verified or written (code 1) |
|
||||
| `end` | `code` | Last line, with the exit status the process returns (see [Exit status](#exit-status)) |
|
||||
|
||||
Schema 1 can gain a kind (`found` came with `--from`, `declared` with `--declared-only`, `unsearched` with the search that goes on past a closed folder, `app` with `--add-app`), never a key of an
|
||||
Schema 1 can gain a kind (`found` came with `--from`, `declared` with `--declared-only`, `unsearched` with the search that goes on past a closed folder, `app` with `--add-app`, `data` with the size of the data trees), never a key of an
|
||||
existing kind: a reader skips a kind it does not know. An option refused
|
||||
before the run starts (`install.py: error:`, code 2) writes
|
||||
no event at all. A program reads a missing `end` as that case, or as a
|
||||
|
||||
Reference in new issue
Block a user