fix: say what to act on by default

Five lines the default text hid are back where the user reads them:
- the result names each failed file again, after Exit too;
- a run, --check, the plan and the result count the files an emulator
  needs that are not available here, --verbose or Details naming them;
- --check names each copy that fails its check, the files that sit too
  deep and what a run copies, and says such a copy is backed up; only
  the plain missing files wait on --verbose;
- the plan and the result keep the count of save files left out and the
  remedy, the reason going to Details;
- the plan counts the --from files that could not be read where no row
  names them, and says another drive to add.

Exit codes are unchanged. The result's word budget rises by the failed
file line.
This commit is contained in:
Abdessamad Derraz committed 2026-11-10 01:46:23 +01:00
1 parent d57120ec0e
commit b82c4be6c9
26 files changed
+259 -98

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
# the site's build rewrites this line there (scripts/sitebootstrap.py).
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
$defaultInstallSha256 = "d07c90a00f5bab9c583c34a9a1413bed5625542e7c88fed7bd9bab0020e9e637"
$defaultInstallSha256 = "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738"
# The pin above refuses any changed byte: this cap only stops a body that
# cannot be the installer (a sign-in portal's page) before it is hashed.
$maximumInstallerBytes = 8MB
+73 -28
View File
@@ -6853,12 +6853,16 @@ def _report_outcome(
say(f"BIOS folder: {_locations(paths)}")
for line in kept:
say(line)
left = [entry for entry in omitted_entries or []
if entry.get("reason") != "no_save_directory"]
if omitted > len(unplaced) and OUTPUT.verbose:
for line in _omission_lines(omitted - len(unplaced),
[entry for entry in omitted_entries or []
if entry.get("reason") != "no_save_directory"],
for line in _omission_lines(omitted - len(unplaced), left,
Path(paths[0]) if len(paths) == 1 else None):
say(line)
elif omitted > len(unplaced) and any(entry.get("required") for entry in left):
# A file an emulator cannot start without is no detail: counted.
say(needed_absent_words(sum(1 for entry in left if entry.get("required")),
"--verbose"))
# RomM's scan is the step after the install: the other hints say how to
# install less, a detail.
for line in hints or []:
@@ -6927,7 +6931,7 @@ def _retry_words(failures: list | None) -> str:
f"{'it' if len(lacking) == 1 else 'them'}.")
def _check_fetch_line(fetch: int, beside: str, lacking: int = 0) -> str:
def _check_fetch_line(fetch: int, beside: str, lacking: int = 0, wrong: bool = False) -> str:
"""What --check says a run without it does with the files not in place:
offline, those it cannot fetch are said so, not "run it to fetch them".
With a pack, those it holds are copied and those it lacks are said lacking
@@ -6945,10 +6949,10 @@ def _check_fetch_line(fetch: int, beside: str, lacking: int = 0) -> str:
return " Run the same command without --check to copy them into place."
return (f" {agree(fetch, 'file needs', 'files need')} the "
f"internet, which {offline_named()} does not use. {_offline_remedy()}")
# Why nothing is lost goes with the details: the remedy is the line.
# What becomes of a wrong copy is said where there is one.
return (" Run the same command without --check to fetch them."
+ (f"\n A wrong copy is replaced, and the one in place kept in {BACKUP_DIR} "
f"beside {beside}." if OUTPUT.verbose else ""))
f"beside {beside}." if OUTPUT.verbose or wrong else ""))
def _offline_failure(reason: str) -> bool:
@@ -7028,6 +7032,15 @@ def _omission_lines(count: int, entries: list[dict], bios_path: Path | None = No
return lines
def needed_absent_words(count: int, where: str) -> str:
"""The count of files an emulator cannot start without that the
collection does not hold, said in every mode: where names what lists
them, --verbose in line mode, Details on the screen."""
return (f"{_plural(count, 'file')} an emulator needs {'is' if count == 1 else 'are'} "
f"not available here ({where} names "
f"{'it' if count == 1 else 'them'}).")
def _absent_required(omitted: list[dict], bios_path: Path) -> list[dict]:
"""Required entries the manifest leaves out that the user has not placed."""
absent = []
@@ -12846,7 +12859,8 @@ def check_lines(
lines += _check_copy_lines(missing + mismatched, copies, index)
explained = [e for e in missing if id(e) in found.reasons or id(e) in copies]
plain = [e for e in missing if id(e) not in found.reasons and id(e) not in copies]
shown = plain if every else plain[:10]
# A missing file with no more to say is a list: --verbose names each.
shown = plain if every else []
def block(label: str, entry: dict) -> list[str]:
game = " (game data)" if entry.get("category") == GAME_DATA else ""
@@ -12872,7 +12886,7 @@ def check_lines(
lines += block("missing", entry)
for entry in shown:
lines += block("missing", entry)
if len(shown) < len(plain):
if len(shown) < len(plain) and lines:
lines.append(f" ... and {len(plain) - len(shown)} more missing "
"(--verbose lists them all)")
return shown_lines(lines)
@@ -16216,7 +16230,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
else "to download."))
for line in check_lines(missing, mismatched, files, manifest, bios_path,
args.verbose, up_to_date, bios_index, copies):
detail(line)
print(line)
for line in unreadable_lines(unread, args.verbose):
print(line)
print(f" BIOS folder: {shown_path(bios_path)}")
@@ -16224,7 +16238,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
# need the internet" stood under "10805 files not in place".
trees_fetched = trees_missing if LOCAL.pack is None else 0
print(_check_fetch_line(len(to_download) - len(copies) + trees_fetched,
"the BIOS folder", len(lacking)))
"the BIOS folder", len(lacking), bool(mismatched)))
check_pending += len(to_download)
elif trees_missing:
# The screen's headline: "Everything is in place." stood
@@ -16236,7 +16250,10 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
absent = _absent_required(omitted_files, bios_path)
# The collection cannot supply these, so they leave the exit
# code alone, but a user asking why a game will not start needs
# their names, and which system and emulator each one is for.
# their count, and with --verbose their names, and which system
# and emulator each one is for.
if absent and not OUTPUT.verbose:
print(f" {needed_absent_words(len(absent), '--verbose')}")
for line in _needed_lines(
absent, "Not in the collection and absent here, needed to start an emulator",
every=args.verbose,
@@ -17200,6 +17217,8 @@ class TuiLine(str):
powershell: bool | None = None
# Shown on the Details screen, not on the screen it explains.
detail = False
# A count standing for Details lines: left out where they are shown.
brief = False
def _styled(text: str, style: str = "") -> TuiLine:
@@ -21503,12 +21522,25 @@ def _tui_found_elsewhere(state: TuiState, dests: set | None = None) -> dict[str,
def _scan_lines(state: TuiState) -> list[str]:
"""What the reading of the --from folders left out, one line each."""
lines = []
lines: list = []
for scan in state.scanned:
if scan["truncated"]:
lines.append(f"Read the first {MAX_SCANNED_FILES:,} files of {scan['folder']}.")
lines.append(_detail(f"Read the first {MAX_SCANNED_FILES:,} files of "
f"{scan['folder']}."))
lines += [f"{drive} is another drive: add it to copy from it too."
for drive in scan["other_drives"]]
# The count, which line mode always says, stays where no row names
# these files: the plan's Show row does.
if state.unreadable and any(row.action == "unreadable" for row in _plan_rows(state)):
lines += _details(_unreadable_count(state))
elif state.unreadable:
lines += _unreadable_count(state)
return lines
def _unreadable_count(state: TuiState) -> list[str]:
"""How many files the --from folders hold that could not be read."""
lines = []
if state.unreadable:
count = len(state.unreadable)
# Named as it was given: "your old card" was said of a folder.
@@ -21686,12 +21718,12 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
lines += _commands(state, write_remedy(dest, state.platform, tui_hint(state),
state.pad not in TUI_PAD_ROUTES))
if check is state.check:
lines += _details(_scan_lines(state))
lines += _scan_lines(state)
if check.get("not_installable"):
lines += _details(_unavailable_lines(state))
lines += _unavailable_brief(state) + _details(_unavailable_lines(state))
if check is state.check:
lines += _details(_unplaced_lines(_choice_summary(state), state,
"Choose it in Options, Save folder (--save-dir)."))
lines += _unplaced_lines(_choice_summary(state), state,
"Choose it in Options, Save folder (--save-dir).")
if check is state.check and state.from_folders:
# A folder copied from that holds none of them: the plan came back
# the same, still saying to copy from a folder.
@@ -21813,11 +21845,10 @@ def _unplaced_lines(summary: dict | None, state: TuiState,
if not count:
return []
name = display_name(state.platform) if state.platform else "the frontend"
left = f"{_plural(count, 'file')} read from {name}'s save directory left out"
if state.manifest and not state.manifest.get("save_directory"):
return [f"{_plural(count, 'file')} read from {name}'s save directory left out: "
f"{unlisted_saves_words(name)}."]
return [f"{_plural(count, 'file')} read from {name}'s save directory left out: "
f"its place is not known. {remedy or TUI_SAVE_REMEDY}"]
return [f"{left}.", _detail(f"{unlisted_saves_words(name)}.")]
return [f"{left}. {remedy or TUI_SAVE_REMEDY}", _detail("Its place is not known.")]
# Said when ES-DE is found, and in fewer words where the long form would
@@ -22218,9 +22249,11 @@ def tui_verbose(state: TuiState) -> bool:
def _shown(state: TuiState, lines: list) -> list:
"""The lines a screen shows: all with --verbose, else all but Details."""
return list(lines) if tui_verbose(state) else [
line for line in lines if not _is_detail(line)]
"""The lines a screen shows: with --verbose every line but the counts
standing for them, else every line but Details."""
if tui_verbose(state):
return [line for line in lines if not getattr(line, "brief", False)]
return [line for line in lines if not _is_detail(line)]
def _view_plan(state: TuiState, width: int, height: int) -> tuple:
@@ -22721,6 +22754,18 @@ def _unavailable(state: TuiState) -> tuple[list[dict], int]:
return needed, others
def _unavailable_brief(state: TuiState) -> list[TuiLine]:
"""The count of the files an emulator needs that cannot be supplied, on
the screen itself: their names are on Details, and in place with
--verbose, where the count is not said again."""
needed, _others = _unavailable(state)
if not needed:
return []
line = _styled(needed_absent_words(len(needed), "Details"))
line.brief = True
return [line]
def _unavailable_lines(state: TuiState) -> list[str]:
"""The files that cannot be supplied and stop an emulator, by name and
system: "1 file ... an emulator needs it" named neither."""
@@ -22913,10 +22958,10 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]:
continue
for dest in dests:
if shown < 20:
lines.append(_detail(f" failed: {dest}" + (f" ({reason})" if reason else "")))
lines.append(f" failed: {dest}" + (f" ({reason})" if reason else ""))
shown += 1
if shown > 20:
lines.append(_detail(f" ... and {shown - 20} more"))
lines.append(f" ... and {shown - 20} more")
if title == "Finished with errors":
# What a rerun does, as line mode and the first install page say
# it: the screen never did.
@@ -22957,10 +23002,10 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]:
lines += _details([_command_line(f"To put them back: {run.launcher()} --restore "
f"{run.word(str(kept['folder']))}", run)])
if summary and not summary.get("interrupted") and installed + up_to_date:
lines += _details(_unavailable_lines(state))
lines += _unavailable_brief(state) + _details(_unavailable_lines(state))
for group in summary.get("left_out") or []:
lines.append(_detail(f"{left_out_words(group)}."))
lines += _details(_unplaced_lines(summary, state))
lines += _unplaced_lines(summary, state)
lines += _tui_app_lines(state)
if title == "Done" and not (state.platform == "romm" and installed):
# Last of what the result says by default: RomM's scan is its step.
+1 -1
View File
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
# curl or wget stop on a cut transfer before the hash is read: the site's
# build rewrites this line there (scripts/sitebootstrap.py).
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
DEFAULT_INSTALL_SHA256="d07c90a00f5bab9c583c34a9a1413bed5625542e7c88fed7bd9bab0020e9e637"
DEFAULT_INSTALL_SHA256="61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738"
# The pin above refuses any changed byte: this cap only stops a body that
# cannot be the installer (a sign-in portal's page) before it is hashed.
MAX_INSTALLER_BYTES=8388608
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 1141160,
"sha256": "d07c90a00f5bab9c583c34a9a1413bed5625542e7c88fed7bd9bab0020e9e637",
"size": 1143258,
"sha256": "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1194785,
"sha256": "344b9bb17095fef0ae3dfcee82f7144075b0c388033337552e37a2910f473dc9"
"size": 1196883,
"sha256": "2c84115b87ed4bcd4fdf4d64d56fc1221485cbd9822cfeb0770b07b9b4226e51"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 1141160,
"sha256": "d07c90a00f5bab9c583c34a9a1413bed5625542e7c88fed7bd9bab0020e9e637",
"size": 1143258,
"sha256": "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 1141160,
"sha256": "d07c90a00f5bab9c583c34a9a1413bed5625542e7c88fed7bd9bab0020e9e637",
"size": 1143258,
"sha256": "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738",
"executable": false
},
{
+2 -2
View File
@@ -741,6 +741,7 @@
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Log: /tmp/retrobios-x/install.log
@@ -773,7 +774,6 @@
^[[0;1;35mA/Enter^[[0m Exit ^[[0;2mY/o^[[0m Details
@@ -1026,7 +1026,6 @@
=== result-details
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Details^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
@@ -1061,6 +1060,7 @@
^[[0;1;35mA/Enter^[[0m Back
+2 -2
View File
@@ -238,12 +238,12 @@
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
BIOS folder:
^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
Log: /tmp/retrobios-x/install.log
^[[0;1;35mA^[[0m Exit ^[[0;2mY^[[0m Details
=== error
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
@@ -326,7 +326,6 @@
=== result-details
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Details^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore .../2026-10-23_1200
^[[0;2m Shortened to fit this window.^[[0m
@@ -335,4 +334,5 @@
--platform retroarch \
--dest .../retroarch/system
^[[0;2m Shortened to fit this window.^[[0m
^[[0;1;35mA^[[0m Back
+2 -2
View File
@@ -292,6 +292,7 @@
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
BIOS folder:
^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
@@ -300,7 +301,6 @@
^[[0;1;35mA^[[0m Exit ^[[0;2mY^[[0m Details
=== error
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
@@ -401,7 +401,6 @@
=== result-details
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Details^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore .../2026-10-23_1200
^[[0;2m Shortened to fit this window.^[[0m
@@ -413,4 +412,5 @@
^[[0;1;35mA^[[0m Back
+2 -2
View File
@@ -453,6 +453,7 @@
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
@@ -471,7 +472,6 @@
^[[0;1;35mA/Enter^[[0m Exit ^[[0;2mY/o^[[0m Details
=== error
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
@@ -626,7 +626,6 @@
=== result-details
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Details^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
@@ -647,4 +646,5 @@
^[[0;1;35mA/Enter^[[0m Back
+1 -1
View File
@@ -2,6 +2,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Log: /tmp/retrobios-x/install.log
@@ -34,7 +35,6 @@
A/Enter Exit Y/o Details
+1 -1
View File
@@ -3,10 +3,10 @@
3 installed, 1 failed.
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
Log: /tmp/retrobios-x/install.log
A Exit Y Details
+1 -1
View File
@@ -3,6 +3,7 @@
3 installed, 1 failed.
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
@@ -11,5 +12,4 @@
A Exit Y Details
+1 -1
View File
@@ -2,6 +2,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
@@ -20,5 +21,4 @@
A/Enter Exit Y/o Details
+1 -1
View File
@@ -1,6 +1,5 @@
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
@@ -35,6 +34,7 @@
A/Enter Back
+1 -1
View File
@@ -1,6 +1,5 @@
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore .../2026-10-23_1200
Shortened to fit this window.
@@ -9,4 +8,5 @@
--platform retroarch \
--dest .../retroarch/system
Shortened to fit this window.
A Back
+1 -1
View File
@@ -1,6 +1,5 @@
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore .../2026-10-23_1200
Shortened to fit this window.
@@ -12,4 +11,5 @@
A Back
+1 -1
View File
@@ -1,6 +1,5 @@
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
@@ -21,4 +20,5 @@
A/Enter Back
+4 -4
View File
@@ -888,6 +888,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
@@ -906,7 +907,6 @@
A Exit Y Details
=== error 40x12
RetroBIOS: Something went wrong
@@ -1132,7 +1132,7 @@
=== result-details 40x12
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
@@ -1145,7 +1145,7 @@
=== result-details 40x15
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
@@ -1161,7 +1161,7 @@
=== result-details 80x24
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
+4 -4
View File
@@ -888,6 +888,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
@@ -906,7 +907,6 @@
A Exit Y Details
=== error 40x12
RetroBIOS: Something went wrong
@@ -1132,7 +1132,7 @@
=== result-details 40x12
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
@@ -1145,7 +1145,7 @@
=== result-details 40x15
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
@@ -1161,7 +1161,7 @@
=== result-details 80x24
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
+4 -4
View File
@@ -888,6 +888,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
@@ -906,7 +907,6 @@
A Exit Y Details
=== error 40x12
RetroBIOS: Something went wrong
@@ -1132,7 +1132,7 @@
=== result-details 40x12
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
@@ -1145,7 +1145,7 @@
=== result-details 40x15
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
@@ -1161,7 +1161,7 @@
=== result-details 80x24
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
+6 -6
View File
@@ -922,12 +922,12 @@
3 installed, 1 failed.
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
Log: /tmp/retrobios-x/install.log
Enter Exit o Details
=== result 40x15
RetroBIOS: Finished with errors
@@ -935,6 +935,7 @@
3 installed, 1 failed.
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
@@ -943,13 +944,13 @@
Enter Exit o Details
=== result 80x24
RetroBIOS: Finished with errors
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
@@ -968,7 +969,6 @@
Enter Exit o Details
=== error 40x12
RetroBIOS: Something went wrong
@@ -1297,7 +1297,6 @@
=== result-details 40x12
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore .../2026-10-23_1200
Shortened to fit this window.
@@ -1306,11 +1305,11 @@
--platform retroarch \
--dest .../retroarch/system
Shortened to fit this window.
Enter Back
=== result-details 40x15
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore .../2026-10-23_1200
Shortened to fit this window.
@@ -1322,11 +1321,11 @@
Enter Back
=== result-details 80x24
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
To put them back: python3 install.py \
--restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
@@ -1347,4 +1346,5 @@
Enter Back
+6 -6
View File
@@ -918,12 +918,12 @@
3 installed, 1 failed.
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
Log: /tmp/retrobios-x/install.log
A Exit L2 Details
=== result 40x15
RetroBIOS: Finished with errors
@@ -931,6 +931,7 @@
3 installed, 1 failed.
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
@@ -939,13 +940,13 @@
A Exit L2 Details
=== result 80x24
RetroBIOS: Finished with errors
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
@@ -964,7 +965,6 @@
A Exit L2 Details
=== error 40x12
RetroBIOS: Something went wrong
@@ -1235,7 +1235,7 @@
=== result-details 40x12
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
@@ -1248,7 +1248,7 @@
=== result-details 40x15
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
@@ -1264,7 +1264,7 @@
=== result-details 80x24
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
+6 -6
View File
@@ -976,12 +976,12 @@
3 installed, 1 failed.
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
Log: /tmp/retrobios-x/install.log
Start Exit
=== result 40x15
RetroBIOS: Finished with errors
@@ -989,6 +989,7 @@
3 installed, 1 failed.
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
@@ -997,13 +998,13 @@
Start Exit
=== result 80x24
RetroBIOS: Finished with errors
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
@@ -1022,7 +1023,6 @@
Start Exit
=== error 40x12
RetroBIOS: Something went wrong
@@ -1351,7 +1351,7 @@
=== result-details 40x12
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
@@ -1364,7 +1364,7 @@
=== result-details 40x15
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
@@ -1380,7 +1380,7 @@
=== result-details 80x24
RetroBIOS: Details
failed: scph5500.bin (HTTP 404)
Nothing more to say here.
+1 -2
View File
@@ -4979,9 +4979,8 @@ class TheInstallerSaysWhatItFound(unittest.TestCase):
(dest / "lynxboot.img").write_bytes(b"corrupt")
proc = self._run("--dest", str(dest), "--system", "atari-lynx", "--check")
self.assertEqual(proc.returncode, 3)
# A wrong copy is named in every mode: it is what the user acts on.
self.assertIn("1 file to download.", proc.stdout)
self.assertNotIn("lynxboot.img", proc.stdout)
proc = self._run("--dest", str(dest), "--system", "atari-lynx", "--check", "--verbose")
self.assertIn("wrong copy: lynxboot.img", proc.stdout)
self.assertIn("without --check", proc.stdout)
+27
View File
@@ -452,6 +452,33 @@ class LineModeJourneys(unittest.TestCase):
self.assertLessEqual(shown, said, sorted(shown - said))
self.assertGreater(len(said), len(shown))
def test_what_to_act_on_is_said_by_default(self):
"""A file an emulator needs that the collection lacks, a wrong copy
and what becomes of it: said without --verbose (review of
2026-11-10). The plain missing files wait on --verbose."""
repo = Repository(self)
repo.listed["omitted_files"] = [{"dest": "lost.bin", "name": "lost.bin",
"system": "nintendo-gba", "systems": ["nintendo-gba"],
"required": True, "reason": "not_found"}]
repo.write_list()
folder = repo.root / "dest"
base = ["--platform", "retroarch", "--dest", str(folder)]
needs = "1 file an emulator needs is not available here (--verbose names it)."
proc = self.run_line(repo, *base, "--yes")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn(needs, proc.stdout)
(folder / "md_eu.bin").write_bytes(b"changed")
(folder / "gba_bios.bin").unlink()
proc = self.run_line(repo, *base, "--check")
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
self.assertIn("wrong copy: md_eu.bin", proc.stdout)
self.assertIn("A wrong copy is replaced, and the one in place kept in "
"retrobios-backup", proc.stdout)
self.assertIn(needs, proc.stdout)
self.assertNotIn("missing: gba_bios.bin", proc.stdout)
self.assertIn("missing: gba_bios.bin",
self.run_line(repo, *base, "--check", "--verbose").stdout)
if __name__ == "__main__":
unittest.main()
+99 -11
View File
@@ -3038,16 +3038,19 @@ class SaveDirectoryLeftOut(unittest.TestCase):
"""RetroPie's list leaves its save files out with no hash or address:
--save-dir installs none of them, and was advised again each run."""
state, summary = self._ended(MANIFEST)
line = ("2 files read from RetroArch's save directory left out: RetroArch keeps "
"it in no fixed place, and its file list holds none of them.")
# The count on the screen, why on Details.
lines = ["2 files read from RetroArch's save directory left out.",
"RetroArch keeps it in no fixed place, and its file list holds none of them."]
plan = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
self.assertIn(line, plan)
self.assertIn(" ".join(lines), plan)
self.assertNotIn("--save-dir", plan)
state.stack.append("result")
state.code = 0
state.summary = {**summary, "mode": "install", "installed": 5}
self.assertEqual([text for text in install.tui_outcome(state)[1] if "save" in text],
[line])
outcome = install.tui_outcome(state)[1]
self.assertEqual([text for text in outcome if "save" in text or "place" in text],
lines)
self.assertTrue(install._is_detail(outcome[outcome.index(lines[1])]))
@in_full
def test_files_left_out_of_the_save_directory_are_said(self):
@@ -3065,12 +3068,12 @@ class SaveDirectoryLeftOut(unittest.TestCase):
with mock.patch.object(install, "_tui_folder_facts", return_value={
"free": 50 * 1024 ** 3, "writable": True, "cores": []}):
install.tui_receive(state, ("ended", state.run_id, 1, []))
line = ("2 files read from RetroArch's save directory left out: its place is "
"not known. ")
line = "2 files read from RetroArch's save directory left out. "
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 (--save-dir).", plan)
self.assertIn(line + "Choose it in Options, Save folder (--save-dir). Its place is "
"not known.", plan)
state.stack.append("result")
state.code = 0
state.summary = {**summary, "mode": "install", "installed": 5}
@@ -14933,7 +14936,8 @@ class GamesEndToEnd(unittest.TestCase):
# path is no word. The plan, Confirm and the result say what happens, where
# and what next; their counts, notes and commands wait on Details. A list
# shows more rows on a taller window. A change that adds words to a screen
# raises its numbers here and says why.
# raises its numbers here and says why: the result names its failed files
# (review of 2026-11-10), what the user acts on.
SCREEN_WORDS = {
"setup": (29, 36, 34, 34), "platforms": (23, 27, 89, 225), "folder": (27, 27, 25, 25),
"folder-card": (52, 52, 50, 50), "browse": (17, 17, 15, 15), "path": (23, 23, 25, 25),
@@ -14941,10 +14945,10 @@ SCREEN_WORDS = {
"plan-in-place": (22, 22, 19, 19), "systems": (36, 37, 36, 36),
"options": (40, 42, 59, 59), "regions": (45, 44, 42, 42), "cores": (47, 47, 45, 45),
"preview": (29, 29, 28, 28), "command": (22, 22, 16, 16), "confirm": (22, 22, 20, 20),
"installing": (32, 32, 31, 31), "result": (29, 29, 27, 27), "error": (37, 34, 32, 31),
"installing": (32, 32, 31, 31), "result": (33, 33, 31, 31), "error": (37, 34, 32, 31),
"quit": (14, 14, 12, 12), "help": (24, 26, 26, 26), "plan-games": (36, 46, 54, 54),
"games": (36, 45, 44, 44), "plan-details": (30, 30, 29, 29),
"result-details": (34, 34, 23, 23),
"result-details": (30, 30, 19, 19),
}
@@ -15072,5 +15076,89 @@ class DetailsScreen(unittest.TestCase):
self.assertIn("ok gba_bios.bin", self.text(state))
class WhatToActOnStaysOnTheScreen(unittest.TestCase):
"""A failure, a file an emulator needs that the collection lacks, a
remedy to choose: on the screen by default, never only on Details
(review of 2026-11-10)."""
def setUp(self):
patcher = mock.patch.object(install, "_mounted_cards", return_value=[])
patcher.start()
self.addCleanup(patcher.stop)
def text(self, state: install.TuiState) -> str:
return " ".join(" ".join(install.tui_view(state, 80, 24)).split())
def test_a_failed_file_is_named_on_the_result_and_after_exit(self):
state = golden_states_result()
self.assertIn("failed: scph5500.bin (HTTP 404)", self.text(state))
press(state, "enter")
self.assertIn("failed: scph5500.bin (HTTP 404)", "\n".join(install.tui_farewell(state)))
def needs(self, state: install.TuiState) -> install.TuiState:
state.manifest = {**MANIFEST, "omitted_files": [
{"dest": "lost.bin", "system": "sony-playstation", "required": True},
{"dest": "spare.bin", "system": "sony-playstation", "required": False}]}
return state
def test_a_file_an_emulator_needs_is_counted_on_the_plan(self):
state = self.needs(checked_state())
said = "1 file an emulator needs is not available here (Details names it)."
self.assertIn(said, self.text(state))
press(state, "d")
self.assertIn("lost.bin", self.text(state))
# With --verbose the names are on the plan, and the count not again.
state = self.needs(checked_state())
state.passthrough = [("--verbose", None)]
self.assertNotIn(said, self.text(state))
self.assertIn("lost.bin", self.text(state))
def test_a_file_an_emulator_needs_is_counted_on_the_result(self):
state = self.needs(result_states()["done"])
self.assertIn("1 file an emulator needs is not available here (Details names it).",
self.text(state))
def test_the_save_folder_remedy_is_on_the_result(self):
state = result_states()["done"]
state.manifest = {**MANIFEST, "save_directory": {"folder": "saves", "by_core": True}}
state.summary["skipped"] = [{"reason": "no_save_directory", "files": 2}]
text = self.text(state)
self.assertIn("2 files read from RetroArch's save directory left out. "
f"{install.TUI_SAVE_REMEDY}", text)
self.assertNotIn("Its place is not known.", text)
press(state, "o")
self.assertIn("Its place is not known.", self.text(state))
def test_the_save_folder_remedy_is_on_the_plan(self):
state = checked_state()
state.manifest = {**MANIFEST, "save_directory": {"folder": "saves", "by_core": True}}
state.check_summary = {"skipped": [{"reason": "no_save_directory", "files": 2}]}
self.assertIn("Choose it in Options, Save folder (--save-dir).", self.text(state))
def test_files_that_could_not_be_read_are_counted_where_no_row_says_so(self):
unread = [{"event": "unreadable", "platform": "retroarch", "folder": "/media/OLD",
"path": "/media/OLD/scph5501.bin", "member": "",
"reason": "Permission denied"}]
state = checked_state()
state.unreadable = list(unread)
# The plan's row names them: the count waits on Details.
self.assertIn("Show the files that could not be read", self.text(state))
# Everything in place: no row, the count on the plan.
done = checked_state()
done.check = {**CHECK, "present": 5, "missing": 0, "wrong_hash": 0,
"bytes_needed": 0, "not_installable": 0}
done.unreadable = list(unread)
self.assertNotIn("unreadable", [row.action for row in install.tui_rows(done)])
self.assertIn("1 file in OLD could not be read.", self.text(done))
def test_another_drive_to_add_is_said_on_the_plan(self):
state = checked_state()
state.scanned = [{"event": "scanned", "folder": "E:\\", "files": 200000,
"truncated": True, "other_drives": ["E:\\D"]}]
text = self.text(state)
self.assertIn("E:\\D is another drive: add it to copy from it too.", text)
self.assertNotIn("Read the first", text)
if __name__ == "__main__":
unittest.main()
+3 -1
View File
@@ -454,7 +454,8 @@ The figures change with each release.
<!-- /generated: run-date -->
By default a run says five things: what it installs, where, a progress line
at a terminal, the result and the next step.
at a terminal, the result and the next step. A failure, a wrong copy or a file
an emulator needs that is not available is said too.
<!-- generated: run-default -->
@@ -463,6 +464,7 @@ Installing 18175 files (9.2 GB) for Batocera.
BIOS folder: /userdata/bios
Done. 18175 files installed.
80 files an emulator needs are not available here (--verbose names them).
Next: start Batocera and play.
```
<!-- /generated: run-default -->