mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
chore: merge the card setup fixes
This commit is contained in:
commit
d6cad0c2e8
19 files changed
+928
-207
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "000b7e9e965f6d81c254d7ee7967077f9bbae8aca2286179f31d4f4f7b555d06"
|
||||
$defaultInstallSha256 = "b7eec15ba746bbaaaea471396964a83a255a4a1c4bd51316fbdb0ff62a065169"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+376
-97
@@ -2617,11 +2617,12 @@ def did_you_mean(
|
||||
return f"{listing} lists all {len(known)}."
|
||||
|
||||
|
||||
def normalize_platform(name: str) -> str:
|
||||
def normalize_platform(name: str, say: bool = True) -> str:
|
||||
"""Return the canonical platform id for user-supplied input.
|
||||
|
||||
The site writes MiSTer FPGA and RetroArch: letters and digits decide,
|
||||
as they do for verify.py and generate_pack.py.
|
||||
as they do for verify.py and generate_pack.py. say is False where the
|
||||
name is read again later, which says it once.
|
||||
"""
|
||||
key = _name_key(name)
|
||||
for plat in AVAILABLE_PLATFORMS:
|
||||
@@ -2631,8 +2632,9 @@ def normalize_platform(name: str) -> str:
|
||||
# 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}.")
|
||||
if say:
|
||||
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"),
|
||||
@@ -4218,7 +4220,7 @@ def place_save_entries(
|
||||
if place.by_core:
|
||||
where += f"{os.sep}<core>"
|
||||
print(f" {_plural(len(saved), 'file')} read from {display}'s save directory "
|
||||
f"go to {where}")
|
||||
f"{'goes' if len(saved) == 1 else 'go'} to {where}")
|
||||
EVENTS.emit("saves", platform=str(manifest.get("platform", "")),
|
||||
files=len(saved), folder=where)
|
||||
return files, []
|
||||
@@ -5401,7 +5403,7 @@ def _report_outcome(
|
||||
print(f" ... and {len(failures) - 10} more")
|
||||
if declined and not downloaded and not errors and not interrupted:
|
||||
print(f"\nNothing installed: {_plural(declined, 'file')} not downloaded, "
|
||||
"as answered.")
|
||||
+ ("which --offline does not do." if _offline_only() else "as answered."))
|
||||
print(f"{_plural(up_to_date, 'file')} already up to date at {_locations(paths)}.")
|
||||
return
|
||||
kept = _kept_lines()
|
||||
@@ -6275,10 +6277,16 @@ def _choose_platform(title: str, default: str = "") -> str:
|
||||
"or q to quit.")
|
||||
|
||||
|
||||
def _prompt_manual_platform(os_type: str) -> list[tuple[str, Path]]:
|
||||
"""Ask for a platform and a BIOS directory when nothing was detected."""
|
||||
def _prompt_manual_platform(os_type: str, ask: bool = True) -> list[tuple[str, Path]]:
|
||||
"""Ask for a platform and a BIOS directory when nothing was detected.
|
||||
|
||||
A system that runs from its card gets the card's root asked for, as
|
||||
--platform rocknix does: /storage/roms/bios was offered on a PC.
|
||||
"""
|
||||
plat = _choose_platform("No supported platform found here. Choose the one to "
|
||||
"install for:", "retroarch")
|
||||
if card_bound(plat, plat) is not None:
|
||||
return [(plat, card_install_dest(plat, plat, True, ask))]
|
||||
default_dest = _default_dest(os_type, plat)
|
||||
raw = ""
|
||||
while not raw:
|
||||
@@ -6504,7 +6512,7 @@ INSTALLED_ONLY = "installed"
|
||||
def _prompt_custom_selection(
|
||||
plat: str, files: list[dict], omitted: list[dict],
|
||||
installed: tuple[list[dict], list[dict]] | None = None,
|
||||
name: str = "", dry_run: bool = False,
|
||||
name: str = "", dry_run: bool = False, data_trees: list[dict] | None = None,
|
||||
) -> tuple[list[str], list[str], list[str]] | str | None:
|
||||
"""Offer to narrow the install by system, core and region.
|
||||
|
||||
@@ -6513,7 +6521,15 @@ def _prompt_custom_selection(
|
||||
installed read) holds, or the three choices. Each empty list keeps
|
||||
everything on its axis.
|
||||
"""
|
||||
print(f"\n{name or display_name(plat)}: {_sized(files)}.")
|
||||
trees = data_trees or []
|
||||
data = sum(len(entry.get("files") or {}) for entry in trees)
|
||||
data_size = sum(meta.get("size", 0) for entry in trees
|
||||
for meta in (entry.get("files") or {}).values())
|
||||
print(f"\n{name or display_name(plat)}: {_sized(files)}"
|
||||
+ (f", and {_plural(data, 'data file')} ({format_size(data_size)})" if data else "")
|
||||
+ ".")
|
||||
if _offline_only():
|
||||
print(" --offline: only the files found on this machine are installed.")
|
||||
# A dry run installs nothing: "install everything" was asked before it.
|
||||
print(" Enter) list everything, nothing is written" if dry_run
|
||||
else " Enter) install everything")
|
||||
@@ -8226,7 +8242,7 @@ def _check_copy_lines(
|
||||
if fixed:
|
||||
verb = "is" if fixed == 1 else "are"
|
||||
them = "it" if fixed == 1 else "them"
|
||||
lines.append(f" {fixed} of these files {verb} in the BIOS folder at the wrong place: "
|
||||
lines.append(f" {fixed} of these files {verb} in the BIOS folder in the wrong place: "
|
||||
f"a run without --check copies {them} where the cores open {them}, and "
|
||||
f"leaves your {'copy' if fixed == 1 else 'copies'} as "
|
||||
f"{'it is' if fixed == 1 else 'they are'}.")
|
||||
@@ -9257,7 +9273,7 @@ def _open_local_sources(args: argparse.Namespace) -> None:
|
||||
+ (f", {parts} parts" if parts > 1 or _PART_FILE.match(pack.files[0].name) else ""))
|
||||
spelled = pack.platforms()
|
||||
if args.platform is not None:
|
||||
plat = normalize_platform(args.platform)
|
||||
plat = normalize_platform(args.platform, say=False)
|
||||
if spelled and plat not in spelled:
|
||||
print(f"Error: {pack.name} is a pack for "
|
||||
f"{' and '.join(display_name(n) for n in spelled)}, not "
|
||||
@@ -9495,18 +9511,31 @@ def _ends_with(path: Path, relative: str) -> bool:
|
||||
return len(parts) >= len(tail) and parts[len(parts) - len(tail):] == tail
|
||||
|
||||
|
||||
def card_dest(named: str, plat: str, dest: Path) -> tuple[str, Path]:
|
||||
"""The file list and folder for a --dest that names a handheld's card.
|
||||
class CardChoice(NamedTuple):
|
||||
"""Where the files of a folder named for a card go, and what is said: a
|
||||
note, or why the folder is not the card of the system named."""
|
||||
|
||||
plat: str
|
||||
folder: Path
|
||||
note: str = ""
|
||||
refused: str = ""
|
||||
# The system's folder below its card, and the file that names the card.
|
||||
below: str = ""
|
||||
marker: str = ""
|
||||
|
||||
|
||||
def card_choice(named: str, plat: str, dest: Path) -> CardChoice:
|
||||
"""The file list and folder for a folder that may be a handheld's card.
|
||||
|
||||
The card's root is where the user points, its BIOS folder is below it:
|
||||
`--platform muos --dest /media/me/SD1` wrote to the root, which muOS
|
||||
never reads. A system named on the command line whose card this is not
|
||||
is refused with the folder it reads.
|
||||
never reads. A system named whose card this is not is refused with the
|
||||
folder it reads.
|
||||
"""
|
||||
found = card_system(dest)
|
||||
if found and (not plat or found[0] == plat) and found[1] != dest:
|
||||
print(f" {found[2]} card: {display_name(found[0])} files, in {found[1]}")
|
||||
return found[0], found[1]
|
||||
return CardChoice(found[0], found[1],
|
||||
f"{found[2]} card: {display_name(found[0])} files, in {found[1]}")
|
||||
# Only a system named by its own name: --platform retroarch or knulli
|
||||
# says which list, not which card.
|
||||
key = _name_key(named)
|
||||
@@ -9516,23 +9545,48 @@ def card_dest(named: str, plat: str, dest: Path) -> tuple[str, Path]:
|
||||
if rule is None or found:
|
||||
if len({(match.manifest, match.bios) for match in matched}) > 1:
|
||||
names = " and ".join(sorted({match.name for match in matched}))
|
||||
print(f" {screen_text(str(dest))} holds the files of {names}: installing into "
|
||||
"it as named. Name the card's BIOS folder with --dest to write there.")
|
||||
return plat, dest
|
||||
return CardChoice(plat, dest, f"{screen_text(str(dest))} holds the files of "
|
||||
f"{names}: installing into it as named. Name the card's BIOS "
|
||||
"folder with --dest to write there.")
|
||||
return CardChoice(plat, dest)
|
||||
# Its folder on the card, or one its device reads (/run/muos/storage/bios).
|
||||
own = {device.bios for device in _EMBEDDED_RULES if device.name == rule.name}
|
||||
if dest.as_posix() in own or _ends_with(dest, rule.bios):
|
||||
return plat, dest
|
||||
return CardChoice(plat, dest)
|
||||
if any(match.name == rule.name for match in matched):
|
||||
# A card two systems claim, the one named among them.
|
||||
print(f" {rule.name} card: {display_name(plat)} files, in {dest / rule.bios}")
|
||||
return plat, dest / rule.bios
|
||||
return CardChoice(plat, dest / rule.bios, f"{rule.name} card: {display_name(plat)} "
|
||||
f"files, in {dest / rule.bios}")
|
||||
marker = rule.values[0].split("/")[0]
|
||||
print(f"Error: {dest} is not a {rule.name} card: it holds no {marker}. "
|
||||
f"{rule.name} reads its BIOS files from {rule.bios} on its card: name the card's "
|
||||
f"root (the folder holding {marker}) or that folder: --dest <card>/{rule.bios}",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return CardChoice(plat, dest, refused=f"{dest} is not a {rule.name} card: it holds no "
|
||||
f"{marker}. {rule.name} reads its BIOS files from {rule.bios} on its "
|
||||
"card", below=rule.bios, marker=marker)
|
||||
|
||||
|
||||
def card_dest(named: str, plat: str, dest: Path) -> tuple[str, Path]:
|
||||
"""The file list and folder for a --dest that names a handheld's card,
|
||||
the run ended on a folder that is not the card of the system named."""
|
||||
choice = card_choice(named, plat, dest)
|
||||
if choice.refused:
|
||||
print(f"Error: {choice.refused}: name the card's root (the folder holding "
|
||||
f"{choice.marker}) or that folder: --dest <card>/{choice.below}",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if choice.note:
|
||||
print(f" {choice.note}")
|
||||
return choice.plat, choice.folder
|
||||
|
||||
|
||||
def card_root_above(dest: Path) -> tuple[str, Path] | None:
|
||||
"""(platform, root) of the MinUI-family card dest is inside, when dest is
|
||||
a folder below its root and not the root."""
|
||||
if card_platform(dest) is not None:
|
||||
return None
|
||||
for parent in list(dest.absolute().parents)[:2]:
|
||||
holder = card_platform(parent)
|
||||
if holder:
|
||||
return holder, parent
|
||||
return None
|
||||
|
||||
|
||||
def _check_card_root(plat: str, dest: Path) -> None:
|
||||
@@ -9541,16 +9595,13 @@ def _check_card_root(plat: str, dest: Path) -> None:
|
||||
The file list writes Bios/<TAG>/ below --dest: given the card's Bios
|
||||
folder, every file would land in Bios/Bios/<TAG>/, which no core reads.
|
||||
"""
|
||||
above = card_root_above(dest)
|
||||
if above:
|
||||
print(f"Error: {dest} is inside a {display_name(above[0])} card. "
|
||||
f"Name the card's root: --dest {above[1]}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
found = card_platform(dest)
|
||||
if found is None:
|
||||
for parent in list(dest.absolute().parents)[:2]:
|
||||
holder = card_platform(parent)
|
||||
if holder:
|
||||
print(f"Error: {dest} is inside a {display_name(holder)} card. "
|
||||
f"Name the card's root: --dest {parent}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return
|
||||
if found != plat:
|
||||
if found is not None and found != plat:
|
||||
print(f" Note: this card holds {display_name(found)} ({CARD_MARKER}), and the "
|
||||
f"{display_name(plat)} file list was asked for.")
|
||||
|
||||
@@ -9579,13 +9630,50 @@ def mounted_cards_of(system: str, plat: str) -> list[Path]:
|
||||
if any(rule.name == system for rule in _card_matches(mount))]
|
||||
|
||||
|
||||
def card_install_dest(named: str, plat: str, interactive: bool) -> Path:
|
||||
def card_root_folder(named: str, plat: str, root: Path) -> CardChoice:
|
||||
"""The BIOS folder for a root chosen as the card of a card-bound system,
|
||||
or why root is not that card.
|
||||
|
||||
A typed root was taken as it was: a typo four folders deep was created
|
||||
and filled, a menu answer (8) became a relative card, and a folder that
|
||||
was no Onion card ended the run. Each is refused here, with its reason,
|
||||
for the question to be asked again.
|
||||
"""
|
||||
system, below = card_bound(named, plat) or (display_name(plat), "")
|
||||
# The card is mounted, its BIOS folder may not be made yet.
|
||||
card = (root.parents[len(PurePosixPath(below).parts) - 1]
|
||||
if below and _ends_with(root, below) else root)
|
||||
if not _is_dir(card):
|
||||
return CardChoice(plat, root, refused=f"{card} does not exist, or is not a folder")
|
||||
if plat in CARD_PLATFORMS:
|
||||
above = card_root_above(root)
|
||||
if above:
|
||||
return CardChoice(plat, root, refused=f"{root} is inside a "
|
||||
f"{display_name(above[0])} card, whose root is {above[1]}")
|
||||
found = card_platform(root)
|
||||
if found is None:
|
||||
return CardChoice(plat, root, refused=f"{root} holds no {CARD_MARKER}: it is "
|
||||
f"not a {system} card")
|
||||
note = ("" if found == plat else f"Note: this card holds {display_name(found)} "
|
||||
f"({CARD_MARKER}), and the {display_name(plat)} file list was asked for.")
|
||||
return CardChoice(plat, root, note)
|
||||
choice = card_choice(named, plat, root)
|
||||
if choice.refused or choice.folder != root or not below or _ends_with(root, below):
|
||||
return choice
|
||||
# A KNULLI share or a ROCKNIX card read as a list's name: its folder
|
||||
# below the root, as the device reads it.
|
||||
return choice._replace(folder=root / below)
|
||||
|
||||
|
||||
def card_install_dest(named: str, plat: str, interactive: bool, ask: bool = True) -> 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.
|
||||
asked for, never this machine's own folder, and asked again until it is
|
||||
that card. Without questions the run stops on the --dest that names the
|
||||
card. ask is whether creating several missing folders is asked first,
|
||||
as it is for --dest.
|
||||
"""
|
||||
system, below = card_bound(named, plat) or (display_name(plat), "")
|
||||
cards = mounted_cards_of(system, plat)
|
||||
@@ -9606,21 +9694,27 @@ def card_install_dest(named: str, plat: str, interactive: bool) -> Path:
|
||||
f"Mount the card and name its root with --dest, for example: {example}",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
# The drives the interface lists, each with what its own files say.
|
||||
mounts = _mounted_cards()
|
||||
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:
|
||||
"Mount the card, then type its root folder"
|
||||
+ (" or its number:" if mounts else "."))
|
||||
for index, mount in enumerate(mounts, 1):
|
||||
print(f" {index}) {mount} ({_card_what(mount)})")
|
||||
while True:
|
||||
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
|
||||
if not raw:
|
||||
continue
|
||||
root = (mounts[int(raw) - 1] if raw.isdigit() and 1 <= int(raw) <= len(mounts)
|
||||
else _typed_folder(raw))
|
||||
choice = card_root_folder(named, plat, root)
|
||||
if choice.refused:
|
||||
print(f" {choice.refused}. Type the root of the {system} card.")
|
||||
continue
|
||||
if choice.note:
|
||||
print(f" {choice.note}")
|
||||
_missing_dest(choice.folder, ask, "typed")
|
||||
return choice.folder
|
||||
|
||||
|
||||
def _platform_for_dest(os_type: str, dest: Path, interactive: bool) -> str:
|
||||
@@ -9759,8 +9853,9 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
elif bound is not None:
|
||||
# Its files go on its SD card, which is never this machine's
|
||||
# own folder.
|
||||
platforms = [(args.platform, card_install_dest(named, args.platform,
|
||||
interactive))]
|
||||
platforms = [(args.platform, card_install_dest(
|
||||
named, args.platform, interactive,
|
||||
not (args.check or args.dry_run or args.yes)))]
|
||||
elif args.platform in PORTABLE_ONLY:
|
||||
# A portable emulator reads the folder beside its executable:
|
||||
# any default would be a folder it never opens.
|
||||
@@ -9828,9 +9923,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
for line in _manual_usage_hint(os_type, asked):
|
||||
print(line)
|
||||
sys.exit(1)
|
||||
platforms = _prompt_manual_platform(os_type)
|
||||
platforms = _prompt_manual_platform(
|
||||
os_type, not (args.check or args.dry_run or args.yes))
|
||||
plat, typed = platforms[0]
|
||||
if typed != _default_dest(os_type, plat):
|
||||
# A card's root was asked for, and its folders checked, there.
|
||||
if typed != _default_dest(os_type, plat) and card_bound(plat, plat) is None:
|
||||
_missing_dest(typed, not (args.check or args.dry_run or args.yes),
|
||||
"typed")
|
||||
else:
|
||||
@@ -10100,6 +10197,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
selection = _prompt_custom_selection(
|
||||
plat_name, files, omitted_files, installed,
|
||||
display_name(plat_name, manifest), args.dry_run,
|
||||
manifest.get("data_directories") or [],
|
||||
)
|
||||
if selection is None:
|
||||
smaller = (" Nothing downloaded. To install less, run the same command "
|
||||
@@ -10298,9 +10396,25 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
continue
|
||||
|
||||
if to_download:
|
||||
dl_size = sum(f.get("size", 0) for f in to_download)
|
||||
fetched = [f for f in to_download if id(f) not in copies]
|
||||
if interactive and not args.yes and _offline_only() and fetched:
|
||||
# The screen offers no Install for these: the question asked
|
||||
# to download 6.6 GB that --offline never fetches.
|
||||
print(f"\n {_plural(len(fetched), 'file')} "
|
||||
f"{'needs' if len(fetched) == 1 else 'need'} the internet, which "
|
||||
"--offline does not use. Copy them from your old card or a folder "
|
||||
"(--from), or run the installer again with a pack (--from-pack) or "
|
||||
"without --offline.")
|
||||
# Not done: the summary says they were not downloaded.
|
||||
files_pending += len(fetched)
|
||||
total_declined += len(fetched)
|
||||
to_download = [f for f in to_download if id(f) in copies]
|
||||
fetched = []
|
||||
dl_size = sum(f.get("size", 0) for f in to_download)
|
||||
fetch_size = _bytes(fetched)
|
||||
if not to_download:
|
||||
total_up_to_date += len(up_to_date)
|
||||
continue
|
||||
if (
|
||||
interactive
|
||||
and not args.yes
|
||||
@@ -10729,6 +10843,21 @@ def _split_word(word: str, room: int) -> list[str]:
|
||||
return pieces + [word] if word else pieces
|
||||
|
||||
|
||||
_UNIT = re.compile(r"(B|KB|MB|GB)\b")
|
||||
|
||||
|
||||
def _glued_words(text: str) -> list[str]:
|
||||
"""The words of text, a size kept with its unit: "5.1" / "KB" was
|
||||
broken across two lines of a 40-column headline."""
|
||||
words: list[str] = []
|
||||
for word in text.split():
|
||||
if words and _UNIT.match(word) and re.fullmatch(r"\(?\d+(\.\d+)?", words[-1]):
|
||||
words[-1] += f" {word}"
|
||||
else:
|
||||
words.append(word)
|
||||
return words
|
||||
|
||||
|
||||
def _wrap(text: str, width: int, indent: str = " ") -> list[str]:
|
||||
"""Lines of at most width cells, the ones after the first indented."""
|
||||
if cell_width(text) <= width:
|
||||
@@ -10736,7 +10865,7 @@ def _wrap(text: str, width: int, indent: str = " ") -> list[str]:
|
||||
lead = text[:len(text) - len(text.lstrip(" "))]
|
||||
lines: list[str] = []
|
||||
line = ""
|
||||
for word in text.split():
|
||||
for word in _glued_words(text):
|
||||
for piece in _split_word(word, width - len(lead + indent)):
|
||||
prefix = lead + indent if lines else lead
|
||||
longer = f"{line} {piece}" if line else piece
|
||||
@@ -10795,7 +10924,9 @@ class TuiState:
|
||||
self.status: str = ""
|
||||
self.cols: int = 80
|
||||
self.rows: int = 24
|
||||
self.now: float = 0.0
|
||||
# A check started before the loop's first tick counted the
|
||||
# machine's uptime.
|
||||
self.now: float = time.monotonic()
|
||||
# (platform, folder, label) for each setup found here.
|
||||
self.setups: list = []
|
||||
self.notes: list = []
|
||||
@@ -10814,6 +10945,8 @@ class TuiState:
|
||||
# (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
|
||||
# The mounted cards of that system, as last looked for.
|
||||
self.cards: list | None = None
|
||||
# (width and text, lines) of the last pager wrapped.
|
||||
self.pager_cache: tuple | None = None
|
||||
self.path_text: str = ""
|
||||
@@ -10873,6 +11006,9 @@ class TuiState:
|
||||
self.must_confirm: bool = False
|
||||
# Choices, each one an option of the command line.
|
||||
self.ticked: set = set()
|
||||
# The ticks the systems list opened with: Back with none ticked
|
||||
# brings them back, nothing having been chosen.
|
||||
self.ticked_before: set | None = None
|
||||
self.cli_systems: list = []
|
||||
self.regions: list = []
|
||||
self.cli_regions: list = []
|
||||
@@ -11220,18 +11356,41 @@ def tui_rows(state: TuiState) -> list[TuiRow]:
|
||||
rows = [_filter_row(state)]
|
||||
for plat in AVAILABLE_PLATFORMS:
|
||||
named = _setup_matches(plat, systems.get(plat, []), needle)
|
||||
if named is not None:
|
||||
if named is None:
|
||||
continue
|
||||
# A system that runs from its SD card, named or run by a device
|
||||
# typed, is a row of its own that asks for the card: "onion" on
|
||||
# RetroArch's row led to this computer's RetroArch folder.
|
||||
cards = [name for name in named
|
||||
if name in systems.get(plat, []) and card_bound(name, plat)]
|
||||
others = [name for name in named if name not in cards]
|
||||
if not named or others:
|
||||
# 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))
|
||||
", ".join(others or systems.get(plat, [])), plat))
|
||||
for name in cards:
|
||||
devices = [device for device in _SETUP_DEVICES.get(name, ())
|
||||
if _matches(device, _device_needle(needle))]
|
||||
rows.append(TuiRow("cardsystem", f"{name}, SD card", ", ".join(devices)
|
||||
or f"{display_name(plat)} files", (plat, name)))
|
||||
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")]
|
||||
bound = _tui_card_bound(state)
|
||||
if not bound:
|
||||
return rows + [TuiRow("browse", "Browse for a folder"),
|
||||
TuiRow("type", "Type a path")]
|
||||
# Only the cards whose own files say they run the system: A on the
|
||||
# first mount wrote Onion's layout on a muOS card.
|
||||
mounts = (state.cards if state.cards is not None
|
||||
else mounted_cards_of(bound[0], state.platform))
|
||||
cards = [TuiRow("card", f"{_mount_name(mount)} ({_card_what(mount)})", value=mount)
|
||||
for mount in mounts]
|
||||
rows += cards + [TuiRow("browse", "Browse for a folder"), TuiRow("type", "Type a path")]
|
||||
# On the device itself its folder is found, and no card is looked for.
|
||||
here = state.folder_default is not None and state.folder_kind == "found"
|
||||
return rows + ([] if cards or here else [TuiRow("rescan", "Look again")])
|
||||
if screen == "browse":
|
||||
return list(state.browse_rows)
|
||||
if screen == "create":
|
||||
@@ -11261,6 +11420,9 @@ def tui_rows(state: TuiState) -> list[TuiRow]:
|
||||
if screen == "confirm":
|
||||
if state.running or state.dry is None:
|
||||
return []
|
||||
if _offline_only() and not _dry_copies(state):
|
||||
# The plan's rule: Install would only fail every file.
|
||||
return [TuiRow("back", "Back")]
|
||||
return [TuiRow("install", "Install"), TuiRow("back", "Back")]
|
||||
if screen == "error":
|
||||
rows = ([TuiRow("retry", "Try again")]
|
||||
@@ -11315,6 +11477,13 @@ def _plan_copies(state: TuiState, check: dict) -> list[str]:
|
||||
return [dest for dest in local if dest in sizes]
|
||||
|
||||
|
||||
def _dry_copies(state: TuiState) -> list[str]:
|
||||
"""The files the confirm or preview run would install that this
|
||||
machine holds."""
|
||||
local = {event["dest"] for event in state.found}
|
||||
return [event["dest"] for event in state.dry_files if event["dest"] in local]
|
||||
|
||||
|
||||
def _short_of_space(state: TuiState, check: dict) -> bool:
|
||||
free = state.facts.get("free")
|
||||
return free is not None and check.get("bytes_needed", 0) > free
|
||||
@@ -11452,6 +11621,10 @@ def _back(state: TuiState) -> list[tuple]:
|
||||
state.stack.append("quit")
|
||||
state.focus["quit"] = 0
|
||||
return effects
|
||||
if state.screen == "systems" and not state.ticked and state.ticked_before:
|
||||
# Enter on None, then Esc: the plan kept its headline over an
|
||||
# Install the run refused.
|
||||
state.ticked = set(state.ticked_before)
|
||||
state.stack.pop()
|
||||
if state.screen == "checking" and not state.running:
|
||||
# Stay after Esc on a check started from the command line: a
|
||||
@@ -11566,9 +11739,54 @@ def _choose(state: TuiState, plat: str, dest: Path) -> list[tuple]:
|
||||
return _start_check(state)
|
||||
|
||||
|
||||
def _card_folder(state: TuiState, folder: Path) -> CardChoice | None:
|
||||
"""What a folder chosen for a setup that runs from its SD card resolves
|
||||
to, as a card root typed in line mode does: None when no card is asked
|
||||
for, or when the folder is inside a MinUI card, whose root _choose
|
||||
offers."""
|
||||
bound = _tui_card_bound(state)
|
||||
if bound is None or (state.platform in CARD_PLATFORMS and card_root_above(folder)):
|
||||
return None
|
||||
return card_root_folder(bound[0], state.platform, folder)
|
||||
|
||||
|
||||
def _card_refusal(state: TuiState, folder: Path) -> str:
|
||||
"""Why folder is not the card the chosen setup runs from, or ""."""
|
||||
choice = _card_folder(state, folder)
|
||||
if choice is None or not choice.refused:
|
||||
return ""
|
||||
return f"{choice.refused}. Choose the root of the {_tui_card_bound(state)[0]} card."
|
||||
|
||||
|
||||
def _choose_card(state: TuiState, folder: Path) -> list[tuple]:
|
||||
"""_choose for a folder picked on the folder, browse or path screen, or
|
||||
given with --dest: for a setup that runs from its SD card, the card's
|
||||
own BIOS folder, and a folder that is not its card refused in words."""
|
||||
choice = _card_folder(state, folder)
|
||||
if choice is None:
|
||||
return _choose(state, state.platform, folder)
|
||||
if choice.refused:
|
||||
state.status = _card_refusal(state, folder)
|
||||
if state.stack and state.screen in ("folder", "browse", "path", "platforms"):
|
||||
return []
|
||||
state.folder_default, state.folder_kind = None, "usual"
|
||||
return _push(state, "folder")
|
||||
card = card_system(folder)
|
||||
if card and card[1] == choice.folder:
|
||||
# Titled as the same card found by itself: "Onion (RetroArch files)".
|
||||
_DETECTED_OS.setdefault((choice.plat, choice.folder), card[2])
|
||||
return _choose(state, choice.plat, choice.folder)
|
||||
|
||||
|
||||
def _open_folder_screen(state: TuiState, default: Path | None,
|
||||
kind: str = "usual") -> list[tuple]:
|
||||
state.folder_default, state.folder_kind = default, kind
|
||||
# The cards are looked for when the screen opens and on Look again: a
|
||||
# card inserted meanwhile moved the rows under the focus.
|
||||
state.cards = None
|
||||
bound = _tui_card_bound(state)
|
||||
if bound:
|
||||
state.cards = mounted_cards_of(bound[0], state.platform)
|
||||
refused = kind == "chosen" and (state.facts.get("writable") is False
|
||||
or _short_of_space(state, state.check or {}))
|
||||
# A folder just refused as full or read-only: Browse, not Use this
|
||||
@@ -11577,7 +11795,11 @@ def _open_folder_screen(state: TuiState, default: Path | None,
|
||||
|
||||
|
||||
def _mount_name(mount: Path) -> str:
|
||||
"""How a person names a mounted card: E:\\ on Windows, its label elsewhere."""
|
||||
"""How a person names a mounted card: E:\\ on Windows, its label elsewhere.
|
||||
Under WSL /mnt/d is the Windows D: drive, which "d" did not say."""
|
||||
if (_WSL["active"] and mount.parent == Path(_WSL["root"]) and len(mount.name) == 1
|
||||
and mount.name.isalpha()):
|
||||
return f"{mount.name.upper()}:"
|
||||
return mount.name or str(mount)
|
||||
|
||||
|
||||
@@ -11740,14 +11962,22 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
|
||||
if action == "another":
|
||||
return _push(state, "platforms", _platform_focus(state, "retroarch"))
|
||||
plat, path, _label = state.setups[row.value]
|
||||
# A setup found here is its folder: a card looked at and backed
|
||||
# out of described the RetroArch chosen next as that card.
|
||||
state.card_bound = None
|
||||
return _choose(state, plat, path)
|
||||
if screen == "platforms":
|
||||
plat, system = row.value if action == "cardsystem" else (row.value, row.value)
|
||||
state.platform = plat
|
||||
state.card_bound = card_bound(system, plat)
|
||||
if state.dest_given is not None:
|
||||
return _choose(state, row.value, state.dest_given)
|
||||
state.platform = row.value
|
||||
state.card_bound = card_bound(row.value, row.value)
|
||||
return _choose_card(state, state.dest_given)
|
||||
if state.card_bound:
|
||||
return _open_folder_screen(state, None)
|
||||
# On the device itself, the folder detection found there: a
|
||||
# KNULLI was asked for its own card.
|
||||
found = next((path for name, path, _label in state.setups if name == plat
|
||||
and detected_os(name, path) == state.card_bound[0]), None)
|
||||
return _open_folder_screen(state, found, "found" if found else "usual")
|
||||
# The folder detected here before the built-in one: the built-in
|
||||
# default was offered as where RetroArch reads, when the
|
||||
# retroarch.cfg found here names another.
|
||||
@@ -11758,9 +11988,16 @@ 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 in ("card", "rescan"):
|
||||
if action == "rescan":
|
||||
state.cards = mounted_cards_of(_tui_card_bound(state)[0], state.platform)
|
||||
cards = [index for index, item in enumerate(tui_rows(state))
|
||||
if item.action == "card"]
|
||||
state.focus["folder"] = cards[0] if cards else state.focus.get("folder", 0)
|
||||
state.status = ("" if cards else f"Still no {_tui_card_bound(state)[0]} card "
|
||||
"is mounted.")
|
||||
return []
|
||||
return _choose_card(state, row.value)
|
||||
if action == "browse":
|
||||
state.browse_for = "dest"
|
||||
_push(state, "browse")
|
||||
@@ -11784,7 +12021,7 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
|
||||
# The check runs again with it: the plan then says what it holds.
|
||||
return _start_check(state)
|
||||
if action == "use":
|
||||
return _choose(state, state.platform, state.browse_dir)
|
||||
return _choose_card(state, state.browse_dir)
|
||||
if action == "up" and state.browse_dir is not None:
|
||||
if row.value == state.browse_dir:
|
||||
_browse_to(state, None if os.name == "nt" else state.browse_dir)
|
||||
@@ -11817,6 +12054,8 @@ 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":
|
||||
state.ticked_before = set(state.ticked)
|
||||
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)
|
||||
@@ -12147,8 +12386,12 @@ def _path_key(state: TuiState, key: TuiKey) -> list[tuple]:
|
||||
if closed is not None:
|
||||
state.status = _closed_status(state, closed)
|
||||
return []
|
||||
refused = _card_refusal(state, path)
|
||||
if refused:
|
||||
state.status = refused
|
||||
return []
|
||||
state.stack.pop()
|
||||
return _choose(state, state.platform, path)
|
||||
return _choose_card(state, path)
|
||||
return []
|
||||
|
||||
|
||||
@@ -12505,6 +12748,9 @@ def _route_hint(state: TuiState, parts: tuple) -> tuple:
|
||||
named.append(key + sep + what)
|
||||
if state.pad == "muxterm" and state.screen == "path":
|
||||
named.append("Select Keyboard")
|
||||
if state.pad == "miyoo" and state.screen in _CHECKLISTS:
|
||||
tick = [entry for entry in named if entry.startswith("A ")]
|
||||
named = named[:1] + tick + [entry for entry in named[1:] if entry not in tick]
|
||||
return tuple(named)
|
||||
|
||||
|
||||
@@ -12542,6 +12788,7 @@ _ROW_VERBS = {
|
||||
"jobs": "Change", "retry": "Try again", "back": "Back", "stay": "Stay",
|
||||
"fromcard": "Choose", "from": "Add a folder", "misplaced": "Show", "zip": "Copy from it",
|
||||
"unreadable": "Show", "exit": "Exit", "addapp": "Put it on the card",
|
||||
"cardsystem": "Choose", "rescan": "Look again",
|
||||
}
|
||||
|
||||
|
||||
@@ -12773,15 +13020,18 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
|
||||
lines.append("You cannot write to this folder: choose another one.")
|
||||
counts = (f"{check['present']} in place, {check['missing']} missing, "
|
||||
f"{wrong} wrong {'copy' if wrong == 1 else 'copies'}")
|
||||
lines.append(counts)
|
||||
if copied and fetch:
|
||||
# 47 to download above 49 missing and 1 wrong copy: the 3 found on
|
||||
# this machine are the difference, said where the sum is read.
|
||||
counts += (f": {len(copied)} of them {'is' if len(copied) == 1 else 'are'} "
|
||||
f"copied from this machine, {fetch} downloaded")
|
||||
lines.append(counts)
|
||||
saves = (state.saves if check is state.check
|
||||
# this machine are the difference, on a line of its own ("0 wrong
|
||||
# copies: 2 of them" read as two of the zero).
|
||||
lines.append(f"{need} to install: {len(copied)} copied from this machine, {fetch} "
|
||||
+ ("from the pack" if LOCAL.pack is not None else "downloaded"))
|
||||
after_counts = len(lines)
|
||||
saves =(state.saves if check is state.check
|
||||
else state.dry_saves if check is state.dry else None)
|
||||
lines += [_styled(line, "path") for line in _saves_lines(state, saves)]
|
||||
saves_lines = _saves_lines(state, saves)
|
||||
lines += [_styled(line, "path" if index else "") for index, line in enumerate(saves_lines)]
|
||||
found_in = _retroarch_config_at(dest) if dest is not None else None
|
||||
if found_in:
|
||||
lines.append(_styled(f"Named by {_short_path(Path(found_in[0]), state.home)}", "path"))
|
||||
@@ -12798,7 +13048,7 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
|
||||
f"{'one' if wrong == 1 else 'ones'} kept in a backup folder.")
|
||||
misplaced = _tui_misplaced(state) if check is state.check else []
|
||||
if misplaced:
|
||||
lines.insert(3, f"{_plural(len(misplaced), 'file')} "
|
||||
lines.insert(after_counts, f"{_plural(len(misplaced), 'file')} "
|
||||
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 "
|
||||
@@ -12821,7 +13071,7 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
|
||||
lines += _unavailable_lines(state)
|
||||
if check is state.check:
|
||||
lines += _unplaced_lines(state.check_summary, state,
|
||||
"Choose it in Options, Save folder.")
|
||||
"Choose it in Options, Save folder (--save-dir).")
|
||||
return lines[:2] + offline_lines + lines[2:]
|
||||
|
||||
|
||||
@@ -12833,13 +13083,16 @@ def _saves_lines(state: TuiState, saves: dict | None) -> list[str]:
|
||||
return []
|
||||
name = display_name(state.platform) if state.platform else "the frontend"
|
||||
count = saves["files"]
|
||||
# The folder alone is cut in the middle: cut with the sentence, it
|
||||
# lost "directory goes to" even at 80 columns.
|
||||
return [f"{_plural(count, 'file')} read from {name}'s save directory "
|
||||
f"{'goes' if count == 1 else 'go'} to "
|
||||
f"{_short_path(Path(saves['folder']), state.home)}"]
|
||||
f"{'goes' if count == 1 else 'go'} to:",
|
||||
_short_path(Path(saves['folder']), state.home)]
|
||||
|
||||
|
||||
def _unplaced_lines(summary: dict | None, state: TuiState,
|
||||
remedy: str = "Name it with --save-dir.") -> list[str]:
|
||||
remedy: str = "Choose it in Options, Save folder (--save-dir), "
|
||||
"next time.") -> list[str]:
|
||||
"""The files read from the frontend's save directory that the run left
|
||||
out, that directory being unknown: said, never silent."""
|
||||
count = sum(group.get("files", 0) for group in (summary or {}).get("skipped") or []
|
||||
@@ -12889,12 +13142,22 @@ 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)
|
||||
bound = _tui_card_bound(state)
|
||||
if bound:
|
||||
if bound and state.folder_default is not None and state.folder_kind == "found":
|
||||
# On the device itself: its own folder, which is no card to look for.
|
||||
title = "Which folder?"
|
||||
before = [f"{bound[0]} reads its BIOS files from:", str(state.folder_default)]
|
||||
elif bound:
|
||||
title = "Where is the SD card?"
|
||||
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 ".")]
|
||||
if state.folder_default is not None:
|
||||
# Use this folder takes it: named, not left to guess.
|
||||
before += [f"The BIOS folder chosen for {bound[0]}:", str(state.folder_default)]
|
||||
if any(row.action == "rescan" for row in tui_rows(state)):
|
||||
before.append(f"No {bound[0]} card is mounted. Insert it, then choose "
|
||||
"Look again.")
|
||||
elif state.folder_default is not None:
|
||||
title = "Which folder?"
|
||||
lead = {"found": f"{name} reads its BIOS files from:",
|
||||
@@ -13052,8 +13315,7 @@ def _view_systems(state: TuiState, width: int, height: int) -> tuple:
|
||||
footer.insert(0, OTHER_HELP)
|
||||
elif "shared" in focused.right:
|
||||
count = _shared_needed(state)[focused.value][0]
|
||||
footer.insert(0, f"{count} of these files are shared with other systems, "
|
||||
"counted once in the total.")
|
||||
footer.insert(0, f"+{count} shared: counted once")
|
||||
needle = state.filters.get("systems", "")
|
||||
after = [f"No system matches '{needle}'."] if len(rows) == 3 and needle else []
|
||||
if state.status:
|
||||
@@ -13109,7 +13371,9 @@ _STATE_WORDS = {"missing": "missing", "wrong_hash": "wrong copy"}
|
||||
def _pager_items(state: TuiState) -> list:
|
||||
"""The text of a screen that scrolls, the same for its view and its keys."""
|
||||
if state.screen == "misplaced":
|
||||
return [f"{event['dest']}: {event['reason']}" for event in _tui_misplaced(state)]
|
||||
return ["They are copied where the emulators read them. Your copies stay where "
|
||||
"they are.", ""] + [f"{event['dest']}: {event['reason']}"
|
||||
for event in _tui_misplaced(state)]
|
||||
if state.screen == "unreadable":
|
||||
return [f"{event['path']}" + (f", {event['member']}" if event["member"] else "")
|
||||
+ f": {event['reason']}" for event in state.unreadable]
|
||||
@@ -13146,7 +13410,7 @@ def _view_unreadable(state: TuiState, width: int, height: int) -> tuple:
|
||||
|
||||
|
||||
def _view_misplaced(state: TuiState, width: int, height: int) -> tuple:
|
||||
return ("Files in the wrong place", _pager(state, width, height, _pager_items(state)),
|
||||
return ("Files in the wrong place", _pager(state, width, height, _pager_items(state), 2),
|
||||
("A/Enter Back", "Up/Down Scroll"))
|
||||
|
||||
|
||||
@@ -13259,7 +13523,8 @@ def _tui_sources(state: TuiState, installed: int, failed: int) -> str:
|
||||
name = _last_name(folder)
|
||||
folders[name] = folders.get(name, 0) + 1
|
||||
copied = fixed + sum(folders.values())
|
||||
return sources_line(list(folders.items()), fixed, max(0, installed - copied), failed)
|
||||
return sources_line(list(folders.items()), fixed, max(0, installed - copied), failed,
|
||||
"from the pack" if LOCAL.pack is not None else "downloaded")
|
||||
|
||||
|
||||
def _unavailable(state: TuiState) -> tuple[list[dict], int]:
|
||||
@@ -13359,7 +13624,7 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]:
|
||||
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("Your copies in 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)
|
||||
@@ -13524,7 +13789,8 @@ def _help_lines(state: TuiState) -> list[tuple[str, str]]:
|
||||
keys = [(names["move"], "Move")]
|
||||
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")]
|
||||
*([("Home/End, g/G", "First, last")] if state.pad in ("both", "keys")
|
||||
else [])]
|
||||
primary = _primary(state) or _TUI_VIEWS[screen](state, 80, 24)[2][0]
|
||||
ok = names["ok"]
|
||||
if state.pad == "miyoo" and screen in _CHECKLISTS:
|
||||
@@ -13563,6 +13829,9 @@ def _primary(state: TuiState) -> str | None:
|
||||
verb = _ROW_VERBS.get(row.action)
|
||||
if state.screen == "systems" and row.action == "system" and not state.ticked:
|
||||
verb = "Only this system"
|
||||
if state.screen == "systems" and row.action == "none" and not state.ticked:
|
||||
# Nothing left to untick: A goes to the first system.
|
||||
verb = "First system"
|
||||
return f"A/Enter {verb}" if verb else None
|
||||
|
||||
|
||||
@@ -13707,7 +13976,17 @@ def tui_start(
|
||||
if line.strip() and line.strip() not in unsaid]
|
||||
if plat and args.dest is not None:
|
||||
state.stack = []
|
||||
return state, _choose(state, plat, Path(args.dest))
|
||||
state.platform, state.card_bound = plat, card_bound(args.platform, plat)
|
||||
dest = Path(args.dest)
|
||||
if state.card_bound is None or plat in CARD_PLATFORMS:
|
||||
return state, _choose(state, plat, dest)
|
||||
# Read as the line mode reads --dest: a muOS card named for Onion
|
||||
# gets MUOS/bios, a folder that is no Onion card is refused.
|
||||
choice = card_choice(args.platform, plat, dest)
|
||||
if choice.refused:
|
||||
state.status = f"{choice.refused}. Choose the root of the {state.card_bound[0]} card."
|
||||
return state, _open_folder_screen(state, None)
|
||||
return state, _choose(state, choice.plat, choice.folder)
|
||||
if plat:
|
||||
bound = card_bound(args.platform, plat)
|
||||
# A system that runs from its card is its card or the device, never
|
||||
|
||||
+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="000b7e9e965f6d81c254d7ee7967077f9bbae8aca2286179f31d4f4f7b555d06"
|
||||
DEFAULT_INSTALL_SHA256="b7eec15ba746bbaaaea471396964a83a255a4a1c4bd51316fbdb0ff62a065169"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 636292,
|
||||
"sha256": "000b7e9e965f6d81c254d7ee7967077f9bbae8aca2286179f31d4f4f7b555d06",
|
||||
"size": 650926,
|
||||
"sha256": "b7eec15ba746bbaaaea471396964a83a255a4a1c4bd51316fbdb0ff62a065169",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 688841,
|
||||
"sha256": "65c0ff1debcc384c1a6da526e4c92505a1628036cc4c7f51928d5253fbd07484"
|
||||
"size": 703475,
|
||||
"sha256": "1928a1ee174ab57d2890fa7d668a34fa996637669446bb6ad4329fa0fc3d76dc"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 636292,
|
||||
"sha256": "000b7e9e965f6d81c254d7ee7967077f9bbae8aca2286179f31d4f4f7b555d06",
|
||||
"size": 650926,
|
||||
"sha256": "b7eec15ba746bbaaaea471396964a83a255a4a1c4bd51316fbdb0ff62a065169",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 636292,
|
||||
"sha256": "000b7e9e965f6d81c254d7ee7967077f9bbae8aca2286179f31d4f4f7b555d06",
|
||||
"size": 650926,
|
||||
"sha256": "b7eec15ba746bbaaaea471396964a83a255a4a1c4bd51316fbdb0ff62a065169",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
|
||||
NextUI keeps its BIOS files on its SD card. Choose the card's root folder, the one holding
|
||||
.system.
|
||||
No NextUI card is mounted. Insert it, then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -3,10 +3,10 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
A/Enter Browse B/Esc Back ? Help
|
||||
+3
-3
@@ -3,12 +3,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
|
||||
NextUI keeps its BIOS files on its SD card. Choose the card's root folder, the
|
||||
one holding .system.
|
||||
No NextUI card is mounted. Insert it, then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+8
-8
@@ -166,12 +166,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
A Browse B Back
|
||||
=== folder-card 40x15
|
||||
RetroBIOS: Where is the SD card?
|
||||
@@ -179,12 +179,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
@@ -194,11 +194,11 @@
|
||||
|
||||
NextUI keeps its BIOS files on its SD card. Choose the card's root folder, the
|
||||
one holding .system.
|
||||
No NextUI card is mounted. Insert it, then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
|
||||
+8
-8
@@ -166,12 +166,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
A Browse B Back
|
||||
=== folder-card 40x15
|
||||
RetroBIOS: Where is the SD card?
|
||||
@@ -179,12 +179,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
@@ -194,11 +194,11 @@
|
||||
|
||||
NextUI keeps its BIOS files on its SD card. Choose the card's root folder, the
|
||||
one holding .system.
|
||||
No NextUI card is mounted. Insert it, then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
|
||||
+8
-8
@@ -166,12 +166,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
A Browse B Back
|
||||
=== folder-card 40x15
|
||||
RetroBIOS: Where is the SD card?
|
||||
@@ -179,12 +179,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
@@ -194,11 +194,11 @@
|
||||
|
||||
NextUI keeps its BIOS files on its SD card. Choose the card's root folder, the
|
||||
one holding .system.
|
||||
No NextUI card is mounted. Insert it, then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+8
-8
@@ -166,12 +166,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
Enter Browse Esc Back ? Help
|
||||
=== folder-card 40x15
|
||||
RetroBIOS: Where is the SD card?
|
||||
@@ -179,12 +179,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
@@ -194,11 +194,11 @@
|
||||
|
||||
NextUI keeps its BIOS files on its SD card. Choose the card's root folder, the
|
||||
one holding .system.
|
||||
No NextUI card is mounted. Insert it, then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+14
-14
@@ -166,12 +166,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
A Browse R2 Back
|
||||
=== folder-card 40x15
|
||||
RetroBIOS: Where is the SD card?
|
||||
@@ -179,12 +179,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
@@ -194,11 +194,11 @@
|
||||
|
||||
NextUI keeps its BIOS files on its SD card. Choose the card's root folder, the
|
||||
one holding .system.
|
||||
No NextUI card is mounted. Insert it, then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
@@ -550,7 +550,7 @@
|
||||
|
||||
|
||||
2 of 3 systems: 4 files, 1.0 MB
|
||||
Start Continue R2 Back A Tick
|
||||
Start Continue A Tick R2 Back
|
||||
=== systems 40x15
|
||||
RetroBIOS: Choose systems
|
||||
|
||||
@@ -566,7 +566,7 @@
|
||||
|
||||
|
||||
2 of 3 systems: 4 files, 1.0 MB
|
||||
Start Continue R2 Back A Tick
|
||||
Start Continue A Tick R2 Back
|
||||
=== systems 80x24
|
||||
RetroBIOS: Choose systems
|
||||
|
||||
@@ -591,7 +591,7 @@
|
||||
|
||||
|
||||
2 of 3 systems: 4 files, 1.0 MB
|
||||
Start Continue R2 Back A Tick
|
||||
Start Continue A Tick R2 Back
|
||||
=== options 40x12
|
||||
RetroBIOS: Options
|
||||
|
||||
@@ -712,7 +712,7 @@
|
||||
|
||||
|
||||
|
||||
Start Filter R2 Back A Tick
|
||||
Start Filter A Tick R2 Back
|
||||
=== cores 40x15
|
||||
RetroBIOS: Cores
|
||||
|
||||
@@ -728,7 +728,7 @@
|
||||
|
||||
|
||||
|
||||
Start Filter R2 Back A Tick
|
||||
Start Filter A Tick R2 Back
|
||||
=== cores 80x24
|
||||
RetroBIOS: Cores
|
||||
|
||||
@@ -753,7 +753,7 @@
|
||||
|
||||
|
||||
|
||||
Start Filter R2 Back A Tick
|
||||
Start Filter A Tick R2 Back
|
||||
=== preview 40x12
|
||||
RetroBIOS: Preview
|
||||
|
||||
|
||||
Vendored
+8
-8
@@ -166,12 +166,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
Start Browse B Back
|
||||
=== folder-card 40x15
|
||||
RetroBIOS: Where is the SD card?
|
||||
@@ -179,12 +179,12 @@
|
||||
NextUI keeps its BIOS files on its SD
|
||||
card. Choose the card's root folder,
|
||||
the one holding .system.
|
||||
No NextUI card is mounted. Insert it,
|
||||
then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
@@ -194,11 +194,11 @@
|
||||
|
||||
NextUI keeps its BIOS files on its SD card. Choose the card's root folder, the
|
||||
one holding .system.
|
||||
No NextUI card is mounted. Insert it, then choose Look again.
|
||||
|
||||
> Browse for a folder
|
||||
Type a path
|
||||
|
||||
|
||||
Look again
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -438,7 +438,7 @@ class MisplacedInTheBiosFolder(Scratch):
|
||||
for name, data in PAYLOAD.items():
|
||||
self.place(f"system/{name}", data)
|
||||
out = self.check()
|
||||
self.assertIn("4 of these files are in the BIOS folder at the wrong place: a run "
|
||||
self.assertIn("4 of these files are in the BIOS folder in the wrong place: a run "
|
||||
"without --check copies them where the cores open them, and leaves "
|
||||
"your copies as they are.", out)
|
||||
self.assertFalse((self.dest / "scph5501.bin").exists())
|
||||
|
||||
+456
-23
@@ -40,6 +40,7 @@ sys.path.insert(0, str(REPO_ROOT))
|
||||
sys.path.insert(0, str(REPO_ROOT / "tests"))
|
||||
|
||||
import install # noqa: E402
|
||||
from test_install_app import make_card # noqa: E402
|
||||
from test_install_selection import FakeRepository, _QuietHandler # noqa: E402
|
||||
|
||||
try:
|
||||
@@ -1294,6 +1295,20 @@ class ControlCharacters(unittest.TestCase):
|
||||
|
||||
|
||||
class SmallFixes(unittest.TestCase):
|
||||
def test_a_system_name_is_said_once_with_a_pack(self):
|
||||
"""tui_audit_5 P3-8.1: "Onion gets the RetroArch file list" was
|
||||
printed by the pack check, then again where the platform is read."""
|
||||
pack = mock.Mock(files=[Path("RetroArch_BIOS_Pack.zip")])
|
||||
pack.name = "RetroArch_BIOS_Pack.zip"
|
||||
pack.platforms.return_value = ["retroarch"]
|
||||
args = argparse.Namespace(manifest=None, from_pack=Path("p.zip"), platform="onion",
|
||||
**{"from": None})
|
||||
with mock.patch.object(install, "PackSource", return_value=pack), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
install._open_local_sources(args)
|
||||
self.assertNotIn("Onion gets", out.getvalue())
|
||||
install.LOCAL.reset()
|
||||
|
||||
def test_a_command_line_region_the_list_lacks(self):
|
||||
state = checked_state()
|
||||
state.cli_regions = ["europe"]
|
||||
@@ -1727,7 +1742,8 @@ class AuditRoundTwo(unittest.TestCase):
|
||||
state = fresh_state()
|
||||
state.stack = ["platforms"]
|
||||
press(state, "/", "m", "u", "o", "s")
|
||||
self.assertEqual([row.value for row in install.tui_rows(state)][1:], ["retroarch"])
|
||||
self.assertEqual([row.value for row in install.tui_rows(state)][1:],
|
||||
[("retroarch", "muOS")])
|
||||
|
||||
def test_p2_13_a_long_preview_wraps_only_what_is_in_view(self):
|
||||
state = checked_state()
|
||||
@@ -2028,7 +2044,7 @@ class CopyFromAnOldCard(unittest.TestCase):
|
||||
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)
|
||||
"Your copies in the wrong place were left as they are.", text)
|
||||
|
||||
def test_a_local_copy_is_no_download_speed(self):
|
||||
state = checked_state()
|
||||
@@ -2130,6 +2146,11 @@ class AuditRoundThree(unittest.TestCase):
|
||||
def test_p1_1_a_drive_letter_names_the_card(self):
|
||||
self.assertEqual(install._mount_name(PureWindowsPath("E:\\")), "E:\\")
|
||||
self.assertEqual(install._mount_name(Path("/media/me/MUOS")), "MUOS")
|
||||
# tui_audit_5 P3-2: under WSL the card screen listed "d (card or drive)".
|
||||
with mock.patch.dict(install._WSL, {"active": True}):
|
||||
self.assertEqual(install._mount_name(Path("/mnt/d")), "D:")
|
||||
self.assertEqual(install._mount_name(Path("/mnt/wslg")), "wslg")
|
||||
self.assertEqual(install._mount_name(Path("/mnt/d")), "d")
|
||||
|
||||
def test_p1_1_on_the_device_its_card_is_not_offered_twice(self):
|
||||
root = self.card(self.CARDS["muOS"][0])
|
||||
@@ -2411,7 +2432,7 @@ class AuditRoundThree(unittest.TestCase):
|
||||
self.assertEqual(rows[three].right, "1 file, 100 B + 2 shared")
|
||||
state.focus["systems"] = three
|
||||
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
|
||||
self.assertIn("2 of these files are shared with other systems, counted once", text)
|
||||
self.assertIn("+2 shared: counted once", text)
|
||||
# The line mode counts the same files apart.
|
||||
self.assertIn("and 2 files, 500 B shared with other systems", install._system_sized([
|
||||
{"dest": "panafz1.bin", "size": 100, "systems": ["3do"]},
|
||||
@@ -2746,11 +2767,12 @@ class AuditRoundFour(unittest.TestCase):
|
||||
plan = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
|
||||
# The plan names the row that sets it, the result the option a
|
||||
# command carries.
|
||||
self.assertIn(line + "Choose it in Options, Save folder.", plan)
|
||||
self.assertIn(line + "Choose it in Options, Save folder (--save-dir).", plan)
|
||||
state.stack.append("result")
|
||||
state.code = 0
|
||||
state.summary = {**summary, "mode": "install", "installed": 5}
|
||||
self.assertIn(line + "Name it with --save-dir.", install.tui_outcome(state)[1])
|
||||
self.assertIn(line + "Choose it in Options, Save folder (--save-dir), next time.",
|
||||
install.tui_outcome(state)[1])
|
||||
|
||||
|
||||
class TheSaveFolderHasARow(unittest.TestCase):
|
||||
@@ -2849,23 +2871,201 @@ class ACardSystemNamedOnAComputer(unittest.TestCase):
|
||||
self.assertEqual(install.card_install_dest("knulli", "knulli", True),
|
||||
knulli / "bios")
|
||||
|
||||
def test_the_interface_opens_the_card_screen_with_the_cards_first(self):
|
||||
def ask(self, *answers: str):
|
||||
"""_ask answering in turn, and the prompts it was asked."""
|
||||
given = iter(answers)
|
||||
asked: list = []
|
||||
|
||||
def answer(prompt: str) -> str:
|
||||
asked.append(prompt)
|
||||
return next(given)
|
||||
return mock.patch.object(install, "_ask", side_effect=answer), asked
|
||||
|
||||
def test_a_typed_root_that_does_not_exist_is_asked_again(self):
|
||||
"""code_audit_12 P2-3: a typo four folders deep was created and
|
||||
filled, and a menu answer (8) became the card."""
|
||||
onion = self.card(self.ONION)
|
||||
typo = REPO_ROOT / "tmp" / "no-such" / "media" / "NEXT"
|
||||
patcher, asked = self.ask("8", str(typo), str(onion))
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
dest = install.card_install_dest("onion", "retroarch", interactive=True)
|
||||
self.assertEqual(dest, onion / "BIOS")
|
||||
self.assertEqual(len(asked), 3)
|
||||
self.assertIn("8 does not exist, or is not a folder. Type the root of the Onion card.",
|
||||
out.getvalue())
|
||||
self.assertFalse((REPO_ROOT / "tmp" / "no-such").exists())
|
||||
|
||||
def test_a_typed_root_that_is_no_card_of_that_system_is_asked_again(self):
|
||||
"""code_audit_12 P3-4: a folder that was no Onion card ended the run."""
|
||||
blank, onion = self.card({"roms/a": ""}), self.card(self.ONION)
|
||||
patcher, asked = self.ask(str(blank), str(onion))
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
dest = install.card_install_dest("onion", "retroarch", interactive=True)
|
||||
self.assertEqual((dest, len(asked)), (onion / "BIOS", 2))
|
||||
self.assertIn(f"{blank} is not a Onion card: it holds no .tmp_update.", out.getvalue())
|
||||
|
||||
def test_a_minui_root_is_a_minui_card(self):
|
||||
blank = self.card({"roms/a": ""})
|
||||
(blank / "BIOS").mkdir()
|
||||
minui = self.card({".system/version.txt": "NextUI-20250101\n", "Bios/x": ""})
|
||||
patcher, asked = self.ask(str(minui / "Bios"), str(blank), str(minui))
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
dest = install.card_install_dest("nextui", "nextui", interactive=True)
|
||||
self.assertEqual((dest, len(asked)), (minui, 3))
|
||||
self.assertIn(f"is inside a NextUI card, whose root is {minui}", out.getvalue())
|
||||
self.assertIn("holds no .system/version.txt: it is not a NextUI card", out.getvalue())
|
||||
|
||||
def test_the_mounted_drives_are_listed_and_taken_by_number(self):
|
||||
"""tui_audit_5 P3-8.4: the screen lists the mounted cards, the
|
||||
prompt did not."""
|
||||
muos, blank = self.card({"MUOS/bios/x": ""}), self.card({"roms/a": ""})
|
||||
patcher, _asked = self.ask("2")
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[muos, blank]), \
|
||||
patcher, contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
dest = install.card_install_dest("knulli", "knulli", interactive=True)
|
||||
self.assertEqual(dest, blank / "bios")
|
||||
self.assertIn(f"2) {blank} (card or drive)", out.getvalue())
|
||||
self.assertIn(f"1) {muos} (muOS card)", out.getvalue())
|
||||
|
||||
def test_creating_several_folders_on_the_card_is_asked(self):
|
||||
root = self.card({"roms/a.txt": ""})
|
||||
rocknix = root / "R"
|
||||
rocknix.mkdir()
|
||||
patcher, asked = self.ask(str(rocknix), "n")
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
|
||||
contextlib.redirect_stdout(io.StringIO()), \
|
||||
self.assertRaises(SystemExit) as stop:
|
||||
install.card_install_dest("rocknix", "rocknix", interactive=True)
|
||||
self.assertEqual(stop.exception.code, 2)
|
||||
self.assertIn("[y/N]", asked[-1])
|
||||
self.assertFalse((rocknix / "roms").exists())
|
||||
|
||||
def test_the_menu_asks_a_card_system_for_its_card(self):
|
||||
"""code_audit_12 P3-3: ROCKNIX picked from the menu on a PC was
|
||||
offered /storage/roms/bios."""
|
||||
card = self.card({"roms/a": ""})
|
||||
make_card(card, "ROCKNIX")
|
||||
patcher, asked = self.ask(str(card))
|
||||
with mock.patch.object(install, "_choose_platform", return_value="rocknix"), \
|
||||
mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
|
||||
contextlib.redirect_stdout(io.StringIO()):
|
||||
chosen = install._prompt_manual_platform("linux")
|
||||
self.assertEqual(chosen, [("rocknix", card / "roms" / "bios")])
|
||||
self.assertEqual(asked, ["Card root (Ctrl+C to stop): "])
|
||||
|
||||
def test_a_card_of_another_system_is_not_offered(self):
|
||||
"""tui_audit_5 P1-1: A on the first mount wrote Onion's BIOS/ on a
|
||||
muOS card, which reads MUOS/bios."""
|
||||
muos, blank = self.card({"MUOS/bios/x": ""}), self.card({"roms/a": ""})
|
||||
found = [("retroarch", SYSTEM_DIR)]
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[blank]), \
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[muos, 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.assertEqual([row.action for row in rows], ["browse", "type", "rescan"])
|
||||
self.assertEqual(state.focus["folder"], 0)
|
||||
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")
|
||||
self.assertIn("No Onion card is mounted. Insert it, then choose Look again.", text)
|
||||
|
||||
def test_only_the_cards_of_that_system_are_listed(self):
|
||||
first, second = self.card(self.ONION), self.card(self.ONION)
|
||||
muos = self.card({"MUOS/bios/x": ""})
|
||||
state = fresh_state()
|
||||
state.stack = ["setup", "platforms"]
|
||||
state.filters["platforms"] = "onion"
|
||||
state.focus["platforms"] = 1
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[muos, first, second]):
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.screen, "folder")
|
||||
rows = install.tui_rows(state)
|
||||
install.tui_update(state, Key("enter"))
|
||||
self.assertEqual([(row.action, row.value) for row in rows][:2],
|
||||
[("card", first), ("card", second)])
|
||||
self.assertNotIn("rescan", [row.action for row in rows])
|
||||
self.assertEqual(state.dest, first / "BIOS")
|
||||
# Titled as the card found by itself (tui_audit_5 P3-7).
|
||||
self.assertEqual(install.detected_os("retroarch", first / "BIOS"), "Onion")
|
||||
|
||||
def test_look_again_finds_the_card_inserted(self):
|
||||
onion = self.card(self.ONION)
|
||||
mounts: list = []
|
||||
with mock.patch.object(install, "_mounted_cards", side_effect=lambda: list(mounts)), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]):
|
||||
state, _effects = install.tui_start(_args(platform="onion"), "linux")
|
||||
press(state, "down", "down", "enter")
|
||||
self.assertEqual(state.status, "Still no Onion card is mounted.")
|
||||
mounts.append(onion)
|
||||
press(state, "enter")
|
||||
rows = install.tui_rows(state)
|
||||
self.assertEqual(rows[state.focus["folder"]].action, "card")
|
||||
self.assertEqual(state.status, "")
|
||||
|
||||
def test_a_folder_that_is_no_card_of_that_system_is_refused(self):
|
||||
blank = self.card({"roms/a": ""})
|
||||
muos = self.card({"MUOS/bios/x": ""})
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]):
|
||||
state, _effects = install.tui_start(_args(platform="onion"), "linux")
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.screen, "browse")
|
||||
install._browse_to(state, blank)
|
||||
effects = press(state, "enter")
|
||||
self.assertEqual((effects, state.screen), ([], "browse"))
|
||||
self.assertIn(f"{blank} is not a Onion card: it holds no .tmp_update.",
|
||||
state.status)
|
||||
self.assertIsNone(state.dest)
|
||||
# A muOS card gets the folder muOS reads, as --dest does.
|
||||
install._browse_to(state, muos)
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.dest, muos / "MUOS" / "bios")
|
||||
|
||||
def test_a_typed_card_root_is_checked_on_the_path_screen(self):
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]):
|
||||
state, _effects = install.tui_start(_args(platform="rocknix"), "linux")
|
||||
press(state, "down", "enter")
|
||||
self.assertEqual(state.screen, "path")
|
||||
state.path_text = str(REPO_ROOT / "tmp" / "no-such-card")
|
||||
effects = press(state, "enter")
|
||||
self.assertEqual((effects, state.screen), ([], "path"))
|
||||
self.assertIn("does not exist, or is not a folder. Choose the root of the "
|
||||
"ROCKNIX card.", state.status)
|
||||
|
||||
def test_a_dest_that_is_no_card_of_that_system_is_refused(self):
|
||||
blank = self.card({"roms/a": ""})
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]):
|
||||
state, effects = install.tui_start(_args(platform="onion", dest=str(blank)),
|
||||
"linux")
|
||||
self.assertEqual((effects, state.screen), ([], "folder"))
|
||||
self.assertIn("is not a Onion card", state.status)
|
||||
|
||||
def test_change_folder_names_the_folder_it_reuses(self):
|
||||
"""code_audit_12 P3-1: Use this folder on the card screen named
|
||||
nothing."""
|
||||
onion = self.card(self.ONION)
|
||||
state = checked_state()
|
||||
state.platform, state.card_bound = "retroarch", ("Onion", "BIOS")
|
||||
state.dest = onion / "BIOS"
|
||||
press(state, "o")
|
||||
rows = install.tui_rows(state)
|
||||
state.focus["options"] = [row.action for row in rows].index("folder")
|
||||
press(state, "enter")
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]):
|
||||
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
|
||||
self.assertEqual(state.screen, "folder")
|
||||
self.assertIn("Where is the SD card?", text)
|
||||
self.assertIn("The BIOS folder chosen for Onion:", text)
|
||||
self.assertEqual(state.folder_default, onion / "BIOS")
|
||||
|
||||
def test_the_interface_takes_the_one_onion_card_mounted(self):
|
||||
onion = self.card(self.ONION)
|
||||
@@ -2914,17 +3114,22 @@ class TheSetupFilterKnowsDevices(unittest.TestCase):
|
||||
state = fresh_state()
|
||||
state.stack = ["platforms"]
|
||||
state.filters["platforms"] = needle
|
||||
return [row for row in install.tui_rows(state) if row.action == "platform"]
|
||||
return [row for row in install.tui_rows(state)
|
||||
if row.action in ("platform", "cardsystem")]
|
||||
|
||||
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)
|
||||
self.assertEqual({row.value[0] if row.action == "cardsystem" else 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"])
|
||||
rows = self.rows("miyoo")
|
||||
retroarch = {row.value: row for row in rows}["retroarch"]
|
||||
self.assertEqual(retroarch.right, "MiyooCFW")
|
||||
self.assertEqual([row.label for row in rows if row.action == "cardsystem"],
|
||||
["spruceOS, SD card", "Onion, SD card", "Allium, SD card",
|
||||
"Koriki, SD card"])
|
||||
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)
|
||||
@@ -2943,6 +3148,76 @@ class TheSetupFilterKnowsDevices(unittest.TestCase):
|
||||
tuple(str(name) for name in system["devices"]))
|
||||
|
||||
|
||||
class ACardSetupIsOneDesign(unittest.TestCase):
|
||||
"""code_audit_12 P2-1 to P3-2, tui_audit_5 P1-2: a setup that runs from
|
||||
its SD card, chosen on a computer, always asks for that card."""
|
||||
|
||||
def platforms(self, needle: str = "") -> install.TuiState:
|
||||
state = fresh_state()
|
||||
state.stack = ["setup", "platforms"]
|
||||
state.filters["platforms"] = needle
|
||||
state.focus["platforms"] = 1
|
||||
return state
|
||||
|
||||
def test_a_handheld_typed_in_the_filter_asks_for_its_card(self):
|
||||
for needle in ("onion", "miyoo mini", "rg35xx"):
|
||||
with self.subTest(needle=needle):
|
||||
state = self.platforms(needle)
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]):
|
||||
row = install.tui_rows(state)[1]
|
||||
effects = press(state, "enter")
|
||||
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
|
||||
self.assertEqual(row.action, "cardsystem")
|
||||
self.assertEqual((effects, state.screen), ([], "folder"))
|
||||
self.assertEqual(state.card_bound, install.card_bound(row.value[1], "retroarch"))
|
||||
self.assertIn("Where is the SD card?", text)
|
||||
self.assertNotIn("default", [r.action for r in install.tui_rows(state)])
|
||||
self.assertNotIn(str(SYSTEM_DIR), text)
|
||||
|
||||
def test_retroarch_typed_is_still_this_computer(self):
|
||||
state = self.platforms("retroarch")
|
||||
press(state, "enter")
|
||||
self.assertIsNone(state.card_bound)
|
||||
self.assertEqual(state.folder_default, SYSTEM_DIR)
|
||||
|
||||
def test_a_card_backed_out_of_is_forgotten(self):
|
||||
state = fresh_state()
|
||||
state.stack = ["setup"]
|
||||
state.focus["setup"] = 2
|
||||
press(state, "enter")
|
||||
state.focus["platforms"] = install._platform_focus(state, "rocknix")
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]):
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.card_bound, ("ROCKNIX", "roms/bios"))
|
||||
press(state, "esc", "esc")
|
||||
self.assertEqual(state.screen, "setup")
|
||||
state.focus["setup"] = 0
|
||||
with mock.patch.object(install.Path, "exists", return_value=True):
|
||||
press(state, "enter")
|
||||
self.assertIsNone(state.card_bound)
|
||||
self.assertEqual((state.platform, state.dest), ("retroarch", SYSTEM_DIR))
|
||||
install._open_folder_screen(state, state.dest, "chosen")
|
||||
title = install.tui_view(state, 80, 24)[0]
|
||||
self.assertIn("Which folder?", title)
|
||||
self.assertNotIn("card", [row.action for row in install.tui_rows(state)])
|
||||
|
||||
def test_on_the_device_its_own_folder_is_offered(self):
|
||||
share = Path("/userdata/bios")
|
||||
state = fresh_state()
|
||||
state.setups = [("knulli", share, "KNULLI")]
|
||||
state.stack = ["setup", "platforms"]
|
||||
state.focus["platforms"] = install._platform_focus(state, "knulli")
|
||||
with mock.patch.dict(install._DETECTED_OS, {("knulli", share): "KNULLI"}), \
|
||||
mock.patch.object(install, "_mounted_cards", return_value=[]):
|
||||
press(state, "enter")
|
||||
rows = [row.action for row in install.tui_rows(state)]
|
||||
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
|
||||
self.assertEqual(rows, ["default", "browse", "type"])
|
||||
self.assertEqual((state.folder_default, state.folder_kind), (share, "found"))
|
||||
self.assertIn("Which folder? KNULLI reads its BIOS files from: /userdata/bios", text)
|
||||
self.assertNotIn("No KNULLI card is mounted", text)
|
||||
|
||||
|
||||
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."""
|
||||
@@ -3167,6 +3442,164 @@ class OfflineInTheInterface(unittest.TestCase):
|
||||
self.assertIn("--offline", install.tui_command(state))
|
||||
|
||||
|
||||
class AuditFive(unittest.TestCase):
|
||||
"""tui_audit_5 P2-1, P2-2 and its P3 points."""
|
||||
|
||||
def long_list(self) -> install.TuiState:
|
||||
state = OneSystemInFewPresses().long_list("keys")
|
||||
state.cols, state.rows = 80, 24
|
||||
return state
|
||||
|
||||
def test_back_from_none_keeps_the_ticks_it_opened_with(self):
|
||||
state = self.long_list()
|
||||
everything = set(state.ticked)
|
||||
press(state, "down", "enter")
|
||||
self.assertEqual(install.tui_rows(state)[state.focus["systems"]].action, "none")
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.ticked, set())
|
||||
press(state, "up", "up")
|
||||
self.assertEqual(install._primary(state), "A/Enter First system")
|
||||
press(state, "esc")
|
||||
self.assertEqual(state.screen, "plan")
|
||||
self.assertEqual(state.ticked, everything)
|
||||
text = " ".join(install.tui_view(state, 80, 24))
|
||||
self.assertNotIn("No system is ticked", text)
|
||||
|
||||
def test_back_after_a_choice_keeps_the_choice(self):
|
||||
state = self.long_list()
|
||||
press(state, "down", "enter", "enter")
|
||||
press(state, " ")
|
||||
chosen = set(state.ticked)
|
||||
press(state, "esc")
|
||||
self.assertEqual((state.screen, state.ticked), ("plan", chosen))
|
||||
|
||||
def test_confirm_offline_with_nothing_on_hand_has_no_install(self):
|
||||
state = checked_state()
|
||||
state.stack.append("confirm")
|
||||
state.dry = dict(CHECK)
|
||||
state.dry_files = [dict(event, state="missing") for event in FILES]
|
||||
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
|
||||
rows = [row.action for row in install.tui_rows(state)]
|
||||
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
|
||||
self.assertEqual(rows, ["back"])
|
||||
self.assertIn("Copy them from your old card or a folder", text)
|
||||
state.found = [{"event": "found", "platform": "retroarch",
|
||||
"dest": FILES[0]["dest"], "folder": "/media/old",
|
||||
"path": "/media/old/x", "member": "", "reason": ""}]
|
||||
self.assertEqual(install.tui_rows(state)[0].action, "install")
|
||||
|
||||
def test_the_checking_clock_starts_at_zero(self):
|
||||
state = install.TuiState()
|
||||
self.assertLess(abs(state.now - time.monotonic()), 5)
|
||||
|
||||
def test_a_pack_installs_from_the_pack(self):
|
||||
state = checked_state()
|
||||
state.found = [{"event": "found", "platform": "retroarch", "dest": FILES[0]["dest"],
|
||||
"folder": "/media/old", "path": "/media/old/x", "member": "",
|
||||
"reason": "misplaced"}]
|
||||
with mock.patch.object(install.LOCAL, "pack", mock.Mock()):
|
||||
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
|
||||
self.assertIn("1 copied from this machine,", text)
|
||||
self.assertIn("from the pack", text)
|
||||
self.assertNotIn("downloaded", text)
|
||||
self.assertNotIn("of them", text)
|
||||
state.finished = [(FILES[0]["dest"], True, ""), (FILES[1]["dest"], True, "")]
|
||||
state.local_dests = {FILES[0]["dest"]: ("/media/old", "misplaced")}
|
||||
self.assertIn("1 from the pack", install._tui_sources(state, 2, 0))
|
||||
|
||||
def test_the_wrong_place_screen_says_the_copies_stay(self):
|
||||
state = checked_state()
|
||||
state.stack.append("misplaced")
|
||||
self.assertEqual(install._pager_items(state)[0],
|
||||
"They are copied where the emulators read them. Your copies stay "
|
||||
"where they are.")
|
||||
|
||||
def test_the_save_folder_is_a_line_of_its_own(self):
|
||||
state = checked_state()
|
||||
saves = {"files": 1, "folder": str(HOME / ".config" / "retroarch" / "saves" / "<core>")}
|
||||
lines = install._saves_lines(state, saves)
|
||||
self.assertEqual(lines, ["1 file read from RetroArch's save directory goes to:",
|
||||
"~/.config/retroarch/saves/<core>"])
|
||||
|
||||
def test_the_shared_note_is_one_line(self):
|
||||
state = self.long_list()
|
||||
state.cols, state.rows = 40, 12
|
||||
state.files = [{"event": "file", "dest": "mame.zip", "size": 10,
|
||||
"systems": ["sys-00", "sys-01"]}]
|
||||
state.stack.append("systems")
|
||||
state.focus["systems"] = 3
|
||||
lines = install.tui_view(state, 40, 12)
|
||||
self.assertIn(" +1 shared: counted once", lines)
|
||||
|
||||
def test_the_save_folder_remedy_is_the_same_on_both_screens(self):
|
||||
summary = {"skipped": [{"reason": "no_save_directory", "files": 1}]}
|
||||
state = checked_state()
|
||||
lines = install._unplaced_lines(summary, state)
|
||||
self.assertIn("Options, Save folder (--save-dir)", lines[0])
|
||||
|
||||
def test_a_size_keeps_its_unit(self):
|
||||
for width in range(20, 45):
|
||||
for line in install._wrap("2 files to install from the pack, 5.1 KB", width):
|
||||
self.assertNotEqual(line.strip(), "KB")
|
||||
|
||||
def test_the_miyoo_bar_names_the_tick(self):
|
||||
state = self.long_list()
|
||||
state.pad = "miyoo"
|
||||
press(state, "down", "enter")
|
||||
state.ticked = set()
|
||||
state.focus["systems"] = 3
|
||||
bar = install.tui_view(state, 40, 15)[-1]
|
||||
self.assertIn("A Tick", bar)
|
||||
self.assertTrue(bar.strip().startswith("Start Only this system"))
|
||||
|
||||
def test_muxterm_help_names_no_home_key(self):
|
||||
state = self.long_list()
|
||||
state.pad = "muxterm"
|
||||
press(state, "down", "enter")
|
||||
keys = [key for key, _what in install._help_lines(state)]
|
||||
self.assertNotIn("Home/End, g/G", keys)
|
||||
state.pad = "keys"
|
||||
self.assertIn("Home/End, g/G", [key for key, _what in install._help_lines(state)])
|
||||
|
||||
|
||||
class LineModeOffline(unittest.TestCase):
|
||||
"""tui_audit_5 P2-3 and P3-8.4: the line mode under --offline asked to
|
||||
download, and said nothing of the data files."""
|
||||
|
||||
def test_nothing_is_offered_that_cannot_come(self):
|
||||
with FakeRepository() as repo:
|
||||
env = {**repo.env(), "TERM": "xterm", install.OFFLINE_VARIABLE: "1"}
|
||||
term = Pty(["--plain", "--manifest", str(repo.root / "install" / "retroarch.json"),
|
||||
"--dest", str(repo.dest), "--offline"], env)
|
||||
term.wait("q) quit")
|
||||
term.send("\r")
|
||||
term.close()
|
||||
out = term.raw.decode("utf-8", "replace")
|
||||
self.assertIn("--offline: only the files found on this machine are installed.", out)
|
||||
self.assertIn("5 files need the internet, which --offline does not use.", out)
|
||||
self.assertNotIn("Downloading", out)
|
||||
self.assertNotIn("failed", out.lower())
|
||||
self.assertIn("Nothing installed: 5 files not downloaded, which --offline does not do.",
|
||||
out.replace("\r\n", " ").replace("\n", " "))
|
||||
|
||||
def test_the_data_files_are_counted_before_the_question(self):
|
||||
files = [{"dest": "a.bin", "size": 1024}]
|
||||
trees = [{"ref": "t", "files": {"x": {"size": 2048}, "y": {"size": 2048}}}]
|
||||
with mock.patch.object(install, "_ask", return_value=""), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
install._prompt_custom_selection("retroarch", files, [], data_trees=trees)
|
||||
self.assertIn("RetroArch: 1 file, 1.0 KB, and 2 data files (4.0 KB).", out.getvalue())
|
||||
|
||||
def test_one_save_file_goes(self):
|
||||
files = [{"dest": "keys.txt", "root": "save", "size": 1}]
|
||||
place = install.SavePlace(Path("/saves"), False)
|
||||
with mock.patch.object(install, "save_directory_for", return_value=place), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
install.place_save_entries(files, Path("/bios"), {}, None, "RetroArch")
|
||||
self.assertIn("1 file read from RetroArch's save directory goes to /saves",
|
||||
out.getvalue())
|
||||
|
||||
|
||||
class NarrowSystemNames(unittest.TestCase):
|
||||
"""At 40 columns the size column took half the line: Game Boy, Game Boy
|
||||
Advance and Game Boy Color all read "Nintendo Gam"."""
|
||||
@@ -3280,7 +3713,7 @@ class TheSaveFolderLineFollowsTheChoice(unittest.TestCase):
|
||||
|
||||
SAVES = {"event": "saves", "platform": "retroarch", "files": 1,
|
||||
"folder": "/home/user/MySaves/<core>"}
|
||||
LINE = "1 file read from RetroArch's save directory goes to ~/MySaves/<core>"
|
||||
LINE = "1 file read from RetroArch's save directory goes to: ~/MySaves/<core>"
|
||||
|
||||
def said(self, lines) -> str:
|
||||
return " ".join(" ".join(lines).split())
|
||||
@@ -4082,11 +4515,11 @@ class EndToEnd(unittest.TestCase):
|
||||
self.assertIn("6 files to download", screen)
|
||||
# The second folder the run writes to, named before
|
||||
# anything is written, and again on the result.
|
||||
self.assertIn("1 file read from RetroArch's save directory goes to "
|
||||
"~/MySaves", screen)
|
||||
self.assertIn("1 file read from RetroArch's save directory goes to:\n"
|
||||
" ~/MySaves", screen)
|
||||
term.send("\r")
|
||||
screen = term.wait("6 files installed.")
|
||||
self.assertIn("save directory goes to ~/MySaves", screen)
|
||||
self.assertIn("save directory goes to:\n ~/MySaves", screen)
|
||||
term.send("\r")
|
||||
self.assertEqual(term.close(), 0)
|
||||
results.append(written(repo, saves))
|
||||
@@ -4467,7 +4900,7 @@ class ThePlanNumbersAddUp(unittest.TestCase):
|
||||
state.found = [found("scph5500.bin", str(SYSTEM_DIR), "one folder too deep")]
|
||||
screen = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
|
||||
self.assertIn("3 files to download", screen)
|
||||
self.assertIn("1 in place, 3 missing, 1 wrong copy: 1 of them is copied from this "
|
||||
self.assertIn("1 in place, 3 missing, 1 wrong copy 4 to install: 1 copied from this "
|
||||
"machine, 3 downloaded", screen)
|
||||
|
||||
|
||||
|
||||
@@ -736,7 +736,7 @@ class TheInstallerWritesThem(unittest.TestCase):
|
||||
b"keys below the save directory",
|
||||
)
|
||||
self.assertFalse((app / "system" / "Azahar").exists())
|
||||
self.assertIn("read from RetroArch's save directory go to", proc.stdout)
|
||||
self.assertIn("read from RetroArch's save directory goes to", proc.stdout)
|
||||
|
||||
def test_where_save_dir_points(self):
|
||||
repo = _Repository(self, self.FILES)
|
||||
|
||||
@@ -11,8 +11,8 @@ From an empty BIOS folder to games that start.
|
||||
|
||||
## 1. Open a terminal and paste
|
||||
|
||||
A **terminal** is a window where you type commands. Paste the line for your
|
||||
system, then press Enter.
|
||||
A **terminal** is a window where you type commands. Paste your system's
|
||||
line, then press Enter.
|
||||
|
||||
<Tabs syncKey="os">
|
||||
<TabItem label="Windows">
|
||||
@@ -81,9 +81,10 @@ It opens a menu:
|
||||
|
||||
- **Right folder?** Press Enter.
|
||||
- **Wrong folder?** Choose **Another setup or folder**, then browse to yours.
|
||||
- **A handheld's SD card in a reader?** It is listed, as **muOS SD card**.
|
||||
- **A handheld's SD card?** Choose it, or its system under **Another setup
|
||||
or folder**: **Where is the SD card?**
|
||||
|
||||
Arrows move, Enter chooses, Esc goes back. A handheld's A and B do the same.
|
||||
Arrows move, Enter chooses, Esc goes back, as A and B on a handheld.
|
||||
|
||||
## 3. Install
|
||||
|
||||
|
||||
+15
-7
@@ -132,7 +132,8 @@ 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. A long list opens on **None**:
|
||||
A there unticks every system, and A on a system then installs that one alone. **Options** sets the
|
||||
A or Enter there unticks every system, and A on a system then installs that one alone. Back
|
||||
with nothing ticked keeps the ticks the list opened with. **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
|
||||
@@ -144,7 +145,7 @@ files that folder holds and how many are left to download. After an install
|
||||
on a muOS, KNULLI or ROCKNIX card, the result offers **Put RetroBIOS on this
|
||||
card (update from the console later)**, and **Options** has the same row
|
||||
([On a handheld](#on-a-handheld-the-retrobios-port)). Files the BIOS
|
||||
folder holds at the wrong place are counted on the plan, and **Show the files
|
||||
folder holds in the wrong place are counted on the plan, and **Show the files
|
||||
in the wrong place** lists each one with its reason. Back on the plan
|
||||
after a change of options, the folder is checked again, so its numbers are the
|
||||
exact ones for the new choice. Ticked systems get their exact count before the
|
||||
@@ -155,7 +156,7 @@ install starts.
|
||||
| Move | Up and Down, `k` and `j` | D-pad Up and Down |
|
||||
| Page | PgUp and PgDn | L1 and R1 |
|
||||
| First, last | Home and End, `g` and `G` | |
|
||||
| Continue, open, choose | Enter, Space | A, Start |
|
||||
| Continue, open, choose | Enter, and Space outside a list of ticks | A, Start |
|
||||
| Tick, untick | Space, Right, Left | X, D-pad Right and Left |
|
||||
| Back | Esc, Backspace | B, Select |
|
||||
| Options | `o`, Tab | Y |
|
||||
@@ -903,8 +904,8 @@ When a platform named with `--platform` is not found on the machine, the
|
||||
destination falls back to the directory its detection looks for:
|
||||
`/userdata/bios` for Batocera, `/recalbox/share/bios` for Recalbox,
|
||||
`/storage/system` for Lakka, `~/retrodeck` for RetroDECK, `~/Emulation/bios`
|
||||
for EmuDeck (`C:\Emulation\bios` on Windows), `/storage/roms/bios` for ROCKNIX,
|
||||
`/userdata/bios` for KNULLI, `/storage/roms` for AmberELEC and EmuELEC, `/media/fat/games` for MiSTer,
|
||||
for EmuDeck (`C:\Emulation\bios` on Windows), `/storage/roms` for AmberELEC and
|
||||
EmuELEC, `/media/fat/games` for MiSTer,
|
||||
`~/RetroPie/BIOS` for RetroPie, `/romm/library/bios` for RomM,
|
||||
`C:\RetroBat\bios` for RetroBat, and RetroArch's own default system
|
||||
directory for RetroArch. BizHawk has no such folder: it reads `Firmware`
|
||||
@@ -912,8 +913,15 @@ beside its executable wherever the archive was extracted, so a forced
|
||||
`--platform bizhawk` that finds no install stops and asks for `--dest`. MinUI,
|
||||
NextUI and MyMinUI keep their files on the SD card. Run on the device, the
|
||||
installer finds the card root. From a computer the card is mounted on, a
|
||||
forced `--platform minui` stops and asks for the card's root as `--dest`, and a `--dest` that is a folder inside such a
|
||||
card (its `Bios/`) is refused with the root to name instead. On Android a forced `--platform retroarch` falls back to
|
||||
forced `--platform minui` uses the one MinUI card mounted. Otherwise the screen
|
||||
opens **Where is the SD card?**, which lists only the cards of that system and
|
||||
offers **Look again**, and `--plain` asks `Card root`, listing the mounted
|
||||
drives by number. A root that does not exist is asked again, and so is a
|
||||
folder without the card's own files for MinUI, NextUI, MyMinUI, Onion, muOS
|
||||
and spruceOS. Without questions (`--no-input`) the run stops with a `--dest`
|
||||
example. The same holds for Onion, muOS, spruceOS, KNULLI and ROCKNIX, also
|
||||
when chosen from the setup list or its filter. A `--dest` that is a folder
|
||||
inside such a card (its `Bios/`) is refused with the root to name instead. On Android a forced `--platform retroarch` falls back to
|
||||
`<shared storage>/RetroArch/system` instead, a home directory there belonging
|
||||
to Termux rather than to the frontend. Pass `--dest` to say where instead of
|
||||
relying on that. When detection met a folder this user cannot open, under the
|
||||
|
||||
Reference in new issue
Block a user