feat: suggest systems by short name

--system gba, psx or ps1 was refused with no suggestion on RetroArch.
Short names now suggest the ids they stand for, never taken in their
place, and two aliases of one system name it once. --platform muos and
the other systems --list-platforms names stand for the file list they
get, and the run says which.
This commit is contained in:
Abdessamad Derraz committed 2026-10-23 13:55:09 +02:00
1 parent 41839b7784
commit e041fac351
6 files changed
+104 -10

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "853300c169f08b2554a421aac33da1b5004d3eedd70ae9dec1d732766d06b1ca"
$defaultInstallSha256 = "50afca134ab44a140421e3da43ab3a74fbbb35ba7c524adf688b8af5909dcc38"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+66 -6
View File
@@ -2236,15 +2236,58 @@ def _name_key(name: str) -> str:
SHORT_LIST = 12
# Names people type for a system, each with its id. Only ever suggested:
# --system gba, psx and ps1 were refused with no hint on RetroArch, whose
# list names no system that way, and taking one in silence could install
# another console than the one meant.
SYSTEM_SHORT_NAMES = {
**dict.fromkeys(("psx", "ps1", "ps", "playstation1"), "sony-playstation"),
"ps2": "sony-playstation-2", "ps3": "sony-playstation-3", "psp": "sony-psp",
**dict.fromkeys(("vita", "psvita"), "sony-playstation-vita"),
"gb": "nintendo-gb", "gbc": "nintendo-gbc", "gba": "nintendo-gba",
**dict.fromkeys(("nes", "famicom", "fc"), "nintendo-nes"), "fds": "nintendo-fds",
**dict.fromkeys(("snes", "sfc", "superfamicom"), "nintendo-snes"),
"n64": "nintendo-64", "n64dd": "nintendo-64dd", "sgb": "nintendo-sgb",
**dict.fromkeys(("nds", "ds"), "nintendo-ds"), "dsi": "nintendo-dsi",
"3ds": "nintendo-3ds", "wiiu": "nintendo-wii-u", "switch": "nintendo-switch",
**dict.fromkeys(("gc", "ngc", "gcn", "gamecube"), "nintendo-gamecube"),
"pokemini": "nintendo-pokemon-mini",
**dict.fromkeys(("md", "gen", "genesis", "megadrive"), "sega-mega-drive"),
**dict.fromkeys(("scd", "segacd", "megacd"), "sega-mega-cd"),
"32x": "sega-32x", **dict.fromkeys(("sms", "mastersystem"), "sega-master-system"),
**dict.fromkeys(("gg", "gamegear"), "sega-game-gear"),
**dict.fromkeys(("ss", "saturn"), "sega-saturn"),
**dict.fromkeys(("dc", "dreamcast"), "sega-dreamcast"),
**dict.fromkeys(("naomi", "atomiswave"), "sega-dreamcast-arcade"),
**dict.fromkeys(("pce", "tg16", "turbografx", "turbografx16", "pcengine",
"supergrafx"), "nec-pc-engine"),
**dict.fromkeys(("pcecd", "tgcd", "turbografxcd"), "nec-pc-engine-cd"),
"pcfx": "nec-pc-fx",
**dict.fromkeys(("mvs", "aes"), "snk-neogeo"),
**dict.fromkeys(("neocd", "neogeocd"), "snk-neogeo-cd"),
**dict.fromkeys(("msx", "msx2"), "microsoft-msx"),
"c64": "commodore-c64", "c128": "commodore-c128",
**dict.fromkeys(("amiga", "cd32"), "commodore-amiga"),
"x68k": "sharp-x68000", **dict.fromkeys(("jag", "jaguar"), "atari-jaguar"),
**dict.fromkeys(("st", "atarist"), "atari-st"), "a800": "atari-400-800",
**dict.fromkeys(("coleco", "colecovision"), "coleco-colecovision"),
**dict.fromkeys(("intv", "intellivision"), "mattel-intellivision"),
"o2": "magnavox-odyssey2", "channelf": "fairchild-channel-f",
**dict.fromkeys(("zx", "spectrum", "zxspectrum"), "sinclair-zx-spectrum"),
"cpc": "amstrad-cpc", "bbc": "acorn-bbc-micro", "apple2": "apple-ii",
}
def did_you_mean(
typed: list[str], known: list[str], listing: str,
aliases: dict[str, str] | None = None,
aliases: dict[str, str] | None = None, inside_from: int = 4,
) -> str:
"""The closest known names to what was typed, and the option listing them all.
A typo was answered with every name the platform has, up to 400 on one
line, with no hint that playstation is sony-playstation. An alias close
to what was typed suggests the name it stands for.
to what was typed suggests the name it stands for. A name holding what
was typed, inside_from letters or more, is suggested too.
"""
import difflib # noqa: PLC0415
@@ -2259,9 +2302,12 @@ def did_you_mean(
if not key:
continue
matches = difflib.get_close_matches(key, list(by_key), n=3, cutoff=0.6)
if len(key) >= 4:
if len(key) >= inside_from:
matches += sorted(k for k in by_key if key in k and k not in matches)
close += [by_key[k] for k in matches if by_key[k] not in close]
for k in matches:
# Two aliases of one system (psx, ps1) name it once.
if by_key[k] not in close:
close.append(by_key[k])
if close:
return f"Did you mean: {', '.join(close[:3])}? {listing} lists all {len(known)}."
if len(known) <= SHORT_LIST:
@@ -2279,6 +2325,13 @@ def normalize_platform(name: str) -> str:
for plat in AVAILABLE_PLATFORMS:
if key and key in (_name_key(plat), _name_key(display_name(plat))):
return plat
# --list-platforms prints `muOS: retroarch`, and --platform muos was
# refused: a system named there stands for the list it gets.
for rule in _EMBEDDED_RULES:
if key and rule.manifest and key == _name_key(rule.name):
print(f" {rule.name} gets the {display_name(rule.manifest)} file list: "
f"--platform {rule.manifest}.")
return rule.manifest
print(f"Unknown platform '{name.strip()}'. "
+ did_you_mean([name], list(AVAILABLE_PLATFORMS), "--list-platforms"),
file=sys.stderr)
@@ -3423,9 +3476,15 @@ def resolve_choices(
raise ValueError(f"unknown {label}: {', '.join(unknown)}. This list records none.")
listing = {"system": "--list-systems", "emulator": "--list-emulators"}.get(
label, "--list-cores")
hints, inside_from = aliases, 4
if label == "system":
# gba is the end of nintendo-gba: three letters are enough here.
hints = {**{short: name for short, name in SYSTEM_SHORT_NAMES.items()
if name in known}, **aliases}
inside_from = 3
raise ValueError(
f"unknown {label}: {', '.join(unknown)}. "
f"{did_you_mean(unknown, known, listing, aliases)}"
f"{did_you_mean(unknown, known, listing, hints, inside_from)}"
)
if values and not chosen:
raise ValueError(f"--{label} needs at least one name")
@@ -7139,7 +7198,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
print("Available platforms:")
for p in AVAILABLE_PLATFORMS:
print(f" {p}")
print("\nSystems recognized by their own files:")
print("\nSystems recognized by their own files, and the file list each gets "
"(--platform takes either name):")
for line in _system_lines():
print(f" {line}")
detected = detect_platforms(os_type)
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="853300c169f08b2554a421aac33da1b5004d3eedd70ae9dec1d732766d06b1ca"
DEFAULT_INSTALL_SHA256="50afca134ab44a140421e3da43ab3a74fbbb35ba7c524adf688b8af5909dcc38"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+15
View File
@@ -3759,6 +3759,21 @@ class ATypoGetsTheClosestName(unittest.TestCase):
self.assertEqual(install.normalize_platform("RetroArch "), "retroarch")
self.assertEqual(install.normalize_platform("RetroDECK"), "retrodeck")
def test_a_system_name_stands_for_the_list_it_gets(self):
"""--list-platforms printed `muOS: retroarch`, and --platform muos
answered "Unknown platform"."""
for typed, plat in (("muos", "retroarch"), ("muOS", "retroarch"),
("ArkOS", "retroarch"), ("PAN4ELEC", "amberelec"),
("JELOS", "retroarch")):
out = io.StringIO()
with self.subTest(typed=typed), contextlib.redirect_stdout(out):
self.assertEqual(install.normalize_platform(typed), plat)
self.assertIn(f"--platform {plat}", out.getvalue())
def test_a_system_that_gets_no_list_is_still_refused(self):
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
install.normalize_platform("TrimUI")
def test_a_platform_typo_is_answered_with_one_name(self):
err = io.StringIO()
with contextlib.redirect_stderr(err), self.assertRaises(SystemExit) as raised:
+19
View File
@@ -13,6 +13,7 @@ import functools
import hashlib
import http.server
import importlib.util
import io
import json
import os
import random
@@ -311,6 +312,24 @@ class CommandLineChoices(unittest.TestCase):
self.assertIn("sega-saturn", str(ctx.exception))
self.assertIn("nintendo-gba", str(ctx.exception))
def test_a_short_name_is_suggested_never_taken(self):
"""`--system gba`, `psx` and `ps1` were refused on RetroArch with no
suggestion, though nintendo-gba holds the very letters."""
known = [f"system-{i}" for i in range(40)] + [
"nintendo-gb", "nintendo-gba", "nintendo-gbc", "sony-playstation",
"sony-playstation-2", "sega-mega-drive"]
for typed, meant in (("gba", "nintendo-gba"), ("psx", "sony-playstation"),
("ps1", "sony-playstation"), ("PS2", "sony-playstation-2"),
("genesis", "sega-mega-drive"), ("gb", "nintendo-gb")):
with self.subTest(typed=typed), self.assertRaises(ValueError) as ctx:
install.resolve_choices([typed], known, "system")
self.assertIn(f"Did you mean: {meant}", str(ctx.exception))
def test_a_short_name_for_a_system_the_list_lacks_is_not_suggested(self):
with self.assertRaises(ValueError) as ctx:
install.resolve_choices(["psx"], [f"system-{i}" for i in range(40)], "system")
self.assertNotIn("sony-playstation", str(ctx.exception))
def test_an_empty_list_is_refused(self):
with self.assertRaises(ValueError):
install.resolve_regions([" , "])
+2 -2
View File
@@ -493,14 +493,14 @@ what the options name, everything by default.
| Option | Effect |
|--------|--------|
| `--platform NAME` | Install for this platform instead of the detected one. Unknown names are refused with the available list |
| `--platform NAME` | Install for this platform instead of the detected one. A system `--list-platforms` names under its own files (`muos`, `arkos`) stands for the file list it gets, and the run says which. Unknown names are refused with the closest ones |
| `--dest PATH` | Destination directory, overriding detection. Without `--platform`, the file list is the platform detected here: at a terminal the installer asks, Enter taking the one found, and otherwise it uses the one found, RetroArch's when nothing is found, and refuses when several are. The run prints whose list it is. A folder that does not exist is named with the nearest one that does, then created. When more than its last level is missing (a mistyped path), a run at a terminal asks first, and the answer defaults to no |
| `--target NAME` | Keep only the files the cores of that hardware target need, and the systems they emulate. An unknown target is refused rather than ignored, since carrying on would install everything. On Batocera, Recalbox, ROCKNIX and Lakka the board is read from the system (`batocera.arch`, `recalbox.arch`, `HW_DEVICE`, `LIBREELEC_DEVICE` or `LIBREELEC_PROJECT`) and applied when no `--target` is given |
| `--every-board` | Install the files of every board instead of the one the system names, for a card that moves between devices |
| `--installed-cores` | Keep only what the cores already installed read (RetroArch, Lakka, RetroPie). See [Installed cores](#installed-cores) |
| `--cores-dir PATH` | The folder RetroArch loads its cores from, for `--installed-cores` when it is not found. Implies `--installed-cores` |
| `--save-dir PATH` | The frontend's save directory (RetroArch's `savefile_directory`), for the files some cores read there, when the run cannot find it. The core's own folder is still added below it when the frontend sorts saves by core |
| `--system NAMES` | Install only these systems. Comma-separated or repeated. `--list-systems` prints the names, `other` included |
| `--system NAMES` | Install only these systems. Comma-separated or repeated. `--list-systems` prints the names, `other` included. A short name the platform does not use (`gba`, `psx`) is refused with the ids it may stand for, never taken in their place |
| `--core NAMES` | Keep the platform's own list and only the extra files these cores load. `--list-cores` prints the names |
| `--emulator NAMES` | Install what these emulators read on the detected platform, and nothing else: their own files and the platform's files of the systems they run. Each emulator page on the site gives this command. `--list-emulators` prints the names, buildbot names such as `mednafen_psx` included |
| `--region LIST` | Preferred regions, best first (`us,eu,jp`): keep one BIOS per region where a system has several. Accepts the names and aliases of `generate_pack.py --region` |