From b82c4be6c9fb00e55a1f6587163cf78740d8c004 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 01:46:23 +0100 Subject: [PATCH] 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. --- install.ps1 | 2 +- install.py | 101 +++++++++++----- install.sh | 2 +- install/launchers/index.json | 16 +-- tests/fixtures/tui/colour-120x40.txt | 4 +- tests/fixtures/tui/colour-40x12.txt | 4 +- tests/fixtures/tui/colour-40x15.txt | 4 +- tests/fixtures/tui/colour-80x24.txt | 4 +- tests/fixtures/tui/result-120x40.txt | 2 +- tests/fixtures/tui/result-40x12.txt | 2 +- tests/fixtures/tui/result-40x15.txt | 2 +- tests/fixtures/tui/result-80x24.txt | 2 +- tests/fixtures/tui/result-details-120x40.txt | 2 +- tests/fixtures/tui/result-details-40x12.txt | 2 +- tests/fixtures/tui/result-details-40x15.txt | 2 +- tests/fixtures/tui/result-details-80x24.txt | 2 +- tests/fixtures/tui/route-evmapy.txt | 8 +- tests/fixtures/tui/route-gptokeyb.txt | 8 +- tests/fixtures/tui/route-gptokeyb_muxterm.txt | 8 +- tests/fixtures/tui/route-keys.txt | 12 +- tests/fixtures/tui/route-miyoo.txt | 12 +- tests/fixtures/tui/route-muxterm.txt | 12 +- tests/test_install.py | 3 +- tests/test_install_journeys.py | 27 +++++ tests/test_install_tui.py | 110 ++++++++++++++++-- wiki/installer.md | 4 +- 26 files changed, 259 insertions(+), 98 deletions(-) diff --git a/install.ps1 b/install.ps1 index 5d08082f3..4ab6b372f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -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 diff --git a/install.py b/install.py index 6f1431495..13d713275 100644 --- a/install.py +++ b/install.py @@ -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. diff --git a/install.sh b/install.sh index 4c22d6cb4..c8bdfb3e8 100755 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/install/launchers/index.json b/install/launchers/index.json index 4b00af4b4..2bd3d1a2b 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -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 }, { diff --git a/tests/fixtures/tui/colour-120x40.txt b/tests/fixtures/tui/colour-120x40.txt index 66389a8c2..9cf8474af 100644 --- a/tests/fixtures/tui/colour-120x40.txt +++ b/tests/fixtures/tui/colour-120x40.txt @@ -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 diff --git a/tests/fixtures/tui/colour-40x12.txt b/tests/fixtures/tui/colour-40x12.txt index cfdbdf301..2c14f4510 100644 --- a/tests/fixtures/tui/colour-40x12.txt +++ b/tests/fixtures/tui/colour-40x12.txt @@ -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 diff --git a/tests/fixtures/tui/colour-40x15.txt b/tests/fixtures/tui/colour-40x15.txt index 4772e545b..9ec9e5490 100644 --- a/tests/fixtures/tui/colour-40x15.txt +++ b/tests/fixtures/tui/colour-40x15.txt @@ -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 diff --git a/tests/fixtures/tui/colour-80x24.txt b/tests/fixtures/tui/colour-80x24.txt index f86488285..64a66ec3e 100644 --- a/tests/fixtures/tui/colour-80x24.txt +++ b/tests/fixtures/tui/colour-80x24.txt @@ -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 diff --git a/tests/fixtures/tui/result-120x40.txt b/tests/fixtures/tui/result-120x40.txt index 628078bd4..339c84f33 100644 --- a/tests/fixtures/tui/result-120x40.txt +++ b/tests/fixtures/tui/result-120x40.txt @@ -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 diff --git a/tests/fixtures/tui/result-40x12.txt b/tests/fixtures/tui/result-40x12.txt index d0c87bb72..9137c0bd5 100644 --- a/tests/fixtures/tui/result-40x12.txt +++ b/tests/fixtures/tui/result-40x12.txt @@ -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 diff --git a/tests/fixtures/tui/result-40x15.txt b/tests/fixtures/tui/result-40x15.txt index 395f4cf8e..ee783f715 100644 --- a/tests/fixtures/tui/result-40x15.txt +++ b/tests/fixtures/tui/result-40x15.txt @@ -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 diff --git a/tests/fixtures/tui/result-80x24.txt b/tests/fixtures/tui/result-80x24.txt index ca5097819..d9de0e4ce 100644 --- a/tests/fixtures/tui/result-80x24.txt +++ b/tests/fixtures/tui/result-80x24.txt @@ -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 diff --git a/tests/fixtures/tui/result-details-120x40.txt b/tests/fixtures/tui/result-details-120x40.txt index 1896a865d..f87873e51 100644 --- a/tests/fixtures/tui/result-details-120x40.txt +++ b/tests/fixtures/tui/result-details-120x40.txt @@ -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 diff --git a/tests/fixtures/tui/result-details-40x12.txt b/tests/fixtures/tui/result-details-40x12.txt index 063668de9..d2702b407 100644 --- a/tests/fixtures/tui/result-details-40x12.txt +++ b/tests/fixtures/tui/result-details-40x12.txt @@ -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 diff --git a/tests/fixtures/tui/result-details-40x15.txt b/tests/fixtures/tui/result-details-40x15.txt index efd9f6523..ae7bf6663 100644 --- a/tests/fixtures/tui/result-details-40x15.txt +++ b/tests/fixtures/tui/result-details-40x15.txt @@ -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 diff --git a/tests/fixtures/tui/result-details-80x24.txt b/tests/fixtures/tui/result-details-80x24.txt index 9d05dd8aa..8486e650c 100644 --- a/tests/fixtures/tui/result-details-80x24.txt +++ b/tests/fixtures/tui/result-details-80x24.txt @@ -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 diff --git a/tests/fixtures/tui/route-evmapy.txt b/tests/fixtures/tui/route-evmapy.txt index b8a915afb..d799d290a 100644 --- a/tests/fixtures/tui/route-evmapy.txt +++ b/tests/fixtures/tui/route-evmapy.txt @@ -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. diff --git a/tests/fixtures/tui/route-gptokeyb.txt b/tests/fixtures/tui/route-gptokeyb.txt index f1179b688..9e9647ae4 100644 --- a/tests/fixtures/tui/route-gptokeyb.txt +++ b/tests/fixtures/tui/route-gptokeyb.txt @@ -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. diff --git a/tests/fixtures/tui/route-gptokeyb_muxterm.txt b/tests/fixtures/tui/route-gptokeyb_muxterm.txt index c285fd3d9..9390cd9a6 100644 --- a/tests/fixtures/tui/route-gptokeyb_muxterm.txt +++ b/tests/fixtures/tui/route-gptokeyb_muxterm.txt @@ -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. diff --git a/tests/fixtures/tui/route-keys.txt b/tests/fixtures/tui/route-keys.txt index 557245079..571fc7b59 100644 --- a/tests/fixtures/tui/route-keys.txt +++ b/tests/fixtures/tui/route-keys.txt @@ -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 diff --git a/tests/fixtures/tui/route-miyoo.txt b/tests/fixtures/tui/route-miyoo.txt index 9c0dc475a..d828a7dac 100644 --- a/tests/fixtures/tui/route-miyoo.txt +++ b/tests/fixtures/tui/route-miyoo.txt @@ -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. diff --git a/tests/fixtures/tui/route-muxterm.txt b/tests/fixtures/tui/route-muxterm.txt index f118e37e6..397254a58 100644 --- a/tests/fixtures/tui/route-muxterm.txt +++ b/tests/fixtures/tui/route-muxterm.txt @@ -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. diff --git a/tests/test_install.py b/tests/test_install.py index 41e7aab15..cd75993d7 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -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) diff --git a/tests/test_install_journeys.py b/tests/test_install_journeys.py index 653784f64..1582290c6 100644 --- a/tests/test_install_journeys.py +++ b/tests/test_install_journeys.py @@ -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() diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index b0aaa73ce..017a46519 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -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() diff --git a/wiki/installer.md b/wiki/installer.md index 55c2899a9..99a3cc53f 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -454,7 +454,8 @@ The figures change with each release. 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. @@ -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. ```