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. ```