From 36745abf10787f95ad081ea7e88006791967aa11 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 00:21:37 +0100 Subject: [PATCH 1/4] feat: line mode says details only with --verbose By default a run prints what it installs, the BIOS folder, the progress at a terminal, the result and the next step. The steps, the per-file log lines, the counts and the reasons a file is left out come back with --verbose. --check gives the count and the folder, and lists each file with --verbose. The installer page shows the default run and walks through the --verbose one. --- install.ps1 | 2 +- install.py | 233 +++++++++++++++++++----------- install.sh | 2 +- install/launchers/index.json | 16 +- scripts/generate_installer_doc.py | 24 +-- tests/test_doc_pages.py | 2 +- tests/test_install.py | 44 ++++-- tests/test_install_cores.py | 4 +- tests/test_install_experience.py | 17 ++- tests/test_install_filesystems.py | 4 +- tests/test_install_from.py | 8 +- tests/test_install_games.py | 5 +- tests/test_install_offline.py | 12 +- tests/test_install_selection.py | 10 +- tests/test_install_tui.py | 21 ++- wiki/installer.md | 29 +++- 16 files changed, 281 insertions(+), 152 deletions(-) diff --git a/install.ps1 b/install.ps1 index 55444d30e..087b4755c 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 = "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7" + $defaultInstallSha256 = "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e" # 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 a3ad31f83..5d50497f8 100644 --- a/install.py +++ b/install.py @@ -494,6 +494,25 @@ class Terminal: return self.paint(text, "strong") +class _Output: + """What a run's text says. By default: what will happen, where, the + progress, the result and the next step. --verbose adds every step, list + and reason, the lines a beginner lost the result among. where_said is + set once the BIOS folder was named before the result.""" + + verbose = False + where_said = False + + +OUTPUT = _Output() + + +def detail(*args: Any, **kwargs: Any) -> None: + """Print a line only --verbose shows.""" + if OUTPUT.verbose: + print(*args, **kwargs) + + def format_duration(seconds: float) -> str: """0:42, 3:05 or 1:02:09.""" seconds = max(0, int(seconds + 0.5)) @@ -521,8 +540,8 @@ class Progress: """Bytes and files across the parallel downloads, shown as they move. On a terminal one line redraws in place: a bar, the bytes, the speed and - the time left. Elsewhere each finished file gets its line, as a log - wants it. Failures are always printed, quiet or not. + the time left. Elsewhere each finished file gets its line with --verbose, + as a log wants it. Failures are always printed, quiet or not. """ BAR = 24 @@ -601,7 +620,7 @@ class Progress: why = f" ({reason})" if reason else "" line = f" {self.term.bad('FAILED')} {dest}{why}" SAID_FAILED.add(dest) - elif not self.term.live and not self.term.quiet: + elif not self.term.live and not self.term.quiet and OUTPUT.verbose: line = f" [{self.files}/{self.total_files}] {dest} {self.term.ok('ok')}" if line is not None: self._clear() @@ -5228,7 +5247,7 @@ def say_saves(plat: str, files: list[dict], omitted: list[dict], bios_path: Path if printed: return for line in save_lines([], omitted, bios_path, display): - print(f" {line}") + detail(f" {line}") # The digests the line menu's head read of the files in place, by path and @@ -5701,8 +5720,9 @@ def _kept_lines() -> list[str]: lines.append(f"Kept {_plural(item['files'], 'file')} already there " f"({format_size(item['bytes'])}), which the collection's " f"copies replaced, in {folder}") - lines.append(f" To put them back: {_INVOCATION.launcher()} --restore " - f"{_INVOCATION.word(str(folder))}") + if OUTPUT.verbose: + lines.append(f" To put them back: {_INVOCATION.launcher()} --restore " + f"{_INVOCATION.word(str(folder))}") return lines @@ -6634,7 +6654,7 @@ def _report_outcome( # The command making the same choice ends every result the screen ends # with it: "Finished with files missing" gave none. same_lines = ([f"Same choice without questions: {screen_text(same_choice)}"] - if same_choice else []) + if same_choice and OUTPUT.verbose else []) # A file --offline left out is not a failure, whether the run asked or # not: without questions it was "14698 files failed", and "Run the same # command again to retry only those", which cannot fetch them. @@ -6644,7 +6664,7 @@ def _report_outcome( if not isinstance(failure[1], OfflineReason)] errors = max(0, errors - len(left)) offline += len(left) - if transferred and seconds > 0: + if transferred and seconds > 0 and OUTPUT.verbose: verb = "copied from the pack" if LOCAL.pack is not None else "downloaded" say(f"\n{format_size(transferred)} {verb} in " f"{format_duration(seconds)} ({format_size(int(transferred / seconds))}/s).") @@ -6723,7 +6743,7 @@ def _report_outcome( # The kinds installed, on every end that installed some, under where # they came from: as the screen's result says them. installed_kinds = (f" Installed: {installed_words(kinds[0].bios, kinds[0].games, kinds[1])}." - if kinds and kinds[0].files and downloaded else "") + if kinds and kinds[0].files and downloaded and OUTPUT.verbose else "") if offline: remedy = _offline_remedy() if not downloaded and not errors: @@ -6829,25 +6849,43 @@ def _report_outcome( say(f" {sources}") if installed_kinds: say(installed_kinds) - say(f"BIOS folder: {_locations(paths)}") + if OUTPUT.verbose or not OUTPUT.where_said: + say(f"BIOS folder: {_locations(paths)}") for line in kept: say(line) - if omitted > len(unplaced): + 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"], Path(paths[0]) if len(paths) == 1 else None): say(line) + # RomM's scan is the step after the install: the other hints say how to + # install less, a detail. for line in hints or []: - say(line) - if same_choice: + if OUTPUT.verbose or line == ROMM_SCAN_WORDS: + say(line) + if same_choice and OUTPUT.verbose: # After the questions, as the screen's result ends: "the same # command" was one that asked them. for line in same_lines: say(line) say("To check this later, run that command with --check.") - return - say("To check this later, run the same command with --check.") + elif OUTPUT.verbose: + say("To check this later, run the same command with --check.") + say(next_step_words(platform_of or {}, paths)) + + +def next_step_words(platform_of: dict[Path, str], paths: list[Path]) -> str: + """The step after an install that ended well: a card goes back in its + device, a setup on this machine is started.""" + setups = [(platform_of.get(path, ""), path) for path in paths] + if setups and all(plat and on_its_card(plat, path) for plat, path in setups): + return "Next: put the card back in the device and start a game." + names = list(dict.fromkeys(detected_os(plat, path) or display_name(plat) + for plat, path in setups if plat)) + if len(names) == 1: + return f"Next: start {names[0]} and play." + return "Next: start your emulators and play." def _offline_remedy(folders: list[str] | None = None, named: bool = False) -> str: @@ -6904,8 +6942,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()}") - return (" Run the same command without --check to fetch them. A wrong copy is " - f"replaced, and the one in place kept in {BACKUP_DIR} beside {beside}.") + # Why nothing is lost goes with the details: the remedy is the line. + return (" Run the same command without --check to fetch them." + + (f" A wrong copy is replaced, and the one in place kept in {BACKUP_DIR} " + f"beside {beside}." if OUTPUT.verbose else "")) def _offline_failure(reason: str) -> bool: @@ -11335,7 +11375,7 @@ def read_games(args: argparse.Namespace, plat: str, bios_path: Path, manifest: d os_type=os_type): return None where = "on the card" if folder is None else f"in {folder}" - print(f"\nReading the games {where}...") + detail(f"\nReading the games {where}...") try: plan = plan_games(plat, bios_path, manifest, folder, term, os_type=os_type, network=network) @@ -11356,7 +11396,7 @@ def read_games(args: argparse.Namespace, plat: str, bios_path: Path, manifest: d return None emit_games(plat, plan, bool(args.required_only)) for line in games_lines(plan, manifest, bool(args.verbose)): - print(fold_line(line if line.startswith(" ") else f" {line}")) + detail(fold_line(line if line.startswith(" ") else f" {line}")) return plan @@ -14017,6 +14057,9 @@ def _run(args: argparse.Namespace) -> None: raise finally: LOCAL.close_indexes() + # A caller running the installer in its own process gets the next + # run's text as quiet as any first run. + OUTPUT.verbose = OUTPUT.where_said = False def _run_text(args: argparse.Namespace) -> None: @@ -14268,8 +14311,10 @@ def _missing_dest(dest: Path, ask: bool, named: str = "named with --dest", while not nearest.exists() and nearest.parent != nearest: nearest = nearest.parent missing = len(dest.absolute().relative_to(nearest).parts) - print(f" {shown_path(dest)} does not exist. The nearest folder that does: " - f"{shown_path(nearest)}") + # Said where it comes before a question: elsewhere the folder is made. + (print if missing > 1 and ask else detail)( + f" {shown_path(dest)} does not exist. The nearest folder that does: " + f"{shown_path(nearest)}") if missing > 1 and ask: answer = _ask(f" Create {_plural(missing, 'folder')} under {shown_path(nearest)}? [y/N] ") if not answer.strip().lower().startswith("y"): @@ -15334,7 +15379,9 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: print(f"{where}, so nothing is asked: the run goes on as with --no-input. " f"Add --no-input to say so, or run it without {undo} to answer.", file=sys.stderr) - print(f"{term.bold('RetroBIOS')} {term.dim(installer_id())}\n") + OUTPUT.verbose = bool(getattr(args, "verbose", False)) + OUTPUT.where_said = False + detail(f"{term.bold('RetroBIOS')} {term.dim(installer_id())}\n") os_type = detect_os() if os_type == "wsl": @@ -15382,7 +15429,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: if args.platform and args.dest: platforms = [(args.platform, args.dest)] elif args.platform: - print("Looking for setups here...") + detail("Looking for setups here...") if not listing: # The cards mounted here too, as the screen finds them, with or # without questions: a pack's RetroArch list went to this @@ -15401,7 +15448,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: platforms = matched offer = getattr(args, "platform_from_list", False) for name, path in matched: - print(fold_line(f" Found {found_label(name, path)}")) + detail(fold_line(f" Found {found_label(name, path)}")) elif listing: # A listing reads the manifest and writes nothing: no folder is # needed, and asking BizHawk's for one refused a question asked @@ -15501,16 +15548,16 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: platforms = [(args.platform, default_dest)] assumed_dests.add(default_dest) elif args.dest: - print(f" Using destination: {args.dest}") + detail(f" Using destination: {args.dest}") plat, args.dest = _platform_for_dest(os_type, args.dest, interactive and not listing) platforms = [(plat, args.dest)] else: - print("Looking for setups here...") + detail("Looking for setups here...") frontends = detect_frontends(os_type) if "esde" in frontends: - print(" Found ES-DE (BIOS files go to each emulator, not to ES-DE itself)") + detail(" Found ES-DE (BIOS files go to each emulator, not to ES-DE itself)") if "launchbox" in frontends: - print(" Found LaunchBox") + detail(" Found LaunchBox") if not listing: # A card mounted here is found as the screen finds it, in every # mode: line mode said "No supported platform detected" beside @@ -15553,7 +15600,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: offer = False else: for name, path in platforms: - print(fold_line(f" Found {found_label(name, path)}")) + detail(fold_line(f" Found {found_label(name, path)}")) if len(platforms) > 1 and not interactive and not listing: # A run without questions installs for one setup, the one a person @@ -15674,11 +15721,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: if bios_path not in installed_paths: installed_paths.append(bios_path) if LOCAL.manifest is not None: - print(f"\nReading the file index for {plat_name} from {LOCAL.manifest}...") + detail(f"\nReading the file index for {plat_name} from {LOCAL.manifest}...") elif LOCAL.pack is not None: - print(f"\nReading the file index for {plat_name} from {LOCAL.pack.name}...") + detail(f"\nReading the file index for {plat_name} from {LOCAL.pack.name}...") else: - print(f"\nFetching file index for {plat_name}...") + detail(f"\nFetching file index for {plat_name}...") manifest = fetch_manifest(plat_name) categorized = categorized or bool(manifest.get("categorized")) files = manifest.get("files", []) @@ -15696,7 +15743,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: # spelling overwrote the first, and every --check found one # of them wrong and fetched it again. shown = ", ".join(folded[:3]) + (" ..." if len(folded) > 3 else "") - print( + detail( f" {_plural(len(folded), 'file')} left out: {bios_path} does not keep " f"case and holds another file under the same name ({shown})" ) @@ -15704,7 +15751,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: and not holds_fat_refused_names(bios_path): files, refused = split_fat_refused(files) shown = ", ".join(refused[:3]) + (" ..." if len(refused) > 3 else "") - print( + detail( f" {_plural(len(refused), 'file')} left out: the file system of " f"{shown_path(bios_path)} cannot hold a name with any of " f"{FAT_REFUSED_SHOWN} ({screen_text(shown)})" @@ -15722,7 +15769,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: if args.declared_only: before = len(files) files, omitted_files = keep_declared(files, omitted_files) - print(f" The platform's own list: {before} to {len(files)} files " + detail(f" The platform's own list: {before} to {len(files)} files " "(--declared-only)") if args.list_targets: @@ -15761,7 +15808,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: words = (f"Board {board} {where} has no " f"{PLATFORM_NAMES.get(plat_name, plat_name)} core list: " "installing the files of every board") - print(f" {words}") + detail(f" {words}") EVENTS.emit("board", platform=plat_name, board=board, applied=target == board, message=words) if target: @@ -15777,7 +15824,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: # the subset the target needs. Refuse rather than ignore. _unknown_target(target, plat_name, targets) elif target_info.get("cores") is None: - print( + detail( f" Target '{target}' publishes no core list: " "installing every file" ) @@ -15786,13 +15833,13 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: files, omitted_files = narrow_to_cores( manifest, target_info["cores"], files, omitted_files ) - print(f" Filtered {before} to {len(files)} files for target {target}") + detail(f" Filtered {before} to {len(files)} files for target {target}") found_cores = cores_found.get((plat_name, bios_path), []) if "system_cores" not in manifest: found_cores = [] elif found_cores: - print(f" {_plural(len(found_cores), 'core')} installed") + detail(f" {_plural(len(found_cores), 'core')} installed") if args.installed_cores: if "system_cores" not in manifest: print( @@ -15806,7 +15853,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: files, omitted_files = narrow_to_cores( manifest, found_cores, files, omitted_files ) - print(f" Narrowed {before} to {len(files)} files to the installed cores") + detail(f" Narrowed {before} to {len(files)} files to the installed cores") if args.list_emulators: _print_emulators(plat_name, manifest, *( @@ -15863,10 +15910,10 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: files, omitted_files = games_plan.apply(files, omitted_files, args.required_only) unplaced = games_plan.apply([], unplaced, args.required_only)[1] games_applied = True - print(f" For the games: {_plural(len(files), 'file')} of the list" - + (f", {GAMES_REQUIRED_WORDS}" if args.required_only else "")) + detail(f" For the games: {_plural(len(files), 'file')} of the list" + + (f", {GAMES_REQUIRED_WORDS}" if args.required_only else "")) if args.for_games is None: - print(" The card holds games, so only what they need is installed. " + detail(" The card holds games, so only what they need is installed. " "--all-files installs the whole list.") version_notes += [line.strip() for line in fallback_lines(games_plan)] # --no-game-data leaves the game data out once the list is narrowed @@ -15934,6 +15981,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: if args.no_game_data else "", None if games_applied else games_plan, ) + # Its head named the BIOS folder: the result does not again. + OUTPUT.where_said = True if selection is None and LOCAL.pack is not None: smaller = " Nothing copied." elif selection is None: @@ -15972,7 +16021,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: if len(files) != before and not menu: # Enter for every system narrows nothing: "Narrowed 23 to 23". # The menu counted the choice again before Enter. - print(f" Narrowed {before} to {len(files)} files") + detail(f" Narrowed {before} to {len(files)} files") if (cut and cut[0]) or args.no_game_data: # Left out at g or by --no-game-data, from what the choice kept: # g offered the game data of the systems chosen, and the line @@ -15985,7 +16034,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: _INVOCATION.chosen[plat_name] = (_INVOCATION.chosen.get(plat_name, []) + [("--no-game-data", None)]) for line in _left_out_lines(left_out, args.verbose): - print(line) + detail(line) if unplaced: total_omitted += len(unplaced) omitted_seen.extend(unplaced) @@ -16022,18 +16071,18 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: if not menu: # An answered menu said these lines right above: the run head # repeated them under it. - print(f" {_plural(run_counts.files + head_data, 'file')} " - f"({format_size(run_counts.size + head_bytes)})") + detail(f" {_plural(run_counts.files + head_data, 'file')} " + f"({format_size(run_counts.size + head_bytes)})") categorized_list = bool(manifest.get("categorized")) kinds = run_counts if categorized_list else tally( {"size": entry.get("size")} for entry in files) parts = parts_words(kinds, head_data, head_bytes, game_data_cut) if parts: - print(f" {parts}.") + detail(f" {parts}.") defined = kinds_definition( categorized_list and bool(run_counts.games or game_data_cut), bool(head_data)) if defined: - print(f" {defined}") + detail(f" {defined}") # A choice that keeps no file says so where "All files up to date" # was said of it. empty_choice = "" if run_counts.files or not (selection or game_data_cut) else ( @@ -16051,7 +16100,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: for entry in omitted_files: reason = entry.get("reason", "unknown") reasons[reason] = reasons.get(reason, 0) + 1 - print(f" {not_installable_words(len(omitted_files), required_omitted, reasons)}") + detail(f" {not_installable_words(len(omitted_files), required_omitted, reasons)}") total_omitted += len(omitted_files) # Named for the end of the run: ROCKNIX's own list leaves them out, # and the end said "the frontend's save directory". @@ -16071,11 +16120,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: files, library = romm_relayout(files, bios_path, *layout) if library != bios_path: installed_paths[installed_paths.index(bios_path)] = library - print(f" Firmware laid out as {'/'.join(layout[0] + ('{platform}',) + layout[1])} " - f"under {library}") + detail(f" Firmware laid out as {'/'.join(layout[0] + ('{platform}',) + layout[1])} " + f"under {library}") bios_path = library - print("\nChecking existing files...") + detail("\nChecking existing files...") to_download, up_to_date, mismatched = check_local( files, bios_path, jobs=args.jobs, term=term ) @@ -16090,9 +16139,9 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: head_reads_done() trees_missing = sum(len(state[1]) for _entry, state in data_states) trees_held = head_data - trees_missing - print(f" {len(up_to_date) + trees_held} in place, " - f"{run_counts.files - present + trees_missing} missing, " - f"{_plural(wrong, 'wrong copy')}") + detail(f" {len(up_to_date) + trees_held} in place, " + f"{run_counts.files - present + trees_missing} missing, " + f"{_plural(wrong, 'wrong copy')}") # A partial copy left by an interrupted run is useless once the file # is in place: it goes. @@ -16157,14 +16206,18 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: # One total with the count above it, the data files among # them as the screen counts them: "0 in place, 10805 missing" # stood over "8164 files to download". + # The count and the folder by default, each file with + # --verbose: 20 paths hid the line saying what to do. print(f"\n {_plural(len(to_download) + trees_missing, 'file')} " - + ("not in place:" if offline or LOCAL.pack is not None - else "to download:")) + + ("not in place" if offline or LOCAL.pack is not None + else "to download") + (":" if OUTPUT.verbose else ".")) for line in check_lines(missing, mismatched, files, manifest, bios_path, args.verbose, up_to_date, bios_index, copies): - print(line) + detail(line) for line in unreadable_lines(unread, args.verbose): print(line) + if not OUTPUT.verbose: + print(f" BIOS folder: {shown_path(bios_path)}") # The data files need the internet too: offline, "8164 files # need the internet" stood under "10805 files not in place". trees_fetched = trees_missing if LOCAL.pack is None else 0 @@ -16177,6 +16230,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: print(f"\n {_plural(trees_missing, 'data file')} to fetch") else: print(f"\n {EVERYTHING_IN_PLACE}") + print(f" BIOS folder: {shown_path(bios_path)}") 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 @@ -16185,11 +16239,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: absent, "Not in the collection and absent here, needed to start an emulator", every=args.verbose, ): - print(line) + detail(line) for entry, (_root, needed, present) in data_states: if needed: - print(f" {entry['ref']}: {present}/{len(entry['files'])} data files " - f"present, {len(needed)} to fetch") + detail(f" {entry['ref']}: {present}/{len(entry['files'])} data files " + f"present, {len(needed)} to fetch") check_pending += len(needed) continue @@ -16280,15 +16334,28 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: files_pending += len(to_download) continue EVENTS.emit("download", platform=plat_name, files=len(to_download), bytes=dl_size) + if not menu: + # What happens and where, the two lines a run without questions + # opens on: the menu's head said them where it was asked. + coming = len(to_download) - len(lacking) + trees_missing + weight = dl_size + sum(data_fetch_bytes(entry, state[1]) + for entry, state in data_states) + print(f"\nInstalling {_plural(coming, 'file')} ({format_size(weight)}) for " + f"{detected_os(plat_name, bios_path) or display}" + + (", what the games need" if games_applied else "") + ".") + print(f"BIOS folder: {shown_path(bios_path)}") + OUTPUT.where_said = True if unread: + # A file of the user's that could not be read is said in + # every mode: it was expected to be copied. print() for line in unreadable_lines(unread, args.verbose): print(line) if copies: - print() + detail() for line in copy_plan_lines(to_download, copies, bios_index, bios_path, files, args.verbose, False): - print(line) + detail(line) held = len(fetched) - len(lacking) rest = (f" and {'copying' if LOCAL.pack is not None else 'downloading'} " f"{held}" if held else "") @@ -16303,17 +16370,17 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: # The size beside what it weighs: after "and downloading # 19" it read as theirs alone. sized = f" ({format_size(dl_size)})" - print(f"\nCopying {_plural(len(copies), 'file')}{'' if rest else sized} " - f"{origin}{rest}{where}{sized if rest else ''}...") + detail(f"\nCopying {_plural(len(copies), 'file')}{'' if rest else sized} " + f"{origin}{rest}{where}{sized if rest else ''}...") elif LOCAL.pack is not None: # The files the pack holds: "Copying 23 files (85.0 KB)" sat # beside the size of 20. - print(f"\nCopying {_plural(len(to_download) - len(lacking), 'file')} " - f"({format_size(dl_size)}) from {LOCAL.pack.name}...") + detail(f"\nCopying {_plural(len(to_download) - len(lacking), 'file')} " + f"({format_size(dl_size)}) from {LOCAL.pack.name}...") else: - print(f"\nDownloading {len(to_download)} files ({format_size(dl_size)})...") - print(" Press Ctrl+C to stop. Running the same command again " - "carries on from here.") + detail(f"\nDownloading {len(to_download)} files ({format_size(dl_size)})...") + detail(" Press Ctrl+C to stop. Running the same command again " + "carries on from here.") # Checked before the folder is created: a run stopped for space # left an empty BIOS folder behind. The data trees count too, # written after the files on the same drive. @@ -16345,7 +16412,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: # of the run installed something. sys.exit(1 if total_downloaded else 2) if created: - print(f" Created {shown_path(bios_path)}") + detail(f" Created {shown_path(bios_path)}") progress = Progress(len(to_download), dl_size, term) failed, interrupted = download_files( to_download, bios_path, jobs=args.jobs, verbose=args.verbose, @@ -16404,7 +16471,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: 1 if total_downloaded else 2, on_its_card(plat_name, bios_path)) if writable: - print("\nData directories:") + detail("\nData directories:") for entry, state in writable: # Its archive is drawn as the BIOS files were, and counted # in the bytes and time the run ends on: 82 MB came in @@ -16424,9 +16491,9 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: if isinstance(counts["error"], OfflineReason): # Left out as the BIOS files above are: counted missing, # not as one failure per tree. - print(f" {entry['ref']}: {counts['present']} already in place, " - f"{agree(counts['absent'], 'needs', 'need')} the internet " - f"({offline_named()})") + detail(f" {entry['ref']}: {counts['present']} already in place, " + f"{agree(counts['absent'], 'needs', 'need')} the internet " + f"({offline_named()})") total_offline += counts["absent"] files_pending += counts["absent"] continue @@ -16442,7 +16509,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: f"{LOCAL.pack.name}" if LOCAL.pack is not None else f", {counts['changed'] + counts['absent']} no longer in " "the source (the pack has them)") - print(line) + detail(line) if counts["error"] in (NOT_WRITABLE, DRIVE_FULL): if counts["error"] == DRIVE_FULL: print(f" {entry['ref']}: {DRIVE_FULL} ({shown_path(bios_path)})", @@ -16461,8 +16528,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: total_errors += 1 if os_type == "android" and not args.check and hide_from_media_scanner(bios_path): - print(f" Added {NOMEDIA} to {shown_path(bios_path)}: the gallery and music " - "apps leave its images, sounds and videos out") + detail(f" Added {NOMEDIA} to {shown_path(bios_path)}: the gallery and music " + "apps leave its images, sounds and videos out") # Standalone copies if ( @@ -16490,11 +16557,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: manifest, os_type, launchbox_bios_dirs(lb_root) if lb_root else None ) if folders: - print("\nStandalone emulator folders found here:") + detail("\nStandalone emulator folders found here:") for folder in folders: - print(f" {folder}") - print(" --standalone-copies also copies the BIOS files and keys " - "they read into them.") + detail(f" {folder}") + detail(" --standalone-copies also copies the BIOS files and keys " + "they read into them.") sys.stdout = real_stdout if not listing: diff --git a/install.sh b/install.sh index c3482d606..36aaf3311 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="185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7" +DEFAULT_INSTALL_SHA256="00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e" # 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 3cda2e006..c056dc24c 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": 1132111, - "sha256": "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7", + "size": 1135666, + "sha256": "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1185736, - "sha256": "cb245dc33b894a9720ae3734bf55512c2b0fca99e0bd33e8c12a72738085f5cc" + "size": 1189291, + "sha256": "c56ce912e436173542905c89c1aee7de025f93bea5df130ab51e589c071f6369" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1132111, - "sha256": "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7", + "size": 1135666, + "sha256": "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1132111, - "sha256": "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7", + "size": 1135666, + "sha256": "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e", "executable": false }, { diff --git a/scripts/generate_installer_doc.py b/scripts/generate_installer_doc.py index 8414de2e0..7baf85733 100644 --- a/scripts/generate_installer_doc.py +++ b/scripts/generate_installer_doc.py @@ -444,14 +444,14 @@ class Sandbox: with zipfile.ZipFile(system / "PSX BIOS.zip", "w") as archive: archive.writestr("extra.bin", data["pcsx/extra.bin"]) # A check that flags a file ends on 3, as documented. - return self.run("retroarch", "--check", "--manifest", str(listed), home=home, - codes=(3,)) + return self.run("retroarch", "--check", "--verbose", "--manifest", str(listed), + home=home, codes=(3,)) old = home / "old" old.mkdir() for dest in ("scph5500.bin", "scph5501.bin"): (old / dest).write_bytes(data[dest]) - return self.run("retroarch", "--yes", "--manifest", str(listed), "--from", str(old), - home=home, landed=True) + return self.run("retroarch", "--yes", "--verbose", "--manifest", str(listed), + "--from", str(old), home=home, landed=True) def run(self, platform: str, *argv: str, landed: bool = False, held: bool = False, home: Path | None = None, codes: tuple = (0,)) -> str: @@ -551,7 +551,7 @@ BULK_SYSTEM = "sony-playstation" def _bulk_line(box: Sandbox | _LazySandbox) -> str: - run = box.run("retroarch", "--dry-run", "--system", BULK_SYSTEM) + run = box.run("retroarch", "--dry-run", "--verbose", "--system", BULK_SYSTEM) found = [line for line in run.splitlines() if re.match(r" \S+ pulls ", line)] if len(found) != 1: raise SampleFailed(f"a RetroArch run with --system {BULK_SYSTEM} names no core " @@ -560,11 +560,17 @@ def _bulk_line(box: Sandbox | _LazySandbox) -> str: def sample_writers(box: Sandbox) -> dict[str, Callable[[str], str]]: - """The run samples, each from the run that prints it.""" - install_run = lambda: box.run(WALKTHROUGH, "--yes", "--jobs", "1", landed=True) # noqa: E731 - check_run = lambda: box.run(WALKTHROUGH, "--check", held=True) # noqa: E731 - dry_run = lambda: box.run("retroarch", "--dry-run") # noqa: E731 + """The run samples, each from the run that prints it: the walk through a + run reads its --verbose lines, the default sample the lines it hides them + from.""" + default_run = lambda: box.run(WALKTHROUGH, "--yes", "--jobs", "1", landed=True) # noqa: E731 + install_run = lambda: box.run(WALKTHROUGH, "--yes", "--jobs", "1", "--verbose", # noqa: E731 + landed=True) + check_run = lambda: box.run(WALKTHROUGH, "--check", "--verbose", held=True) # noqa: E731 + dry_run = lambda: box.run("retroarch", "--dry-run", "--verbose") # noqa: E731 return { + "run-default": lambda _body: _fenced( + _lines_from(default_run(), "Installing ", "Next: ")), "dry-run": lambda _body: ( f"A RetroArch dry run into an empty folder, with the list of " f"{list_date('retroarch')}, printed this:\n\n" diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index 0b58a1f26..bcfda2ef0 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -201,7 +201,7 @@ class InstallerSamplesSayWhatTheCodePrints(unittest.TestCase): at = page.index("") before = page[page.rindex("\n\n", 0, at - 2):at] self.assertIn("At a terminal one progress line", before) - self.assertIn("A pipe or a log gets one line per file", before) + self.assertIn("a pipe or a log gets one\nline per file", before) def test_the_generator_pages_name_the_run_samples_and_step_7(self): tools = _page("tools.md") diff --git a/tests/test_install.py b/tests/test_install.py index 0d25cae85..eba661cfa 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -331,11 +331,16 @@ class TestRunOutcome(unittest.TestCase): return buf.getvalue(), code def test_success_names_the_location_and_the_way_to_check(self): + install.OUTPUT.verbose = install.OUTPUT.where_said = False out, code = self._report(downloaded=12, up_to_date=3) self.assertEqual(code, 0) self.assertIn("12 files installed", out) self.assertIn("/userdata/bios", out) - self.assertIn("--check", out) + self.assertIn("Next: ", out) + self.assertNotIn("--check", out) + self.addCleanup(setattr, install.OUTPUT, "verbose", False) + install.OUTPUT.verbose = True + self.assertIn("--check", self._report(downloaded=12, up_to_date=3)[0]) def test_total_failure_does_not_say_done_and_exits_two(self): out, code = self._report(errors=7) @@ -523,7 +528,12 @@ class AMissingDestIsSaid(unittest.TestCase): def test_one_missing_level_or_no_terminal_is_said_not_asked(self): root = Path(_tempdir(self)) - # A "no" that was asked would stop the run with 2. + # A "no" that was asked would stop the run with 2. Said with + # --verbose: the folder is made, and the run names it. + install.OUTPUT.verbose = False + self.assertEqual(self._call(root / "bios", True, "n"), ("", None)) + self.addCleanup(setattr, install.OUTPUT, "verbose", False) + install.OUTPUT.verbose = True out, code = self._call(root / "bios", True, "n") self.assertIsNone(code) self.assertIn("does not exist", out) @@ -1512,7 +1522,7 @@ class InstallerStaysOnPython38AndTheStdlib(unittest.TestCase): check=False, ) self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) - self.assertIn("to download:", proc.stdout) + self.assertIn("to download.", proc.stdout) def test_no_syntax_a_312_parser_lets_through(self): """Parenthesized context managers (3.9) and f-strings reusing their @@ -2157,7 +2167,7 @@ class TestLaunchboxDetectionPowershell(unittest.TestCase): proc = subprocess.run( [ "pwsh", "-NoProfile", "-File", - str(REPO_ROOT / "install.ps1"), "--standalone-copies", + str(REPO_ROOT / "install.ps1"), "--standalone-copies", "--verbose", ], env=env, capture_output=True, @@ -4163,7 +4173,7 @@ class DataDirectoriesAreInstalled(unittest.TestCase): "files": {"Sys/big.bin": {"sha1": hashlib.sha1(member).hexdigest(), "size": len(member)}}}] (repo.root / "install" / "retroarch.json").write_text(json.dumps(listed)) - proc = repo.run("--no-input") + proc = repo.run("--no-input", "--verbose") bios = sum(len(payload) for payload, *_ in FakeRepository.PAYLOADS.values()) self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn(f"{install.format_size(bios + archive)} downloaded in", proc.stdout) @@ -4447,7 +4457,7 @@ class ACheckNamesWhatTheCollectionLacks(unittest.TestCase): } proc = subprocess.run( [sys.executable, "-I", "install.py", "--platform", "recalbox", - "--no-input", "--dest", str(home / "bios"), "--check"], + "--no-input", "--dest", str(home / "bios"), "--check", "--verbose"], cwd=REPO_ROOT, env=env, capture_output=True, text=True, timeout=300, check=False, ) @@ -4969,6 +4979,9 @@ 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) + 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) @@ -4988,7 +5001,7 @@ class TheInstallerSaysWhatItFound(unittest.TestCase): def test_a_piped_log_keeps_its_order(self): proc = subprocess.run( [sys.executable, "install.py", "--platform", "retroarch", "--no-input", - "--target", "no-such-target", "--dest", str(self.home / "s")], + "--verbose", "--target", "no-such-target", "--dest", str(self.home / "s")], cwd=REPO_ROOT, env=self.env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=300, check=False, ) @@ -5653,7 +5666,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun): dest.mkdir() (dest / "scph5501.bin").write_bytes(b"junk") proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest), - "--check", "--no-input") + "--check", "--no-input", "--verbose") self.assertEqual(proc.returncode, 3, proc.stderr) listing = proc.stdout.split("to download:\n", 1)[1].splitlines() self.assertEqual(listing[0].strip(), "wrong copy: scph5501.bin") @@ -5685,7 +5698,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun): "bios/Other/ppsspp/PPSSPP/ppge_atlas.meta"): shutil.copy(REPO_ROOT / repo_path, dest / "PPSSPP") proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest), - "--system", "sony-psp", "--check", "--no-input") + "--system", "sony-psp", "--check", "--no-input", "--verbose") self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) self.assertNotIn("Every file on the list is in place", proc.stdout) found = re.search(r"(\d+) in place, (\d+) missing", proc.stdout) @@ -5699,6 +5712,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun): dest.mkdir() proc = _install_run({**self.env, "RETROBIOS_INSTALL_OFFLINE": "1"}, "--platform", "retroarch", "--dest", str(dest), "--check", "--no-input", + "--verbose", "--manifest", str(REPO_ROOT / "install" / "retroarch.json")) self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) missing = re.search(r"0 in place, (\d+) missing", proc.stdout).group(1) @@ -5711,7 +5725,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun): (cores / "handy_libretro.so").write_bytes(b"") proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(self.home / "s"), "--cores-dir", str(cores), - "--dry-run", "--no-input") + "--dry-run", "--no-input", "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("to the installed cores", proc.stdout) @@ -5912,7 +5926,7 @@ class StandaloneFoldersAreNamedWhenThereAreAny(unittest.TestCase, _LoopbackRun): def _run(self) -> str: proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(self.home / "system"), "--system", "atari-lynx", - "--no-input") + "--no-input", "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) return proc.stdout @@ -6661,7 +6675,7 @@ class RomMFirmwareInstallsUnderItsTemplate(unittest.TestCase, _LoopbackRun): " firmware: \"{platform}/bios\"\n") proc = _install_run(self.env, "--platform", "romm", "--dest", str(base / "library" / "bios"), "--system", "atari-lynx", - "--no-input") + "--no-input", "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertTrue((base / "library" / "lynx" / "bios" / "lynxboot.img").is_file()) self.assertFalse((base / "library" / "bios").exists()) @@ -6810,7 +6824,7 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree): SKIPPED = [{"reason": "not_found", "label": "not found", "files": 1, "required": 1}] def test_a_check_run(self): - proc, events = self._run("--check") + proc, events = self._run("--check", "--verbose") self.assertEqual(proc.returncode, 3) installer = events[0]["installer"] self.assertEqual(events, self._head("check", installer) + [ @@ -7448,7 +7462,7 @@ class KeptFilesEndToEnd(unittest.TestCase, _SyntheticTree): def test_install_then_restore(self): proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(self.bios), - "--no-input", "--jobs", "1") + "--no-input", "--jobs", "1", "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertEqual((self.bios / "b.bin").read_bytes(), self.FILES["bios/A/b.bin"]) runs = list((self.home / install.BACKUP_DIR).iterdir()) @@ -7614,7 +7628,7 @@ class TheListVersionIsSaidBeforeAndAfter(unittest.TestCase): "may show some files as not matching it.") def test_line_mode_says_it_before_the_install_and_with_the_result(self): - proc = self.run_installer() + proc = self.run_installer("--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertEqual(proc.stdout.count(self.SAID), 2, proc.stdout) self.assertLess(proc.stdout.index(self.SAID), proc.stdout.index("Checking existing")) diff --git a/tests/test_install_cores.py b/tests/test_install_cores.py index 7594890fe..f33f24cb0 100644 --- a/tests/test_install_cores.py +++ b/tests/test_install_cores.py @@ -483,7 +483,7 @@ class TheOneLinerReadsInstalledCores(unittest.TestCase): def test_only_what_the_installed_cores_read(self): with _CoreRepository() as repo: repo.install_core("pcsx_rearmed_neon") - proc = repo.run("--installed-cores") + proc = repo.run("--installed-cores", "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("1 core installed", proc.stdout) self.assertEqual( @@ -562,7 +562,7 @@ class TheInterfaceFindsTheCoresLineModeFinds(unittest.TestCase): found = [] for given in ((), ("--dest", str(repo.dest))): command = [sys.executable, str(REPO_ROOT / "install.py"), "--platform", - "retroarch", *given, "--installed-cores", "--check"] + "retroarch", *given, "--installed-cores", "--check", "--verbose"] proc = subprocess.run(command, env=env, capture_output=True, text=True, timeout=60, check=False, stdin=subprocess.DEVNULL) self.assertNotIn("no installed core found", proc.stderr) diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 6d7ca9a83..6ffcbb1b0 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -308,6 +308,15 @@ class ProgressOutput(unittest.TestCase): return stream, term def test_a_log_gets_one_line_per_file_and_failures_say_why(self): + # A file that went well is a line of --verbose, a failure of every run. + install.OUTPUT.verbose = False + stream, term = self._term() + progress = install.Progress(2, 10, term) + progress.done("a.bin", True) + progress.done("b.bin", False, "HTTP 404") + self.assertEqual(stream.getvalue(), " FAILED b.bin (HTTP 404)\n") + self.addCleanup(setattr, install.OUTPUT, "verbose", False) + install.OUTPUT.verbose = True stream, term = self._term() progress = install.Progress(2, 10, term) progress.done("a.bin", True) @@ -329,6 +338,8 @@ class ProgressOutput(unittest.TestCase): ["0:00", "0:42", "3:05", "1:02:09"]) def test_the_summary_names_each_failure(self): + self.addCleanup(setattr, install.OUTPUT, "verbose", False) + install.OUTPUT.verbose = True buf = io.StringIO() with contextlib.redirect_stdout(buf), self.assertRaises(SystemExit): install._report_outcome( @@ -544,7 +555,7 @@ class AFolderThisUserCannotEnter(unittest.TestCase): with FakeRepository() as repo: locked = self._locked(repo) try: - proc = repo.run("--check") + proc = repo.run("--check", "--verbose") finally: os.chmod(locked, 0o755) self.assertNotIn("Traceback", proc.stderr) @@ -671,7 +682,7 @@ class AHomeThisUserCannotOpen(unittest.TestCase): ("detected", [])): runs[name] = subprocess.run( [sys.executable, str(REPO_ROOT / "install.py"), "--check", - "--no-input", *extra], + "--no-input", "--verbose", *extra], env=env, capture_output=True, text=True, timeout=60, stdin=subprocess.DEVNULL) finally: @@ -864,7 +875,7 @@ class APartlyClosedHome(unittest.TestCase): ("events", ["--events"])): runs[name] = subprocess.run( [sys.executable, str(REPO_ROOT / "install.py"), "--check", - "--no-input", *extra], + "--no-input", "--verbose", *extra], env=env, capture_output=True, text=True, timeout=60, stdin=subprocess.DEVNULL) finally: diff --git a/tests/test_install_filesystems.py b/tests/test_install_filesystems.py index 4eb66519b..8392dc2e4 100644 --- a/tests/test_install_filesystems.py +++ b/tests/test_install_filesystems.py @@ -42,7 +42,7 @@ class AndroidMediaScanner(unittest.TestCase): return self.repo, proc def test_an_android_install_writes_one(self): - repo, proc = self._run("android") + repo, proc = self._run("android", "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) marker = repo.dest / ".nomedia" self.assertTrue(marker.is_file()) @@ -106,7 +106,7 @@ class NamesAFatCardCannotHold(unittest.TestCase): "files": files})) out = io.StringIO() argv = ["install.py", "--platform", "retrodeck", "--dest", str(dest), - "--manifest", str(listed), "--from", str(old), "--no-input"] + "--manifest", str(listed), "--from", str(old), "--no-input", "--verbose"] with mock.patch.object(install, "holds_fat_refused_names", return_value=holds), \ mock.patch.object(install, "detect_os", return_value="linux"), \ mock.patch("sys.argv", argv), contextlib.redirect_stdout(out), \ diff --git a/tests/test_install_from.py b/tests/test_install_from.py index 2b36b408e..f36d9125d 100644 --- a/tests/test_install_from.py +++ b/tests/test_install_from.py @@ -219,8 +219,8 @@ class CopyFromAnOldCard(FromFolder): def test_check_names_what_the_folder_would_give_and_writes_nothing(self): self.keep("BIOS/SCPH5501.BIN", US) self.keep("gba_bios.bin", GBA) - proc = self.run_installer("--check", "--manifest", str(self.list), "--from", - str(self.old)) + proc = self.run_installer("--check", "--verbose", "--manifest", str(self.list), + "--from", str(self.old)) self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) self.assertIn(f"{self.old} holds 2 of these files: a run without --check " "copies them.", proc.stdout) @@ -376,7 +376,7 @@ class MisplacedInTheBiosFolder(Scratch): def test_a_nested_bios_folder_is_fixed_and_left_in_place(self): for name, data in PAYLOAD.items(): self.place(f"system/{name}", data) - proc = self.install() + proc = self.install("--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) for name, data in PAYLOAD.items(): self.assertEqual((self.dest / name).read_bytes(), data) @@ -387,7 +387,7 @@ class MisplacedInTheBiosFolder(Scratch): def test_a_pack_extracted_into_a_folder_of_its_own_is_fixed(self): for name, data in PAYLOAD.items(): self.place(f"RetroArch_BIOS_Pack/{name}", data) - proc = self.install() + proc = self.install("--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("one folder too deep: it is at RetroArch_BIOS_Pack/scph5501.bin", proc.stdout) diff --git a/tests/test_install_games.py b/tests/test_install_games.py index ab1507fbe..cf45f3059 100644 --- a/tests/test_install_games.py +++ b/tests/test_install_games.py @@ -833,7 +833,7 @@ class CommandLine(unittest.TestCase): return [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")] def test_check_narrows_to_what_the_games_need(self): - proc = run_cli(self.card, "--for-games", "--check", "--events") + proc = run_cli(self.card, "--for-games", "--check", "--events", "--verbose") events = self.events(proc) games = next(e for e in events if e["event"] == "games") self.assertEqual(games["games"], 2) @@ -973,7 +973,8 @@ class ARunThatAsksNothing(unittest.TestCase): def test_it_installs_what_the_games_need(self): unasked, said = self.install("--yes") self.assertEqual(unasked, self.install("--for-games", "--yes")[0]) - self.assertIn("--all-files installs the whole list", said) + self.assertIn("what the games need", said) + self.assertIn("--all-files installs the whole list", self.install("--yes", "--verbose")[1]) def test_all_files_installs_the_whole_list(self): everything, said = self.install("--all-files", "--yes") diff --git a/tests/test_install_offline.py b/tests/test_install_offline.py index c9226b185..5994f1a4d 100644 --- a/tests/test_install_offline.py +++ b/tests/test_install_offline.py @@ -461,7 +461,7 @@ class ABoardWithoutTheNetwork(Scratch): (self.root / "targets" / "retroarch.json").write_text(json.dumps(BOARDS)) self.complete() proc = self.run_installer("--offline", "--check", "--manifest", str(self.list), - "--target", "psx-board", "--no-input") + "--target", "psx-board", "--no-input", "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("for target psx-board", proc.stdout) @@ -500,7 +500,7 @@ class FromPackInstallsOffline(Scratch): for p in self.dest.rglob("*") if p.is_file()} def test_the_pack_installs_with_no_network(self): - proc = self.run_installer("--from-pack", str(self.pack)) + proc = self.run_installer("--from-pack", str(self.pack), "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertEqual(self.installed(), PAYLOAD) self.assertIn("Copying 4 files", proc.stdout) @@ -571,7 +571,8 @@ class FromPackInstallsOffline(Scratch): for rel, data in tree.items()}, }] self.list.write_text(json.dumps(listed)) - proc = self.run_installer("--from-pack", str(self.pack), "--manifest", str(self.list)) + proc = self.run_installer("--from-pack", str(self.pack), "--manifest", str(self.list), + "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("dolphin-sys: 2 installed", proc.stdout) for rel, data in tree.items(): @@ -708,6 +709,8 @@ class TheSummaryNamesTheBulk(unittest.TestCase): files, {"system_cores": {"3do": ["mame", "opera"]}}, True, True), []) def test_the_end_summary_prints_the_line(self): + self.addCleanup(setattr, install.OUTPUT, "verbose", False) + install.OUTPUT.verbose = True out = io.StringIO() with contextlib.redirect_stdout(out): install._report_outcome([Path("/x")], 3, 0, 0, 0, False, @@ -950,7 +953,8 @@ class OfflineLeavesFilesMissing(Scratch): }] self.list.write_text(json.dumps(listed)) self.complete() - proc = self.run_installer("--offline", "--manifest", str(self.list), "--no-input") + proc = self.run_installer("--offline", "--manifest", str(self.list), "--no-input", + "--verbose") # Its files are missing, as the BIOS files --offline leaves out are, # with or without questions. self.assertIn("bluemsx: 0 already in place, 1 needs the internet (--offline)", diff --git a/tests/test_install_selection.py b/tests/test_install_selection.py index 852699169..a6e37379f 100644 --- a/tests/test_install_selection.py +++ b/tests/test_install_selection.py @@ -594,7 +594,7 @@ class FakeRepository: class TheOneLinerNarrows(unittest.TestCase): def test_options_install_only_the_choice(self): with FakeRepository() as repo: - proc = repo.run("--system", "sony-playstation", "--region", "us") + proc = repo.run("--system", "sony-playstation", "--region", "us", "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("Narrowed 5 to 2 files", proc.stdout) self.assertEqual(repo.installed(), ["pcsx/extra.bin", "scph5501.bin"]) @@ -1606,14 +1606,14 @@ class ThePlatformsOwnListAlone(unittest.TestCase): def test_install_check_and_dry_run_apply_it(self): with FakeRepository() as repo: - proc = repo.run("--declared-only") + proc = repo.run("--declared-only", "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("5 to 3 files", proc.stdout) self.assertEqual(repo.installed(), ["gba_bios.bin", "scph5500.bin", "scph5501.bin"]) for mode in ("--check", "--dry-run"): with self.subTest(mode=mode), FakeRepository() as repo: - proc = repo.run("--declared-only", mode) + proc = repo.run("--declared-only", mode, "--verbose") self.assertIn("5 to 3 files", proc.stdout, proc.stderr) self.assertIn("0 in place, 3 missing", proc.stdout) self.assertEqual(repo.installed(), []) @@ -1633,7 +1633,7 @@ class ThePlatformsOwnListAlone(unittest.TestCase): def test_a_local_file_list_applies_it(self): with FakeRepository() as repo: - proc = repo.run("--declared-only", "--dry-run", "--manifest", + proc = repo.run("--declared-only", "--dry-run", "--verbose", "--manifest", str(repo.root / "install" / "retroarch.json")) self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("5 to 3 files", proc.stdout) @@ -1778,7 +1778,7 @@ class SomethingElseAtADestination(unittest.TestCase): def test_each_says_why_in_the_check_and_the_install(self): with FakeRepository() as repo: self.prepare(repo) - proc = repo.run("--check") + proc = repo.run("--check", "--verbose") self.assertEqual(proc.returncode, 3, proc.stderr) self.assertNotIn("Traceback", proc.stderr) self.assertIn(" wrong copy: scph5500.bin\n a folder sits where this " diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index 9c42e9650..785e379f8 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -7017,6 +7017,8 @@ def line_decision(args: argparse.Namespace, answer) -> tuple[list, str]: def stop(_args, platforms): raise _Decided(platforms) + # --verbose's text, which names what detection found. + args.verbose = True install.LOCAL.reset() with mock.patch.object(install, "app_refusal", side_effect=stop), \ mock.patch.object(install, "detect_os", return_value="linux"), \ @@ -7798,7 +7800,7 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings, (backup / "gba_bios.bin").write_bytes(b"gba") (backup / "doom" / "doom1.wad").write_bytes(b"wad" * 10) args = _args(manifest=listing, dest=self.here, offline=True, declared_only=False, - no_input=True) + no_input=True, verbose=True) setattr(args, "from", [backup]) real = io.StringIO() install.LOCAL.reset() @@ -7965,7 +7967,7 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings, old = self.root / "OLD" old.mkdir() (old / "gba_bios.bin").write_bytes(payload) - args = _args(manifest=listing, declared_only=False, offline=True) + args = _args(manifest=listing, declared_only=False, offline=True, verbose=True) setattr(args, "from", [old]) real = io.StringIO() install.LOCAL.reset() @@ -8047,7 +8049,10 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings, def line_run(self, args: argparse.Namespace, answers: list, mounts: list = (), found: list = ()) -> tuple[str, list]: """(text, prompts) of line mode at a terminal for args, the answers - given in turn: the run stops at the first question left unanswered.""" + given in turn: the run stops at the first question left unanswered. + Its text is --verbose's: these findings read the lines the default + leaves out.""" + args.verbose = True given, asked = iter(answers), [] def answer(prompt: str) -> str: @@ -12619,7 +12624,7 @@ class ChoiceStateInBothModes(unittest.TestCase): self.assertNotIn("to fetch them", text) self.assertIn("to copy 1 of them from RetroArch_BIOS_Pack.zip", text) options = ("--platform", "retroarch", "--manifest", str(listing), "--dest", str(dest), - "--yes") + "--yes", "--verbose") proc = install_run(*options, "--from-pack", str(pack)) text = proc.stdout + proc.stderr self.assertIn("Copying 1 file (1 B) from RetroArch_BIOS_Pack.zip...", text) @@ -12774,7 +12779,7 @@ class ChoiceStateInBothModes(unittest.TestCase): files = [self.entry("gba_bios.bin", b"gba", ["nintendo-gba"])] listing = self.listing("retroarch", files) proc = install_run("--platform", "retroarch", "--manifest", str(listing), - "--dest", str(self.here), "--check", "--offline") + "--dest", str(self.here), "--check", "--offline", "--verbose") self.assertIn("0 in place, 1 missing, 0 wrong copies", proc.stdout) self.assertNotIn("present (", proc.stdout) source = (REPO_ROOT / "install.py").read_text(encoding="utf-8") @@ -13232,6 +13237,8 @@ class OneVocabularyOneCount(unittest.TestCase): return listing, dest, old def run_line(self, listing: Path, dest: Path, answers: list, **given) -> str: + # --verbose's text: the counts compared are lines the default leaves out. + given.setdefault("verbose", True) from_ = given.pop("from_", None) args = _args(manifest=listing, dest=dest, declared_only=False, **given) if from_: @@ -13626,7 +13633,9 @@ class ProvenCardsAndCounts(unittest.TestCase): def line_from(self, args: argparse.Namespace, answers: list, folders: list[Path] = ()) -> tuple[str, list]: """(text, prompts) of line mode at a terminal, the result included, - the folders --from names read first, as main() reads them.""" + the folders --from names read first, as main() reads them: --verbose's + text.""" + args.verbose = True given, asked = iter(answers), [] def answer(prompt: str) -> str: diff --git a/wiki/installer.md b/wiki/installer.md index 42c69341a..5ce100f68 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -449,6 +449,23 @@ Batocera install into an empty folder, from the list of 2026-11-06. 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. + + + +``` +Installing 18175 files (9.2 GB) for Batocera. +BIOS folder: /userdata/bios + +Done. 18175 files installed. +Next: start Batocera and play. +``` + + +With `--verbose` (`-v`) it says each step, the lists and the reasons too. +The lines below are those of the same run with `--verbose`. + ### Before the run The size depends on the platform and changes with each release. The run prints @@ -473,8 +490,8 @@ Looking for setups here... A line above them, `RetroBIOS` and a code, identifies the installer: the -code is the start of the SHA-256 the command checked before running it. The -path is the folder it will write to. +code is the start of the SHA-256 the command checked before running it. +`--version` prints it too. The path is the folder it will write to. ### The file index and the download @@ -501,8 +518,8 @@ is already there, and only the rest is downloaded. A download of 1 GB or more is confirmed first, the size and the folder in the question. The second phase downloads what is missing. At a terminal one progress line -moves with the count and the size. A pipe or a log gets one line per file, -as the log of the run below: +moves with the count and the size. With `--verbose` a pipe or a log gets one +line per file, as the log of the run below: @@ -537,8 +554,8 @@ To check this later, run the same command with --check. When most of a download of 100 MB or more comes from one system, or from -one core of a single system, the end of the run says so and what narrowing -would save, once that saving is over half. +one core of a single system, the end of a `--verbose` run says so and what +narrowing would save, once that saving is over half. From fcb64e3857d9a08fc64f95c987adff57c2114f86 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 01:09:37 +0100 Subject: [PATCH 2/4] feat: full screen keeps its notes on Details The plan, Confirm and the result say what happens, its size, where and the next step. Their counts, free space, backup and save notes, kinds, reasons a file is left out, per-file failures and commands move to a Details screen: d on the plan, Confirm and the result, the Details row on the plan, Y, o or Tab on the result. Opened on the result, they are also said in the terminal after Exit. Installing lists the files that went well only with --verbose, and --verbose draws every line in place. The log line reads Log: so that Details names one thing. --check prints its count, the folder and the remedy in both modes, so that --verbose prints every default line. Each golden screen gets a word budget per size, and a test proves the screen and its Details together equal the --verbose screen. --- install.ps1 | 2 +- install.py | 305 +++++++++++++----- install.sh | 2 +- install/launchers/index.json | 16 +- scripts/generate_installer_doc.py | 2 +- tests/fixtures/tui/colour-120x40.txt | 128 ++++++-- tests/fixtures/tui/colour-40x12.txt | 56 +++- tests/fixtures/tui/colour-40x15.txt | 70 ++-- tests/fixtures/tui/colour-80x24.txt | 98 ++++-- tests/fixtures/tui/confirm-120x40.txt | 6 +- tests/fixtures/tui/confirm-40x12.txt | 6 +- tests/fixtures/tui/confirm-40x15.txt | 6 +- tests/fixtures/tui/confirm-80x24.txt | 6 +- tests/fixtures/tui/help-120x40.txt | 2 +- tests/fixtures/tui/help-40x12.txt | 4 +- tests/fixtures/tui/help-40x15.txt | 2 +- tests/fixtures/tui/help-80x24.txt | 2 +- tests/fixtures/tui/installing-120x40.txt | 2 +- tests/fixtures/tui/installing-40x12.txt | 2 +- tests/fixtures/tui/installing-40x15.txt | 2 +- tests/fixtures/tui/installing-80x24.txt | 2 +- tests/fixtures/tui/plan-120x40.txt | 6 +- tests/fixtures/tui/plan-40x12.txt | 2 +- tests/fixtures/tui/plan-40x15.txt | 6 +- tests/fixtures/tui/plan-80x24.txt | 6 +- tests/fixtures/tui/plan-details-120x40.txt | 40 +++ tests/fixtures/tui/plan-details-40x12.txt | 12 + tests/fixtures/tui/plan-details-40x15.txt | 15 + tests/fixtures/tui/plan-details-80x24.txt | 24 ++ tests/fixtures/tui/plan-games-120x40.txt | 10 +- tests/fixtures/tui/plan-games-40x12.txt | 2 +- tests/fixtures/tui/plan-games-40x15.txt | 2 +- tests/fixtures/tui/plan-games-80x24.txt | 10 +- tests/fixtures/tui/plan-in-place-120x40.txt | 6 +- tests/fixtures/tui/plan-in-place-40x12.txt | 8 +- tests/fixtures/tui/plan-in-place-40x15.txt | 8 +- tests/fixtures/tui/plan-in-place-80x24.txt | 6 +- tests/fixtures/tui/result-120x40.txt | 14 +- tests/fixtures/tui/result-40x12.txt | 6 +- tests/fixtures/tui/result-40x15.txt | 12 +- tests/fixtures/tui/result-80x24.txt | 16 +- tests/fixtures/tui/result-details-120x40.txt | 40 +++ tests/fixtures/tui/result-details-40x12.txt | 12 + tests/fixtures/tui/result-details-40x15.txt | 15 + tests/fixtures/tui/result-details-80x24.txt | 24 ++ tests/fixtures/tui/route-evmapy.txt | 194 ++++++----- tests/fixtures/tui/route-gptokeyb.txt | 196 ++++++----- tests/fixtures/tui/route-gptokeyb_muxterm.txt | 196 ++++++----- tests/fixtures/tui/route-keys.txt | 224 +++++++++---- tests/fixtures/tui/route-miyoo.txt | 179 +++++++--- tests/fixtures/tui/route-muxterm.txt | 200 +++++++++--- tests/test_doc_pages.py | 2 +- tests/test_install.py | 4 +- tests/test_install_tui.py | 269 +++++++++++++-- tests/test_no_game_data.py | 6 +- wiki/installer.md | 24 +- 56 files changed, 1851 insertions(+), 666 deletions(-) create mode 100644 tests/fixtures/tui/plan-details-120x40.txt create mode 100644 tests/fixtures/tui/plan-details-40x12.txt create mode 100644 tests/fixtures/tui/plan-details-40x15.txt create mode 100644 tests/fixtures/tui/plan-details-80x24.txt create mode 100644 tests/fixtures/tui/result-details-120x40.txt create mode 100644 tests/fixtures/tui/result-details-40x12.txt create mode 100644 tests/fixtures/tui/result-details-40x15.txt create mode 100644 tests/fixtures/tui/result-details-80x24.txt diff --git a/install.ps1 b/install.ps1 index 087b4755c..5d08082f3 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 = "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e" + $defaultInstallSha256 = "d07c90a00f5bab9c583c34a9a1413bed5625542e7c88fed7bd9bab0020e9e637" # 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 5d50497f8..6f1431495 100644 --- a/install.py +++ b/install.py @@ -6875,12 +6875,15 @@ def _report_outcome( say(next_step_words(platform_of or {}, paths)) +NEXT_ON_CARD = "Next: put the card back in the device and start a game." + + def next_step_words(platform_of: dict[Path, str], paths: list[Path]) -> str: """The step after an install that ended well: a card goes back in its device, a setup on this machine is started.""" setups = [(platform_of.get(path, ""), path) for path in paths] if setups and all(plat and on_its_card(plat, path) for plat, path in setups): - return "Next: put the card back in the device and start a game." + return NEXT_ON_CARD names = list(dict.fromkeys(detected_os(plat, path) or display_name(plat) for plat, path in setups if plat)) if len(names) == 1: @@ -6944,7 +6947,7 @@ def _check_fetch_line(fetch: int, beside: str, lacking: int = 0) -> str: f"internet, which {offline_named()} does not use. {_offline_remedy()}") # Why nothing is lost goes with the details: the remedy is the line. return (" Run the same command without --check to fetch them." - + (f" A wrong copy is replaced, and the one in place kept in {BACKUP_DIR} " + + (f"\n A wrong copy is replaced, and the one in place kept in {BACKUP_DIR} " f"beside {beside}." if OUTPUT.verbose else "")) @@ -16209,15 +16212,14 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: # The count and the folder by default, each file with # --verbose: 20 paths hid the line saying what to do. print(f"\n {_plural(len(to_download) + trees_missing, 'file')} " - + ("not in place" if offline or LOCAL.pack is not None - else "to download") + (":" if OUTPUT.verbose else ".")) + + ("not in place." if offline or LOCAL.pack is not 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) for line in unreadable_lines(unread, args.verbose): print(line) - if not OUTPUT.verbose: - print(f" BIOS folder: {shown_path(bios_path)}") + print(f" BIOS folder: {shown_path(bios_path)}") # The data files need the internet too: offline, "8164 files # need the internet" stood under "10805 files not in place". trees_fetched = trees_missing if LOCAL.pack is None else 0 @@ -17196,6 +17198,8 @@ class TuiLine(str): # Whether the command it holds is typed into PowerShell, from the # Invocation that made it: its line ends follow its words. powershell: bool | None = None + # Shown on the Details screen, not on the screen it explains. + detail = False def _styled(text: str, style: str = "") -> TuiLine: @@ -17204,6 +17208,30 @@ def _styled(text: str, style: str = "") -> TuiLine: return line +def _detail(text: str, style: str = "") -> TuiLine: + """A line of the Details screen: an explanation, a list, a reason.""" + line = _styled(text, style) + line.detail = True + return line + + +def _details(lines: Iterable[str]) -> list[TuiLine]: + """lines as Details lines, each keeping how it is drawn.""" + marked = [] + for line in lines: + copy = TuiLine(str(line)) + for name in ("style", "mark", "command", "powershell"): + if hasattr(line, name): + setattr(copy, name, getattr(line, name)) + copy.detail = True + marked.append(copy) + return marked + + +def _is_detail(line: object) -> bool: + return bool(getattr(line, "detail", False)) + + def _command_line(text: str, hint: Invocation) -> TuiLine: """A line holding a command hint made, wrapped with hint's shell.""" line = _styled(text, "command") @@ -17504,6 +17532,9 @@ class TuiState: self.installed_only: bool = False self.jobs: int = 8 self.passthrough: list = [] + # Whether Details were opened on the result: the terminal keeps + # them after Exit too. + self.details_seen = False # The install. self.download: dict | None = None self.progress: dict | None = None @@ -18104,7 +18135,7 @@ class TuiRow(NamedTuple): # Screens that scroll text instead of listing rows. -_PAGERS = ("preview", "command", "result", "misplaced", "unreadable", "games") +_PAGERS = ("preview", "command", "result", "misplaced", "unreadable", "games", "details") _CHECKLISTS = ("systems", "cores") @@ -18483,9 +18514,10 @@ def _plan_rows(state: TuiState) -> list[TuiRow]: rows = [TuiRow("systems", TUI_SYSTEMS_ROW)] game_data = _game_data_row(state) return rows + ([game_data] if game_data is not None else []) + [ - TuiRow("options", "Options"), TuiRow("quit", "Quit")] + *_details_row(state), TuiRow("options", "Options"), TuiRow("quit", "Quit")] if not (_needed(check) if check else 0) and not check.get("data_missing"): - return [TuiRow("close", "Close"), *_games_rows(state), TuiRow("options", "Options")] + return [TuiRow("close", "Close"), *_games_rows(state), *_details_row(state), + TuiRow("options", "Options")] # The size the pack can give: "Install 6.6 GB" from a pack holding # none of the files. lacking = set(_tui_pack_lacks(state, [event["dest"] for event in state.files])) @@ -18542,7 +18574,18 @@ def _plan_rows(state: TuiState) -> list[TuiRow]: rows.append(TuiRow("misplaced", "Show the files in the wrong place")) if state.unreadable: rows.append(TuiRow("unreadable", "Show the files that could not be read")) - return rows + [TuiRow("options", "Options"), TuiRow("quit", "Quit")] + return rows + [*_details_row(state), TuiRow("options", "Options"), TuiRow("quit", "Quit")] + + +# The plan's explanations, counts and reasons, one press away: a pad has +# no d key. +TUI_DETAILS_ROW = TuiRow("details", "Details") + + +def _details_row(state: TuiState) -> list[TuiRow]: + """The Details row, where the plan leaves lines out: --verbose shows + them all on the plan itself.""" + return [] if tui_verbose(state) else [TUI_DETAILS_ROW] def games_where(state: TuiState) -> str: @@ -18604,11 +18647,11 @@ def games_plan_words(state: TuiState) -> list[str]: f"{', '.join(region_title(str(r)) for r in row['regions'])}" for row in games.get("found") or [] if row.get("regions")] if narrowed: - words.append(f"One region where the games prove it: {'; '.join(narrowed)}.") + words.append(_detail(f"One region where the games prove it: {'; '.join(narrowed)}.")) fallback = len(games.get("fallback") or []) if fallback: - words.append(f"{agree(fallback, 'game gets', 'games get')} every BIOS of " - f"{'its' if fallback == 1 else 'their'} system: Show the games.") + words.append(_detail(f"{agree(fallback, 'game gets', 'games get')} every BIOS of " + f"{'its' if fallback == 1 else 'their'} system: Show the games.")) return words @@ -19495,6 +19538,8 @@ def _tui_folder_facts(state: TuiState) -> dict: def _activate(state: TuiState, row: TuiRow) -> list[tuple]: screen, action = state.screen, row.action + if action == "details": + return _open_details(state) if action == "filter": only = _only_match(state) if only is not None: @@ -20438,6 +20483,10 @@ def _pager_key(state: TuiState, key: TuiKey) -> list[tuple]: elif name in ("enter", "esc", "backspace") or char == " ": if screen == "result": return tui_quit(state) + if screen == "details": + # Never _back: a run counting under the plan goes on. + state.stack.pop() + return [] return _back(state) state.focus[screen] = max(0, min(top, _pager_last(state, _pager_items(state)))) return [] @@ -20517,6 +20566,11 @@ def tui_update(state: TuiState, key: TuiKey) -> list[tuple]: state.focus["error_text"] = max(0, state.focus.get("error_text", 0) + (step if key.name == "pgdn" else -step)) return [] + if char == "d" and screen in ("plan", "confirm", "result") and not ( + screen == "confirm" and (state.running or state.dry is None)): + return _open_details(state) + if screen == "result" and not rows and (char == "o" or key.name == "tab"): + return _open_details(state) if screen in _PAGERS and not rows: return _pager_key(state, key) if not rows: @@ -21173,7 +21227,7 @@ _ROW_VERBS = { "unreadable": "Show", "exit": "Exit", "addapp": "Put it on the card", "cardsystem": "Choose", "rescan": "Look again", "elsewhere": "Choose", "drive": "Choose", "recheck": "Check again", "gamedata": "Change", "share": "Choose", "morecores": "Show", - "games": "Change", "startonly": "Change", "gameslist": "Show", + "games": "Change", "startonly": "Change", "gameslist": "Show", "details": "Show", } @@ -21564,7 +21618,7 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list: if found_in: # Right under the folder it names: after the save lines it read as # naming them. - lines.append(_styled(f"Named by {_short_path(Path(found_in[0]), state.home)}", "path")) + lines.append(_detail(f"Named by {_short_path(Path(found_in[0]), state.home)}", "path")) if state.facts.get("writable") is False: # The remedy itself comes below, after the counts. lines += cannot_write_words(dest, state.platform, tui_hint(state), @@ -21578,27 +21632,27 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list: counts = (f"{check['present']} in place, {check['missing'] + data} missing, " f"{_plural(wrong, 'wrong copy')}") # A first install's missing files are its normal state, not a warning. - lines.append(_styled(counts, "quiet") if not check["present"] and not wrong else counts) + lines.append(_detail(counts, "quiet" if not check["present"] and not wrong else "")) # The backup line goes right under the count it explains, before the # pack and copy split: at 40x12 and 40x15 it came after it, and was cut. count_at = len(lines) if copied and fetch and LOCAL.pack is not None: - lines.append(f"{fetch} from the pack, {len(copied)} copied from this machine") + lines.append(_detail(f"{fetch} from the pack, {len(copied)} copied from this machine")) elif copied and fetch and not offline: # 47 to download above 49 missing and 1 wrong copy: the 3 found on # this machine are the difference, on a line of its own ("0 wrong # copies: 2 of them" read as two of the zero). Offline the headline # says it: "3 downloaded" sat under "3 need the internet". - lines.append(f"{need} to install: {len(copied)} copied from this machine, " - f"{fetch} downloaded") + lines.append(_detail(f"{need} to install: {len(copied)} copied from this machine, " + f"{fetch} downloaded")) after_counts = len(lines) saves = (_choice_saves(state) if check is state.check else state.dry_saves if check is state.dry else None) saves_lines = _saves_lines(state, saves) - lines += [_styled(line, "path" if index else "") for index, line in enumerate(saves_lines)] + lines += [_detail(line, "path" if index else "") for index, line in enumerate(saves_lines)] free = state.facts.get("free") if free is not None: - lines.append(f"Free space: {format_size(free)}") + lines.append(_detail(f"Free space: {format_size(free)}")) if check["bytes_needed"] > free: lines.append(space_words(check["bytes_needed"], free, _on_its_card(state))) # The wrong copies something replaces: offline, those a folder copied @@ -21612,30 +21666,32 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list: if wrong_dests else wrong) misplaced = _tui_misplaced(state) if check is state.check else [] if misplaced: - lines.insert(after_counts, f"{_plural(len(misplaced), 'file')} " - f"{'is' if len(misplaced) == 1 else 'are'} in the wrong place, " - f"{'it' if len(misplaced) == 1 else 'they'} will be copied where the " - "emulators read " - f"{'it' if len(misplaced) == 1 else 'them'}, the " - f"{'copy' if len(misplaced) == 1 else 'copies'} left as " - f"{'it is' if len(misplaced) == 1 else 'they are'}.") + lines.insert(after_counts, _detail( + f"{_plural(len(misplaced), 'file')} " + f"{'is' if len(misplaced) == 1 else 'are'} in the wrong place, " + f"{'it' if len(misplaced) == 1 else 'they'} will be copied where the " + "emulators read " + f"{'it' if len(misplaced) == 1 else 'them'}, the " + f"{'copy' if len(misplaced) == 1 else 'copies'} left as " + f"{'it is' if len(misplaced) == 1 else 'they are'}.")) if backup: # With its subject: "They are replaced" sat lines under the count. # Not where nothing can be installed: nothing is replaced then. - lines.insert(count_at, (f"The {_plural(wrong, 'wrong copy')} " if backup == wrong - else f"{backup} of the {_plural(wrong, 'wrong copy')} ") - + f"{'is' if backup == 1 else 'are'} replaced, the old " - f"{'one' if backup == 1 else 'ones'} kept in a backup folder.") + lines.insert(count_at, _detail( + (f"The {_plural(wrong, 'wrong copy')} " if backup == wrong + else f"{backup} of the {_plural(wrong, 'wrong copy')} ") + + f"{'is' if backup == 1 else 'are'} replaced, the old " + f"{'one' if backup == 1 else 'ones'} kept in a backup folder.")) if state.facts.get("writable") is False and dest is not None: lines += _commands(state, write_remedy(dest, state.platform, tui_hint(state), state.pad not in TUI_PAD_ROUTES)) if check is state.check: - lines += _scan_lines(state) + lines += _details(_scan_lines(state)) if check.get("not_installable"): - lines += _unavailable_lines(state) + lines += _details(_unavailable_lines(state)) if check is state.check: - lines += _unplaced_lines(_choice_summary(state), state, - "Choose it in Options, Save folder (--save-dir).") + lines += _details(_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. @@ -22014,7 +22070,8 @@ def _choice_line(state: TuiState) -> list[str]: "count comes before the install."] -def _view_plan(state: TuiState, width: int, height: int) -> tuple: +def _plan_text(state: TuiState) -> list: + """Every line of the plan, the Details lines among them.""" check = state.check or {} lines = _plan_lines(check, state.dest, state) choice = _choice_line(state) @@ -22126,11 +22183,11 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple: if found and parts: parts = (f"Not in {', '.join(_last_name(str(folder)) for folder in found)}: " f"{parts}") - lines[at:at] = [*([state.card_note] if state.card_note + lines[at:at] = [*([_detail(state.card_note)] if state.card_note and state.card_note != state.status else []), *([f"{said[0].upper()}{said[1:]}."] if said else []), *([emptied_named] if emptied_named else []), *choice, - *([parts] if parts else []), *games_plan_words(state)] + *([_detail(parts)] if parts else []), *games_plan_words(state)] if state.version: # Before the free space, which gives way first: appended last, the # note was cut from the 40x12 plan with no sign. @@ -22140,7 +22197,7 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple: if state.board: free = next((index for index, line in enumerate(lines) if str(line).startswith("Free space:")), len(lines)) - lines.insert(free, state.board) + lines.insert(free, _detail(state.board)) if state.status: lines.append(state.status) if defined: @@ -22150,7 +22207,25 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple: # where the screen has no room. saves = next((index for index, line in enumerate(lines) if "'s save directory" in str(line)), len(lines)) - lines.insert(saves, defined) + lines.insert(saves, _detail(defined)) + return lines + + +def tui_verbose(state: TuiState) -> bool: + """Whether the screen was opened with --verbose: every line is shown + where it belongs, as before the Details screen held them.""" + return any(flag == "--verbose" for flag, _value in state.passthrough) + + +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)] + + +def _view_plan(state: TuiState, width: int, height: int) -> tuple: + check = state.check or {} + lines = _shown(state, _plan_text(state)) # A folder that does not fit is wrapped, as the first screen wraps it: # cut in its middle, the plan hid the part being checked before Install. # At 40 columns too, broken after a separator: cut there, it was spelled @@ -22442,7 +22517,9 @@ def _pager_items(state: TuiState) -> list: if state.screen == "games": return games_pager_lines(state) if state.screen == "result": - return tui_outcome(state)[1] + return _shown(state, tui_outcome(state)[1]) + if state.screen == "details": + return details_lines(state) if state.screen == "command": return ["The same choice without questions:", "", _command_line(tui_command(state), tui_hint(state))] @@ -22511,18 +22588,7 @@ def _view_confirm(state: TuiState, width: int, height: int) -> tuple: # Why this screen: the numbers above are counted again for the # systems and options chosen, the last look before anything is # written. - lines = _plan_lines(state.dry, state.dest, state) - # The kinds apart, as the plan counts them, under the folder. - data = state.dry.get("data_missing", 0) - parts, _defined = _kinds_lines(state, state.dry_files, data, - state.dry.get("data_bytes") if data else None) - if parts: - at = next((index + 1 for index, line in enumerate(lines) - if str(line).startswith(_FOLDER_LABEL)), len(lines)) - lines.insert(at, parts) - if any(row.action == "install" for row in tui_rows(state)): - lines.insert(0, "Counted again for these choices. Nothing is written before " - "Install.") + lines = _shown(state, _confirm_text(state)) if not any(row.action == "install" for row in tui_rows(state)) and state.dry is not None: # Back alone: one Back on the bar, and no word of an Install. Titled # for what it is: "Confirm" over nothing to confirm. @@ -22533,6 +22599,23 @@ def _view_confirm(state: TuiState, width: int, height: int) -> tuple: ("A/Enter Install", "B/Esc Back", "? Help")) +def _confirm_text(state: TuiState) -> list: + """Every line of Confirm, the Details lines among them.""" + lines = _plan_lines(state.dry or {}, state.dest, state) + # The kinds apart, as the plan counts them, under the folder. + data = (state.dry or {}).get("data_missing", 0) + parts, _defined = _kinds_lines(state, state.dry_files, data, + (state.dry or {}).get("data_bytes") if data else None) + if parts: + at = next((index + 1 for index, line in enumerate(lines) + if str(line).startswith(_FOLDER_LABEL)), len(lines)) + lines.insert(at, _detail(parts)) + if any(row.action == "install" for row in tui_rows(state)): + lines.insert(0, "Counted again for these choices.") + lines.insert(1, _detail("Nothing is written before Install.")) + return lines + + def _bar(share: float, width: int) -> str: room = max(4, width - 7) filled = int(share * room) @@ -22568,7 +22651,7 @@ def _view_installing(state: TuiState, width: int, height: int) -> tuple: else f"{stops[1]} stops. What is done stays done.") room = height - len(lines) - 1 failed = [item for item in reversed(state.finished) if not item[1]] - good = [item for item in reversed(state.finished) if item[1]] + good = [item for item in reversed(state.finished) if item[1]] if tui_verbose(state) else [] shown = (failed + good)[:max(0, room)] if shown: lines.append("") @@ -22778,13 +22861,13 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]: apart = _tui_tally(state, [{"dest": dest, "size": 0} for dest in missing if dest not in wrong_dests]) if _tui_categories(state) and apart.bios and apart.games: - lines.append(f"Still missing: {installed_words(apart.bios, apart.games)}.") + lines.append(_detail(f"Still missing: {installed_words(apart.bios, apart.games)}.")) # Each folder spelled in full, as the result names its others. lines.append(_offline_remedy([shown_path(folder) for folder in state.from_folders])) ok = {dest for dest, good, _why in state.finished if good} if any(reason and dest in ok for dest, (_folder, reason) in state.local_dests.items()): # A fix copies: the misplaced files are the user's, never moved. - lines.append("The copies in the wrong place were left as they are.") + lines.append(_detail("The copies in the wrong place were left as they are.")) if _tui_categories(state) and title != "Stopped" and installed: # As the plan counted them, and as line mode ends, on every result: # the old card's said "6 files copied" and no kind. The data tree @@ -22792,14 +22875,14 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]: done = _tui_tally(state, [{"dest": dest, "size": 0} for dest, good, _why in state.finished if good]) if done.files and not (title == "Done" and _one_kind(state, installed)): - lines.append("Installed: " + installed_words( - done.bios, done.games, max(0, installed - done.files)) + ".") + lines.append(_detail("Installed: " + installed_words( + done.bios, done.games, max(0, installed - done.files)) + ".")) # The folder the plan named: MinUI's Bios, not the card's root. lines.append(f"BIOS folder: {shown_path(_plan_folder(state, state.dest))}") if installed + up_to_date: # Spelled as the BIOS folder above it is, whole: "~/" beside a full # path was two spellings on one screen. - lines += _saves_lines(state, state.done_saves, whole=True) + lines += _details(_saves_lines(state, state.done_saves, whole=True)) if state.version: # Said again with the result, as line mode ends: the system's own # BIOS check may mark the files just put in place. @@ -22813,7 +22896,8 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]: names = ", ".join(str(item.get("name")) for item in fallback[:3]) more = f" and {len(fallback) - 3} more" if len(fallback) > 3 else "" their = "its" if len(fallback) == 1 else "their" - lines.append(f"Not recognised, every BIOS of {their} system kept: {names}{more}.") + lines.append(_detail(f"Not recognised, every BIOS of {their} system kept: " + f"{names}{more}.")) if space: lines.append("Free some space on the card, then run again." if _on_its_card(state) else "Free some space, or run again and choose another folder.") @@ -22829,10 +22913,10 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]: continue for dest in dests: if shown < 20: - lines.append(f" failed: {dest}" + (f" ({reason})" if reason else "")) + lines.append(_detail(f" failed: {dest}" + (f" ({reason})" if reason else ""))) shown += 1 if shown > 20: - lines.append(f" ... and {shown - 20} more") + lines.append(_detail(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. @@ -22848,8 +22932,9 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]: if expected > landed and not left and title != "Stopped" and summary: # The data files the plan counted, never named on a result that # counted the BIOS files alone. - lines.append(f"{_plural(expected - landed, 'data file')} not installed" - + (f": not in {LOCAL.pack.name}." if LOCAL.pack is not None else ".")) + lines.append(_detail(f"{_plural(expected - landed, 'data file')} not installed" + + (f": not in {LOCAL.pack.name}." if LOCAL.pack is not None + else "."))) if state.dest is not None and any(failure["reason"] == NOT_WRITABLE for failure in failed): # The plan's own advice: the result offered nothing after 22 files @@ -22869,27 +22954,38 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]: lines.append(f"The files it replaced are kept in {kept['folder']}") if typed: # Broken between its words only, never inside one (_wrap). - lines.append(_command_line(f"To put them back: {run.launcher()} --restore " - f"{run.word(str(kept['folder']))}", run)) + 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 += _unavailable_lines(state) + lines += _details(_unavailable_lines(state)) for group in summary.get("left_out") or []: - lines.append(f"{left_out_words(group)}.") - lines += _unplaced_lines(summary, state) + lines.append(_detail(f"{left_out_words(group)}.")) + lines += _details(_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. + lines.append(tui_next_words(state)) if typed: - lines += ["Same choice without questions:", - _command_line(f" {tui_command(state)}", tui_hint(state))] + lines += _details(["Same choice without questions:", + _command_line(f" {tui_command(state)}", tui_hint(state))]) if tui_keeps_log(state): - lines.append(f"Details: {state.log}") + lines.append(f"Log: {state.log}") if state.mouse and typed: # A drag over the command selected nothing: the terminal sends the # press as a report. Said where it is read. - lines.append(_styled(TUI_RESULT_AGAIN if state.os_type == "android" + lines.append(_detail(TUI_RESULT_AGAIN if state.os_type == "android" else TUI_RESULT_COPY, "dim")) return title, lines +def tui_next_words(state: TuiState) -> str: + """The step after an install the result says went well.""" + if _on_its_card(state): + return NEXT_ON_CARD + name = detected_os(state.platform, state.dest) if state.dest is not None else "" + return f"Next: start {name or display_name(state.platform, state.manifest)} and play." + + def _dest_error_outcome(state: TuiState, installed: int) -> tuple[str, list[str]]: """The result of a run whose BIOS folder could not be created: that reason for every file, and its remedy. It said "22 files not fetched @@ -22908,10 +23004,10 @@ def _dest_error_outcome(state: TuiState, installed: int) -> tuple[str, list[str] lines.append("Run it again and choose another folder.") lines += _commands(state, remedy) if state.pad not in TUI_PAD_ROUTES: - lines += ["Same choice without questions:", - _command_line(f" {tui_command(state)}", tui_hint(state))] + lines += _details(["Same choice without questions:", + _command_line(f" {tui_command(state)}", tui_hint(state))]) if tui_keeps_log(state): - lines.append(f"Details: {state.log}") + lines.append(f"Log: {state.log}") return ("Nothing installed" if not installed else "Finished with errors"), lines @@ -22957,6 +23053,7 @@ def tui_keeps_log(state: TuiState) -> bool: def _view_result(state: TuiState, width: int, height: int) -> tuple: title, lines = tui_outcome(state) + lines = _shown(state, lines) # The outcome in bold, its counts in their roles: it led a list of # lines drawn alike. lines[0] = _styled(lines[0], "title") @@ -22971,7 +23068,7 @@ def _view_result(state: TuiState, width: int, height: int) -> tuple: if state.app_running: return (title, _pager(state, width, height, lines), ("Writing to the card...",)) return (title, _pager(state, width, height, lines), - ("A/Enter Exit", *_scroll(state, lines, width, height))) + ("A/Enter Exit", "Y/o Details", *_scroll(state, lines, width, height))) def _view_error(state: TuiState, width: int, height: int) -> tuple: @@ -23006,6 +23103,36 @@ def _view_quit(state: TuiState, width: int, height: int) -> tuple: rows), hint) +def details_lines(state: TuiState) -> list: + """What the screen under Details leaves out: its explanations, lists + and reasons.""" + if len(state.stack) < 2: + return ["Nothing more to say here."] + # Read as the screen under it reads itself: its rows, its words. + top = state.stack.pop() + try: + under = state.screen + lines = (_plan_text(state) if under == "plan" + else _confirm_text(state) if under == "confirm" + else tui_outcome(state)[1] if under == "result" else []) + finally: + state.stack.append(top) + return [line for line in lines if _is_detail(line)] or ["Nothing more to say here."] + + +def _view_details(state: TuiState, width: int, height: int) -> tuple: + items = _pager_items(state) + return ("Details", _pager(state, width, height, items), + ("A/Enter Back", *_scroll(state, items, width, height))) + + +def _open_details(state: TuiState) -> list[tuple]: + if state.screen == "result": + # Looked at, they are said again in the terminal after Exit. + state.details_seen = True + return _push(state, "details") + + _TUI_VIEWS: dict[str, Callable[[TuiState, int, int], tuple]] = { "setup": _view_setup, "platforms": _view_platforms, "folder": _view_folder, "browse": _view_browse, "path": _view_path, "create": _view_create, @@ -23014,7 +23141,7 @@ _TUI_VIEWS: dict[str, Callable[[TuiState, int, int], tuple]] = { "preview": _view_preview, "command": _view_command, "confirm": _view_confirm, "installing": _view_installing, "result": _view_result, "error": _view_error, "quit": _view_quit, "cardroot": _view_cardroot, "misplaced": _view_misplaced, - "unreadable": _view_unreadable, "games": _view_games, + "unreadable": _view_unreadable, "games": _view_games, "details": _view_details, } @@ -23078,6 +23205,10 @@ def _help_lines(state: TuiState) -> list[tuple[str, str]]: keys.append(("Left/Right", "Change a value")) if screen in ("plan", "systems", "confirm"): keys.append((names["options"], "Options")) + if screen in ("plan", "confirm"): + keys.append(("d", "Details")) + elif screen == "result" and not tui_rows(state): + keys.append((f"{names['options']}, d" if names["options"] else "d", "Details")) # The result's bar calls B Exit: the help called it Quit. back = "Exit" if screen == "result" else "Quit" if len(state.stack) == 1 else "Back" keys += [(names["back"], back), ("q, Ctrl-C", "Quit"), *kill, *_route_extras(state), @@ -23156,7 +23287,7 @@ def _view_help(state: TuiState, width: int, height: int) -> tuple: # Cut from its end at 40x12, the help lost Quit and This help with no # sign: what a list's keys also say elsewhere goes first. for spare in ("First, last", "All, none", "extra", "Filter by setup or device name", - "Filter", "Options", "This help", "gap", "definitions"): + "Filter", "Options", "Details", "This help", "gap", "definitions"): if (len(drawn(keys)) + len(extra) + len(defined) + len(_text_lines(more, width)) <= height): break @@ -23975,15 +24106,15 @@ def _said_lines(state: TuiState) -> list[str]: def _closing_lines(state: TuiState) -> list[str]: - if state.screen != "result" and state.download is not None: + if state.screen not in ("result", "details") and state.download is not None: # An install ran: what it wrote is said, whatever ended it. title, lines = tui_outcome(state) - return [f"RetroBIOS: {title}", *_terminal_words(lines)] + return [f"RetroBIOS: {title}", *_terminal_words(_farewell_shown(state, lines))] if state.screen == "crashed": return [f"RetroBIOS stopped on an unexpected error. What it was is in {state.log}."] - if state.screen == "result": + if state.screen == "result" or (state.screen == "details" and "result" in state.stack): title, lines = tui_outcome(state) - return [f"RetroBIOS: {title}", *_terminal_words(lines)] + return [f"RetroBIOS: {title}", *_terminal_words(_farewell_shown(state, lines))] if state.screen == "error": return [f"RetroBIOS: {state.error_title or 'something went wrong'}", *(line.replace("then choose Try again", "then run the installer again") @@ -24012,6 +24143,12 @@ TUI_SAVE_REMEDY = "Choose it in Options, Save folder (--save-dir), next time." LINE_SAVE_REMEDY = "Name it with --save-dir next time." +def _farewell_shown(state: TuiState, lines: list) -> list: + """The result as the terminal keeps it: Details too once they were + opened, as the result's copy hint says.""" + return list(lines) if state.details_seen else _shown(state, lines) + + def _terminal_words(lines: list[str]) -> list[str]: """The result's lines as the terminal prints them after the screen.""" return [line.replace(TUI_SAVE_REMEDY, LINE_SAVE_REMEDY) for line in lines diff --git a/install.sh b/install.sh index 36aaf3311..4c22d6cb4 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="00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e" +DEFAULT_INSTALL_SHA256="d07c90a00f5bab9c583c34a9a1413bed5625542e7c88fed7bd9bab0020e9e637" # 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 c056dc24c..4b00af4b4 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": 1135666, - "sha256": "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e", + "size": 1141160, + "sha256": "d07c90a00f5bab9c583c34a9a1413bed5625542e7c88fed7bd9bab0020e9e637", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1189291, - "sha256": "c56ce912e436173542905c89c1aee7de025f93bea5df130ab51e589c071f6369" + "size": 1194785, + "sha256": "344b9bb17095fef0ae3dfcee82f7144075b0c388033337552e37a2910f473dc9" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1135666, - "sha256": "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e", + "size": 1141160, + "sha256": "d07c90a00f5bab9c583c34a9a1413bed5625542e7c88fed7bd9bab0020e9e637", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1135666, - "sha256": "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e", + "size": 1141160, + "sha256": "d07c90a00f5bab9c583c34a9a1413bed5625542e7c88fed7bd9bab0020e9e637", "executable": false }, { diff --git a/scripts/generate_installer_doc.py b/scripts/generate_installer_doc.py index 7baf85733..5a1b795e8 100644 --- a/scripts/generate_installer_doc.py +++ b/scripts/generate_installer_doc.py @@ -604,7 +604,7 @@ def _refused_lines(text: str) -> list[str]: """The files a check flags, each with its reason and where its cores read it.""" lines = text.splitlines() start = next((i for i, line in enumerate(lines) - if re.fullmatch(r" \d+ files? [a-z ]+:", line)), None) + if re.fullmatch(r" \d+ files? [a-z ]+\.", line)), None) if start is None: raise SampleFailed("the check printed no files not in place") end = max(i for i, line in enumerate(lines) if line.lstrip().startswith("read by ")) diff --git a/tests/fixtures/tui/colour-120x40.txt b/tests/fixtures/tui/colour-120x40.txt index 536322f7f..66389a8c2 100644 --- a/tests/fixtures/tui/colour-120x40.txt +++ b/tests/fixtures/tui/colour-120x40.txt @@ -331,13 +331,11 @@ ^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - ^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m - The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder. - Free space: 50.0 GB ^[[0;7;35m > Install^[[0m Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -363,6 +361,8 @@ + + @@ -372,11 +372,9 @@ ^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - ^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, the old one kept in a backup folder. - Free space: 50.0 GB ^[[0;7;35m > Close^[[0m + Details Options @@ -404,6 +402,8 @@ + + @@ -657,11 +657,9 @@ === confirm ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m - Counted again for these choices. Nothing is written before Install. + Counted again for these choices. ^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB ^[[0;7;35m > Install^[[0m Back @@ -691,6 +689,8 @@ + + @@ -705,7 +705,7 @@ B, Ctrl-C or Select+Start stops. What is done stays done. ^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404) -^[[0;32m ok^[[0m gba_bios.bin + @@ -741,14 +741,9 @@ ^[[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 - To put them back: python3 install.py \ - --restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200 - Same choice without questions: - python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log @@ -776,7 +771,12 @@ - ^[[0;1;35mA/Enter^[[0m Exit + + + + + + ^[[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 @@ -865,6 +865,7 @@ Up/Down, k/j Move A/Enter, Start Install Y, o, Tab Options + d Details B/Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help @@ -896,7 +897,6 @@ - Any key closes this help @@ -906,11 +906,6 @@ ^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m For the 412 games on this card. - One region where the games prove it: nintendo-gba Europe. - 2 games get every BIOS of their system: Show the games. - ^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m - The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder. - Free space: 50.0 GB ^[[0;7;35m > Install^[[0m For the games on this card: yes 412 games @@ -918,6 +913,7 @@ Show the games Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -936,6 +932,10 @@ + + + + @@ -969,6 +969,88 @@ + + + + + + + + + + + + + ^[[0;1;35mA/Enter^[[0m Back +=== plan-details +^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Details^[[0m + + ^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m + The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ^[[0;1;35mA/Enter^[[0m Back +=== 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: + python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/tui/colour-40x12.txt b/tests/fixtures/tui/colour-40x12.txt index ce9ed9559..cfdbdf301 100644 --- a/tests/fixtures/tui/colour-40x12.txt +++ b/tests/fixtures/tui/colour-40x12.txt @@ -108,10 +108,10 @@ ^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - ^[[0;33m1 wrong copy^[[0m: backed up, replaced. ^[[0;7;35m > Install^[[0m Choose systems Copy from an old card or a folder + Details Options Quit ^[[0;1;35mA^[[0m Install ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help @@ -121,12 +121,12 @@ ^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - ^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB + ^[[0;7;35m > Close^[[0m + Details Options + + ^[[0;1;35mA^[[0m Close ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help === systems ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m @@ -210,14 +210,14 @@ ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m Counted again for these choices. - Nothing is written before Install. ^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB ^[[0;7;35m > Install^[[0m Back + + + ^[[0;1;35mA^[[0m Install ^[[0;2mB^[[0m Back ^[[0;2m?^[[0m Help === installing ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m @@ -230,7 +230,7 @@ is done stays done. ^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404) -^[[0;32m ok^[[0m gba_bios.bin + ^[[0;1;35mB^[[0m Stop ^[[0;2m?^[[0m Help === result ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mFinished with errors^[[0m @@ -238,13 +238,13 @@ ^[[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 -^[[0;2m 1-8 of 17^[[0m - ^[[0;1;35mA^[[0m Exit ^[[0;2m^v^[[0m Scroll + 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 @@ -277,12 +277,12 @@ Up/Down, k/j Move A/Enter, Start Install Y, o, Tab Options + d Details B/Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help - BIOS folder: - ^[[0;1m/home/user/.config/retroarch/system^[[0m + Any key closes this help === plan-games ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m @@ -295,7 +295,7 @@ For the games on this card: yes 412 Only what starts: no Show the games -^[[0;2m 1-4 of 8^[[0m +^[[0;2m 1-4 of 9^[[0m ^[[0;1;35mA^[[0m Install ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help === games ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Games on this card^[[0m @@ -310,3 +310,29 @@ ^[[0;2m 1-5 of 9^[[0m ^[[0;1;35mA^[[0m Back ^[[0;2m^v^[[0m Scroll +=== plan-details +^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Details^[[0m + + ^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m + The ^[[0;33m1 wrong copy^[[0m is replaced, the old + one kept in a backup folder. + Free space: 50.0 GB + + + + + + ^[[0;1;35mA^[[0m Back +=== 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 + Same choice without questions: + python3 install.py \ + --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 8b56ed9b5..4772e545b 100644 --- a/tests/fixtures/tui/colour-40x15.txt +++ b/tests/fixtures/tui/colour-40x15.txt @@ -132,15 +132,15 @@ ^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - ^[[0;33m1 wrong copy^[[0m: backed up, replaced. - ^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m - Free space: 50.0 GB ^[[0;7;35m > Install^[[0m Choose systems Copy from an old card or a folder + Details Options Quit + + ^[[0;1;35mA^[[0m Install ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help === plan-in-place ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m @@ -148,15 +148,15 @@ ^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - ^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB ^[[0;7;35m > Close^[[0m + Details Options + + + ^[[0;1;35mA^[[0m Close ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help === systems ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m @@ -258,17 +258,17 @@ ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m Counted again for these choices. - Nothing is written before Install. ^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB ^[[0;7;35m > Install^[[0m Back + + + ^[[0;1;35mA^[[0m Install ^[[0;2mB^[[0m Back ^[[0;2m?^[[0m Help === installing ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m @@ -281,7 +281,7 @@ is done stays done. ^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404) -^[[0;32m ok^[[0m gba_bios.bin + @@ -292,16 +292,16 @@ ^[[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 - To put them back: python3 install.py \ - --restore .../2026-10-23_1200 -^[[0;2m Shortened to fit this window.^[[0m -^[[0;2m 1-11 of 17^[[0m - ^[[0;1;35mA^[[0m Exit ^[[0;2m^v^[[0m Scroll + 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 @@ -340,6 +340,7 @@ Up/Down, k/j Move A/Enter, Start Install Y, o, Tab Options + d Details B/Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help @@ -348,7 +349,6 @@ ^[[0;1m/home/user/.config/retroarch/system^[[0m - Any key closes this help === plan-games ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m @@ -356,13 +356,13 @@ ^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - For the 412 games on this card. ^[[0;7;35m > Install^[[0m For the games on this card: yes 412 Only what starts: no Show the games Choose systems Copy from an old card or a folder + Details Options Quit ^[[0;1;35mA^[[0m Install ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help @@ -381,4 +381,36 @@ that they start: GBA/demo.gba: not recognised GBA/hack.gba: not recognised + ^[[0;1;35mA^[[0m Back +=== plan-details +^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Details^[[0m + + ^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m + The ^[[0;33m1 wrong copy^[[0m is replaced, the old + one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + ^[[0;1;35mA^[[0m Back +=== 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 + Same choice without questions: + python3 install.py \ + --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-80x24.txt b/tests/fixtures/tui/colour-80x24.txt index 1fbde9a63..f86488285 100644 --- a/tests/fixtures/tui/colour-80x24.txt +++ b/tests/fixtures/tui/colour-80x24.txt @@ -203,13 +203,11 @@ ^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - ^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m - The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder. - Free space: 50.0 GB ^[[0;7;35m > Install^[[0m Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -222,17 +220,17 @@ + + ^[[0;1;35mA/Enter^[[0m Install ^[[0;2mB/Esc^[[0m Back ^[[0;2mY/o^[[0m Options ^[[0;2m?^[[0m Help === plan-in-place ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m ^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - ^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, the old one kept in a backup folder. - Free space: 50.0 GB ^[[0;7;35m > Close^[[0m + Details Options @@ -247,6 +245,8 @@ + + ^[[0;1;35mA/Enter^[[0m Close ^[[0;2mB/Esc^[[0m Back ^[[0;2mY/o^[[0m Options ^[[0;2m?^[[0m Help === systems ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m @@ -401,11 +401,9 @@ === confirm ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m - Counted again for these choices. Nothing is written before Install. + Counted again for these choices. ^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB ^[[0;7;35m > Install^[[0m Back @@ -422,6 +420,8 @@ + + ^[[0;1;35mA/Enter^[[0m Install ^[[0;2mB/Esc^[[0m Back ^[[0;2m?^[[0m Help === installing ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m @@ -433,7 +433,7 @@ B, Ctrl-C or Select+Start stops. What is done stays done. ^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404) -^[[0;32m ok^[[0m gba_bios.bin + @@ -453,16 +453,10 @@ ^[[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 - To put them back: python3 install.py \ - --restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200 - Same choice without questions: - python3 install.py --platform retroarch \ - --dest /home/user/.config/retroarch/system - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log @@ -472,7 +466,13 @@ - ^[[0;1;35mA/Enter^[[0m Exit + + + + + + + ^[[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 @@ -529,6 +529,7 @@ Up/Down, k/j Move A/Enter, Start Install Y, o, Tab Options + d Details B/Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help @@ -546,7 +547,6 @@ - Any key closes this help === plan-games ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m @@ -554,11 +554,6 @@ ^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m For the 412 games on this card. - One region where the games prove it: nintendo-gba Europe. - 2 games get every BIOS of their system: Show the games. - ^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m - The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder. - Free space: 50.0 GB ^[[0;7;35m > Install^[[0m For the games on this card: yes 412 games @@ -566,12 +561,17 @@ Show the games Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit + + + + ^[[0;1;35mA/Enter^[[0m Install ^[[0;2mB/Esc^[[0m Back ^[[0;2mY/o^[[0m Options ^[[0;2m?^[[0m Help === games ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Games on this card^[[0m @@ -597,4 +597,54 @@ + ^[[0;1;35mA/Enter^[[0m Back +=== plan-details +^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Details^[[0m + + ^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m + The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + + + + + + + + + + + ^[[0;1;35mA/Enter^[[0m Back +=== 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: + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system + + + + + + + + + + + + + + + ^[[0;1;35mA/Enter^[[0m Back diff --git a/tests/fixtures/tui/confirm-120x40.txt b/tests/fixtures/tui/confirm-120x40.txt index c870132df..733790008 100644 --- a/tests/fixtures/tui/confirm-120x40.txt +++ b/tests/fixtures/tui/confirm-120x40.txt @@ -1,10 +1,8 @@ RetroBIOS: Confirm - Counted again for these choices. Nothing is written before Install. + Counted again for these choices. 1 file to download, 16.0 KB BIOS folder: ~/.config/retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back @@ -34,6 +32,8 @@ + + diff --git a/tests/fixtures/tui/confirm-40x12.txt b/tests/fixtures/tui/confirm-40x12.txt index 9e57bb545..22629b910 100644 --- a/tests/fixtures/tui/confirm-40x12.txt +++ b/tests/fixtures/tui/confirm-40x12.txt @@ -1,12 +1,12 @@ RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + A Install B Back ? Help diff --git a/tests/fixtures/tui/confirm-40x15.txt b/tests/fixtures/tui/confirm-40x15.txt index d18f06885..572a2678f 100644 --- a/tests/fixtures/tui/confirm-40x15.txt +++ b/tests/fixtures/tui/confirm-40x15.txt @@ -1,15 +1,15 @@ RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + A Install B Back ? Help diff --git a/tests/fixtures/tui/confirm-80x24.txt b/tests/fixtures/tui/confirm-80x24.txt index a44f71b14..ba06ca00a 100644 --- a/tests/fixtures/tui/confirm-80x24.txt +++ b/tests/fixtures/tui/confirm-80x24.txt @@ -1,10 +1,8 @@ RetroBIOS: Confirm - Counted again for these choices. Nothing is written before Install. + Counted again for these choices. 1 file to download, 16.0 KB BIOS folder: ~/.config/retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back @@ -21,4 +19,6 @@ + + A/Enter Install B/Esc Back ? Help diff --git a/tests/fixtures/tui/help-120x40.txt b/tests/fixtures/tui/help-120x40.txt index 71ba78093..edf48ff0f 100644 --- a/tests/fixtures/tui/help-120x40.txt +++ b/tests/fixtures/tui/help-120x40.txt @@ -3,6 +3,7 @@ Up/Down, k/j Move A/Enter, Start Install Y, o, Tab Options + d Details B/Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help @@ -34,7 +35,6 @@ - Any key closes this help diff --git a/tests/fixtures/tui/help-40x12.txt b/tests/fixtures/tui/help-40x12.txt index 09b22304d..5362abd16 100644 --- a/tests/fixtures/tui/help-40x12.txt +++ b/tests/fixtures/tui/help-40x12.txt @@ -3,10 +3,10 @@ Up/Down, k/j Move A/Enter, Start Install Y, o, Tab Options + d Details B/Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help - BIOS folder: - /home/user/.config/retroarch/system + Any key closes this help diff --git a/tests/fixtures/tui/help-40x15.txt b/tests/fixtures/tui/help-40x15.txt index 830acb87b..4ee7e371c 100644 --- a/tests/fixtures/tui/help-40x15.txt +++ b/tests/fixtures/tui/help-40x15.txt @@ -3,6 +3,7 @@ Up/Down, k/j Move A/Enter, Start Install Y, o, Tab Options + d Details B/Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help @@ -11,5 +12,4 @@ /home/user/.config/retroarch/system - Any key closes this help diff --git a/tests/fixtures/tui/help-80x24.txt b/tests/fixtures/tui/help-80x24.txt index 557dd2178..b22ec4987 100644 --- a/tests/fixtures/tui/help-80x24.txt +++ b/tests/fixtures/tui/help-80x24.txt @@ -3,6 +3,7 @@ Up/Down, k/j Move A/Enter, Start Install Y, o, Tab Options + d Details B/Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help @@ -20,5 +21,4 @@ - Any key closes this help diff --git a/tests/fixtures/tui/installing-120x40.txt b/tests/fixtures/tui/installing-120x40.txt index ff7651b12..3660aebba 100644 --- a/tests/fixtures/tui/installing-120x40.txt +++ b/tests/fixtures/tui/installing-120x40.txt @@ -7,7 +7,7 @@ B, Ctrl-C or Select+Start stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + diff --git a/tests/fixtures/tui/installing-40x12.txt b/tests/fixtures/tui/installing-40x12.txt index d56a30bbd..e92b7c2f3 100644 --- a/tests/fixtures/tui/installing-40x12.txt +++ b/tests/fixtures/tui/installing-40x12.txt @@ -8,5 +8,5 @@ is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + B Stop ? Help diff --git a/tests/fixtures/tui/installing-40x15.txt b/tests/fixtures/tui/installing-40x15.txt index 950ad6515..011e1dce2 100644 --- a/tests/fixtures/tui/installing-40x15.txt +++ b/tests/fixtures/tui/installing-40x15.txt @@ -8,7 +8,7 @@ is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + diff --git a/tests/fixtures/tui/installing-80x24.txt b/tests/fixtures/tui/installing-80x24.txt index fdd275588..338c47ec9 100644 --- a/tests/fixtures/tui/installing-80x24.txt +++ b/tests/fixtures/tui/installing-80x24.txt @@ -7,7 +7,7 @@ B, Ctrl-C or Select+Start stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + diff --git a/tests/fixtures/tui/plan-120x40.txt b/tests/fixtures/tui/plan-120x40.txt index 3ca54cb75..ca67ddd86 100644 --- a/tests/fixtures/tui/plan-120x40.txt +++ b/tests/fixtures/tui/plan-120x40.txt @@ -2,13 +2,11 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -34,6 +32,8 @@ + + diff --git a/tests/fixtures/tui/plan-40x12.txt b/tests/fixtures/tui/plan-40x12.txt index ef7c55353..26f08e468 100644 --- a/tests/fixtures/tui/plan-40x12.txt +++ b/tests/fixtures/tui/plan-40x12.txt @@ -3,10 +3,10 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. > Install Choose systems Copy from an old card or a folder + Details Options Quit A Install B Back Y Options ? Help diff --git a/tests/fixtures/tui/plan-40x15.txt b/tests/fixtures/tui/plan-40x15.txt index bf4817eda..5e2e6a6ea 100644 --- a/tests/fixtures/tui/plan-40x15.txt +++ b/tests/fixtures/tui/plan-40x15.txt @@ -3,13 +3,13 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. - 1 in place, 3 missing, 1 wrong copy - Free space: 50.0 GB > Install Choose systems Copy from an old card or a folder + Details Options Quit + + A Install B Back Y Options ? Help diff --git a/tests/fixtures/tui/plan-80x24.txt b/tests/fixtures/tui/plan-80x24.txt index 112f7c53a..09a860db6 100644 --- a/tests/fixtures/tui/plan-80x24.txt +++ b/tests/fixtures/tui/plan-80x24.txt @@ -2,13 +2,11 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -21,4 +19,6 @@ + + A/Enter Install B/Esc Back Y/o Options ? Help diff --git a/tests/fixtures/tui/plan-details-120x40.txt b/tests/fixtures/tui/plan-details-120x40.txt new file mode 100644 index 000000000..a875ed363 --- /dev/null +++ b/tests/fixtures/tui/plan-details-120x40.txt @@ -0,0 +1,40 @@ + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A/Enter Back diff --git a/tests/fixtures/tui/plan-details-40x12.txt b/tests/fixtures/tui/plan-details-40x12.txt new file mode 100644 index 000000000..01c228e8e --- /dev/null +++ b/tests/fixtures/tui/plan-details-40x12.txt @@ -0,0 +1,12 @@ + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old + one kept in a backup folder. + Free space: 50.0 GB + + + + + + A Back diff --git a/tests/fixtures/tui/plan-details-40x15.txt b/tests/fixtures/tui/plan-details-40x15.txt new file mode 100644 index 000000000..04d3cb149 --- /dev/null +++ b/tests/fixtures/tui/plan-details-40x15.txt @@ -0,0 +1,15 @@ + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old + one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + A Back diff --git a/tests/fixtures/tui/plan-details-80x24.txt b/tests/fixtures/tui/plan-details-80x24.txt new file mode 100644 index 000000000..5e33341d1 --- /dev/null +++ b/tests/fixtures/tui/plan-details-80x24.txt @@ -0,0 +1,24 @@ + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + + + + + + + + + + + A/Enter Back diff --git a/tests/fixtures/tui/plan-games-120x40.txt b/tests/fixtures/tui/plan-games-120x40.txt index 14b9e837a..157d4e6e9 100644 --- a/tests/fixtures/tui/plan-games-120x40.txt +++ b/tests/fixtures/tui/plan-games-120x40.txt @@ -3,11 +3,6 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system For the 412 games on this card. - One region where the games prove it: nintendo-gba Europe. - 2 games get every BIOS of their system: Show the games. - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install For the games on this card: yes 412 games @@ -15,6 +10,7 @@ Show the games Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -33,6 +29,10 @@ + + + + diff --git a/tests/fixtures/tui/plan-games-40x12.txt b/tests/fixtures/tui/plan-games-40x12.txt index 34dfd18a2..800da31b1 100644 --- a/tests/fixtures/tui/plan-games-40x12.txt +++ b/tests/fixtures/tui/plan-games-40x12.txt @@ -8,5 +8,5 @@ For the games on this card: yes 412 Only what starts: no Show the games - 1-4 of 8 + 1-4 of 9 A Install B Back Y Options ? Help diff --git a/tests/fixtures/tui/plan-games-40x15.txt b/tests/fixtures/tui/plan-games-40x15.txt index b8573927e..2d208f0e1 100644 --- a/tests/fixtures/tui/plan-games-40x15.txt +++ b/tests/fixtures/tui/plan-games-40x15.txt @@ -3,13 +3,13 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - For the 412 games on this card. > Install For the games on this card: yes 412 Only what starts: no Show the games Choose systems Copy from an old card or a folder + Details Options Quit A Install B Back Y Options ? Help diff --git a/tests/fixtures/tui/plan-games-80x24.txt b/tests/fixtures/tui/plan-games-80x24.txt index a6389d14b..81c89f428 100644 --- a/tests/fixtures/tui/plan-games-80x24.txt +++ b/tests/fixtures/tui/plan-games-80x24.txt @@ -3,11 +3,6 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system For the 412 games on this card. - One region where the games prove it: nintendo-gba Europe. - 2 games get every BIOS of their system: Show the games. - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install For the games on this card: yes 412 games @@ -15,10 +10,15 @@ Show the games Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit + + + + A/Enter Install B/Esc Back Y/o Options ? Help diff --git a/tests/fixtures/tui/plan-in-place-120x40.txt b/tests/fixtures/tui/plan-in-place-120x40.txt index 0e5b41f91..7001d8a90 100644 --- a/tests/fixtures/tui/plan-in-place-120x40.txt +++ b/tests/fixtures/tui/plan-in-place-120x40.txt @@ -2,11 +2,9 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options @@ -34,6 +32,8 @@ + + diff --git a/tests/fixtures/tui/plan-in-place-40x12.txt b/tests/fixtures/tui/plan-in-place-40x12.txt index 83ce5207e..7b46aabc7 100644 --- a/tests/fixtures/tui/plan-in-place-40x12.txt +++ b/tests/fixtures/tui/plan-in-place-40x12.txt @@ -3,10 +3,10 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB + > Close + Details Options + + A Close B Back Y Options ? Help diff --git a/tests/fixtures/tui/plan-in-place-40x15.txt b/tests/fixtures/tui/plan-in-place-40x15.txt index f5f2d9033..182af92d6 100644 --- a/tests/fixtures/tui/plan-in-place-40x15.txt +++ b/tests/fixtures/tui/plan-in-place-40x15.txt @@ -3,13 +3,13 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options + + + A Close B Back Y Options ? Help diff --git a/tests/fixtures/tui/plan-in-place-80x24.txt b/tests/fixtures/tui/plan-in-place-80x24.txt index da0702005..36673c825 100644 --- a/tests/fixtures/tui/plan-in-place-80x24.txt +++ b/tests/fixtures/tui/plan-in-place-80x24.txt @@ -2,11 +2,9 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options @@ -21,4 +19,6 @@ + + A/Enter Close B/Esc Back Y/o Options ? Help diff --git a/tests/fixtures/tui/result-120x40.txt b/tests/fixtures/tui/result-120x40.txt index 550ec7724..628078bd4 100644 --- a/tests/fixtures/tui/result-120x40.txt +++ b/tests/fixtures/tui/result-120x40.txt @@ -2,14 +2,9 @@ 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 - To put them back: python3 install.py \ - --restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200 - Same choice without questions: - python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log @@ -37,4 +32,9 @@ - A/Enter Exit + + + + + + A/Enter Exit Y/o Details diff --git a/tests/fixtures/tui/result-40x12.txt b/tests/fixtures/tui/result-40x12.txt index 12339e200..d0c87bb72 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 - 1-8 of 17 - A Exit ^v Scroll + 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 61db5344b..395f4cf8e 100644 --- a/tests/fixtures/tui/result-40x15.txt +++ b/tests/fixtures/tui/result-40x15.txt @@ -3,13 +3,13 @@ 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 - To put them back: python3 install.py \ - --restore .../2026-10-23_1200 - Shortened to fit this window. - 1-11 of 17 - A Exit ^v Scroll + Log: /tmp/retrobios-x/install.log + + + + + A Exit Y Details diff --git a/tests/fixtures/tui/result-80x24.txt b/tests/fixtures/tui/result-80x24.txt index ab4770376..ca5097819 100644 --- a/tests/fixtures/tui/result-80x24.txt +++ b/tests/fixtures/tui/result-80x24.txt @@ -2,16 +2,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 - To put them back: python3 install.py \ - --restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200 - Same choice without questions: - python3 install.py --platform retroarch \ - --dest /home/user/.config/retroarch/system - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log @@ -21,4 +15,10 @@ - A/Enter Exit + + + + + + + A/Enter Exit Y/o Details diff --git a/tests/fixtures/tui/result-details-120x40.txt b/tests/fixtures/tui/result-details-120x40.txt new file mode 100644 index 000000000..1896a865d --- /dev/null +++ b/tests/fixtures/tui/result-details-120x40.txt @@ -0,0 +1,40 @@ + 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: + python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A/Enter Back diff --git a/tests/fixtures/tui/result-details-40x12.txt b/tests/fixtures/tui/result-details-40x12.txt new file mode 100644 index 000000000..063668de9 --- /dev/null +++ b/tests/fixtures/tui/result-details-40x12.txt @@ -0,0 +1,12 @@ + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + To put them back: python3 install.py \ + --restore .../2026-10-23_1200 + Shortened to fit this window. + Same choice without questions: + python3 install.py \ + --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 new file mode 100644 index 000000000..efd9f6523 --- /dev/null +++ b/tests/fixtures/tui/result-details-40x15.txt @@ -0,0 +1,15 @@ + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + To put them back: python3 install.py \ + --restore .../2026-10-23_1200 + Shortened to fit this window. + Same choice without questions: + python3 install.py \ + --platform retroarch \ + --dest .../retroarch/system + Shortened to fit this window. + + + + A Back diff --git a/tests/fixtures/tui/result-details-80x24.txt b/tests/fixtures/tui/result-details-80x24.txt new file mode 100644 index 000000000..9d05dd8aa --- /dev/null +++ b/tests/fixtures/tui/result-details-80x24.txt @@ -0,0 +1,24 @@ + 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: + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system + + + + + + + + + + + + + + + + A/Enter Back diff --git a/tests/fixtures/tui/route-evmapy.txt b/tests/fixtures/tui/route-evmapy.txt index 5f3beed24..b8a915afb 100644 --- a/tests/fixtures/tui/route-evmapy.txt +++ b/tests/fixtures/tui/route-evmapy.txt @@ -407,10 +407,10 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. > Install Choose systems Copy from an old card or a folder + Details Options Quit A Install B Back Y Options @@ -420,28 +420,26 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. - 1 in place, 3 missing, 1 wrong copy - Free space: 50.0 GB > Install Choose systems Copy from an old card or a folder + Details Options Quit + + A Install B Back Y Options === plan 80x24 RetroBIOS: RetroArch 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -454,6 +452,8 @@ + + A Install B Back Y Options === plan-in-place 40x12 RetroBIOS: RetroArch @@ -461,12 +461,12 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB + > Close + Details Options + + A Close B Back Y Options === plan-in-place 40x15 RetroBIOS: RetroArch @@ -474,26 +474,24 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options + + + A Close B Back Y Options === plan-in-place 80x24 RetroBIOS: RetroArch Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options @@ -508,6 +506,8 @@ + + A Close B Back Y Options === systems 40x12 RetroBIOS: Choose systems @@ -779,39 +779,37 @@ RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + A Install B Back === confirm 40x15 RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + A Install B Back === confirm 80x24 RetroBIOS: Confirm - Counted again for these choices. Nothing is written before Install. + Counted again for these choices. 1 file to download, 16.0 KB BIOS folder: ~/.config/retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back @@ -828,6 +826,8 @@ + + A Install B Back === installing 40x12 RetroBIOS: Installing @@ -839,7 +839,7 @@ B stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + B Stop === installing 40x15 @@ -852,7 +852,7 @@ B stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -868,7 +868,7 @@ B stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -883,45 +883,15 @@ B Stop -=== result 40x12 - 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 - Details: /tmp/retrobios-x/install.log - A Exit -=== result 40x15 - 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 - Details: /tmp/retrobios-x/install.log - - - - A 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 - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log @@ -936,7 +906,8 @@ - A Exit + + A Exit Y Details === error 40x12 RetroBIOS: Something went wrong @@ -1006,12 +977,12 @@ D-pad, Up/Down Move A, Start, Enter Install Y, Tab, o Options + d Details B, Select, Esc Back q, Ctrl-C Quit BIOS folder: /home/user/.config/retroarch/system - Any key closes this help === help 40x15 RetroBIOS: Keys @@ -1019,6 +990,7 @@ D-pad, Up/Down Move A, Start, Enter Install Y, Tab, o Options + d Details B, Select, Esc Back q, Ctrl-C Quit @@ -1027,7 +999,6 @@ - Any key closes this help === help 80x24 RetroBIOS: Keys @@ -1035,6 +1006,7 @@ D-pad, Up/Down Move A, Start, Enter Install Y, Tab, o Options + d Details B, Select, Esc Back q, Ctrl-C Quit @@ -1052,7 +1024,6 @@ - Any key closes this help === plan-games 40x12 RetroBIOS: RetroArch @@ -1065,7 +1036,7 @@ For the games on this card: yes 412 Only what starts: no Show the games - 1-4 of 8 + 1-4 of 9 A Install B Back Y Options === plan-games 40x15 RetroBIOS: RetroArch @@ -1073,13 +1044,13 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - For the 412 games on this card. > Install For the games on this card: yes 412 Only what starts: no Show the games Choose systems Copy from an old card or a folder + Details Options Quit A Install B Back Y Options @@ -1089,11 +1060,6 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system For the 412 games on this card. - One region where the games prove it: nintendo-gba Europe. - 2 games get every BIOS of their system: Show the games. - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install For the games on this card: yes 412 games @@ -1101,12 +1067,17 @@ Show the games Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit + + + + A Install B Back Y Options === games 80x24 RetroBIOS: Games on this card @@ -1132,4 +1103,83 @@ + A Back +=== plan-details 80x24 + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + + + + + + + + + + + A Back +=== result-details 40x12 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + A Back +=== result-details 40x15 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + + + + A Back +=== result-details 80x24 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + + + + + + + + + + + + A Back diff --git a/tests/fixtures/tui/route-gptokeyb.txt b/tests/fixtures/tui/route-gptokeyb.txt index 13e24690f..f1179b688 100644 --- a/tests/fixtures/tui/route-gptokeyb.txt +++ b/tests/fixtures/tui/route-gptokeyb.txt @@ -407,10 +407,10 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. > Install Choose systems Copy from an old card or a folder + Details Options Quit A Install B Back Y Options @@ -420,28 +420,26 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. - 1 in place, 3 missing, 1 wrong copy - Free space: 50.0 GB > Install Choose systems Copy from an old card or a folder + Details Options Quit + + A Install B Back Y Options === plan 80x24 RetroBIOS: RetroArch 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -454,6 +452,8 @@ + + A Install B Back Y Options === plan-in-place 40x12 RetroBIOS: RetroArch @@ -461,12 +461,12 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB + > Close + Details Options + + A Close B Back Y Options === plan-in-place 40x15 RetroBIOS: RetroArch @@ -474,26 +474,24 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options + + + A Close B Back Y Options === plan-in-place 80x24 RetroBIOS: RetroArch Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options @@ -508,6 +506,8 @@ + + A Close B Back Y Options === systems 40x12 RetroBIOS: Choose systems @@ -779,39 +779,37 @@ RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + A Install B Back === confirm 40x15 RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + A Install B Back === confirm 80x24 RetroBIOS: Confirm - Counted again for these choices. Nothing is written before Install. + Counted again for these choices. 1 file to download, 16.0 KB BIOS folder: ~/.config/retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back @@ -828,6 +826,8 @@ + + A Install B Back === installing 40x12 RetroBIOS: Installing @@ -840,7 +840,7 @@ stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + B Stop === installing 40x15 RetroBIOS: Installing @@ -853,7 +853,7 @@ stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -868,7 +868,7 @@ B or Select+Start stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -883,45 +883,15 @@ B Stop -=== result 40x12 - 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 - Details: /tmp/retrobios-x/install.log - A Exit -=== result 40x15 - 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 - Details: /tmp/retrobios-x/install.log - - - - A 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 - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log @@ -936,7 +906,8 @@ - A Exit + + A Exit Y Details === error 40x12 RetroBIOS: Something went wrong @@ -1006,12 +977,12 @@ D-pad, Up/Down Move A, Start, Enter Install Y, Tab, o Options + d Details B, Select, Esc Back q, Ctrl-C Quit Select, then Start Quit at once - BIOS folder: - /home/user/.config/retroarch/system + Any key closes this help === help 40x15 RetroBIOS: Keys @@ -1019,6 +990,7 @@ D-pad, Up/Down Move A, Start, Enter Install Y, Tab, o Options + d Details B, Select, Esc Back q, Ctrl-C Quit Select, then Start Quit at once @@ -1027,7 +999,6 @@ /home/user/.config/retroarch/system - Any key closes this help === help 80x24 RetroBIOS: Keys @@ -1035,6 +1006,7 @@ D-pad, Up/Down Move A, Start, Enter Install Y, Tab, o Options + d Details B, Select, Esc Back q, Ctrl-C Quit Select, then Start Quit at once @@ -1052,7 +1024,6 @@ - Any key closes this help === plan-games 40x12 RetroBIOS: RetroArch @@ -1065,7 +1036,7 @@ For the games on this card: yes 412 Only what starts: no Show the games - 1-4 of 8 + 1-4 of 9 A Install B Back Y Options === plan-games 40x15 RetroBIOS: RetroArch @@ -1073,13 +1044,13 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - For the 412 games on this card. > Install For the games on this card: yes 412 Only what starts: no Show the games Choose systems Copy from an old card or a folder + Details Options Quit A Install B Back Y Options @@ -1089,11 +1060,6 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system For the 412 games on this card. - One region where the games prove it: nintendo-gba Europe. - 2 games get every BIOS of their system: Show the games. - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install For the games on this card: yes 412 games @@ -1101,12 +1067,17 @@ Show the games Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit + + + + A Install B Back Y Options === games 80x24 RetroBIOS: Games on this card @@ -1132,4 +1103,83 @@ + A Back +=== plan-details 80x24 + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + + + + + + + + + + + A Back +=== result-details 40x12 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + A Back +=== result-details 40x15 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + + + + A Back +=== result-details 80x24 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + + + + + + + + + + + + A Back diff --git a/tests/fixtures/tui/route-gptokeyb_muxterm.txt b/tests/fixtures/tui/route-gptokeyb_muxterm.txt index 9342b9667..c285fd3d9 100644 --- a/tests/fixtures/tui/route-gptokeyb_muxterm.txt +++ b/tests/fixtures/tui/route-gptokeyb_muxterm.txt @@ -407,10 +407,10 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. > Install Choose systems Copy from an old card or a folder + Details Options Quit A Install B Back Y Options @@ -420,28 +420,26 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. - 1 in place, 3 missing, 1 wrong copy - Free space: 50.0 GB > Install Choose systems Copy from an old card or a folder + Details Options Quit + + A Install B Back Y Options === plan 80x24 RetroBIOS: RetroArch 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -454,6 +452,8 @@ + + A Install B Back Y Options === plan-in-place 40x12 RetroBIOS: RetroArch @@ -461,12 +461,12 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB + > Close + Details Options + + A Close B Back Y Options === plan-in-place 40x15 RetroBIOS: RetroArch @@ -474,26 +474,24 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options + + + A Close B Back Y Options === plan-in-place 80x24 RetroBIOS: RetroArch Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options @@ -508,6 +506,8 @@ + + A Close B Back Y Options === systems 40x12 RetroBIOS: Choose systems @@ -779,39 +779,37 @@ RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + A Install B Back === confirm 40x15 RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + A Install B Back === confirm 80x24 RetroBIOS: Confirm - Counted again for these choices. Nothing is written before Install. + Counted again for these choices. 1 file to download, 16.0 KB BIOS folder: ~/.config/retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back @@ -828,6 +826,8 @@ + + A Install B Back === installing 40x12 RetroBIOS: Installing @@ -840,7 +840,7 @@ stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + B Stop === installing 40x15 RetroBIOS: Installing @@ -853,7 +853,7 @@ stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -868,7 +868,7 @@ B or Select+Start stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -883,45 +883,15 @@ B Stop -=== result 40x12 - 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 - Details: /tmp/retrobios-x/install.log - A Exit -=== result 40x15 - 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 - Details: /tmp/retrobios-x/install.log - - - - A 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 - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log @@ -936,7 +906,8 @@ - A Exit + + A Exit Y Details === error 40x12 RetroBIOS: Something went wrong @@ -1006,12 +977,12 @@ D-pad, Up/Down Move A, Start, Enter Install Y, Tab, o Options + d Details B, Select, Esc Back q, Ctrl-C Quit Select, then Start Quit at once - BIOS folder: - /home/user/.config/retroarch/system + Any key closes this help === help 40x15 RetroBIOS: Keys @@ -1019,6 +990,7 @@ D-pad, Up/Down Move A, Start, Enter Install Y, Tab, o Options + d Details B, Select, Esc Back q, Ctrl-C Quit Select, then Start Quit at once @@ -1027,7 +999,6 @@ /home/user/.config/retroarch/system - Any key closes this help === help 80x24 RetroBIOS: Keys @@ -1035,6 +1006,7 @@ D-pad, Up/Down Move A, Start, Enter Install Y, Tab, o Options + d Details B, Select, Esc Back q, Ctrl-C Quit Select, then Start Quit at once @@ -1052,7 +1024,6 @@ - Any key closes this help === plan-games 40x12 RetroBIOS: RetroArch @@ -1065,7 +1036,7 @@ For the games on this card: yes 412 Only what starts: no Show the games - 1-4 of 8 + 1-4 of 9 A Install B Back Y Options === plan-games 40x15 RetroBIOS: RetroArch @@ -1073,13 +1044,13 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - For the 412 games on this card. > Install For the games on this card: yes 412 Only what starts: no Show the games Choose systems Copy from an old card or a folder + Details Options Quit A Install B Back Y Options @@ -1089,11 +1060,6 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system For the 412 games on this card. - One region where the games prove it: nintendo-gba Europe. - 2 games get every BIOS of their system: Show the games. - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install For the games on this card: yes 412 games @@ -1101,12 +1067,17 @@ Show the games Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit + + + + A Install B Back Y Options === games 80x24 RetroBIOS: Games on this card @@ -1132,4 +1103,83 @@ + A Back +=== plan-details 80x24 + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + + + + + + + + + + + A Back +=== result-details 40x12 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + A Back +=== result-details 40x15 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + + + + A Back +=== result-details 80x24 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + + + + + + + + + + + + A Back diff --git a/tests/fixtures/tui/route-keys.txt b/tests/fixtures/tui/route-keys.txt index 4d2e586eb..557245079 100644 --- a/tests/fixtures/tui/route-keys.txt +++ b/tests/fixtures/tui/route-keys.txt @@ -382,10 +382,10 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. > Install Choose systems Copy from an old card or a folder + Details Options Quit Enter Install Esc Back o Options @@ -395,28 +395,26 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. - 1 in place, 3 missing, 1 wrong copy - Free space: 50.0 GB > Install Choose systems Copy from an old card or a folder + Details Options Quit + + Enter Install Esc Back o Options === plan 80x24 RetroBIOS: RetroArch 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -429,6 +427,8 @@ + + Enter Install Esc Back o Options ? Help === plan-in-place 40x12 RetroBIOS: RetroArch @@ -436,12 +436,12 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB + > Close + Details Options + + Enter Close Esc Back o Options === plan-in-place 40x15 RetroBIOS: RetroArch @@ -449,26 +449,24 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options + + + Enter Close Esc Back o Options === plan-in-place 80x24 RetroBIOS: RetroArch Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options @@ -483,6 +481,8 @@ + + Enter Close Esc Back o Options ? Help === systems 40x12 RetroBIOS: Choose systems @@ -812,39 +812,37 @@ RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + Enter Install Esc Back ? Help === confirm 40x15 RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + Enter Install Esc Back ? Help === confirm 80x24 RetroBIOS: Confirm - Counted again for these choices. Nothing is written before Install. + Counted again for these choices. 1 file to download, 16.0 KB BIOS folder: ~/.config/retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back @@ -861,6 +859,8 @@ + + Enter Install Esc Back ? Help === installing 40x12 RetroBIOS: Installing @@ -873,7 +873,7 @@ stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + Esc Stop ? Help === installing 40x15 RetroBIOS: Installing @@ -886,7 +886,7 @@ stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -901,7 +901,7 @@ Esc or Ctrl-C stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -922,44 +922,38 @@ 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 - 1-8 of 17 - Enter Exit ^v Scroll + Log: /tmp/retrobios-x/install.log + + Enter Exit o Details === result 40x15 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 - To put them back: python3 install.py \ - --restore .../2026-10-23_1200 - Shortened to fit this window. - 1-11 of 17 - Enter Exit ^v Scroll + Log: /tmp/retrobios-x/install.log + + + + + 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 - To put them back: python3 install.py \ - --restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200 - Same choice without questions: - python3 install.py --platform retroarch \ - --dest /home/user/.config/retroarch/system - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log @@ -969,7 +963,13 @@ - Enter Exit + + + + + + + Enter Exit o Details === error 40x12 RetroBIOS: Something went wrong @@ -1084,12 +1084,12 @@ Up/Down, k/j Move Enter Install o, Tab Options + d Details Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help - BIOS folder: - /home/user/.config/retroarch/system + Any key closes this help === help 40x15 RetroBIOS: Keys @@ -1097,6 +1097,7 @@ Up/Down, k/j Move Enter Install o, Tab Options + d Details Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help @@ -1105,7 +1106,6 @@ /home/user/.config/retroarch/system - Any key closes this help === help 80x24 RetroBIOS: Keys @@ -1113,6 +1113,7 @@ Up/Down, k/j Move Enter Install o, Tab Options + d Details Esc, Backspace Back q, Ctrl-C Quit ?, F1 This help @@ -1130,7 +1131,6 @@ - Any key closes this help === plan-games 40x12 RetroBIOS: RetroArch @@ -1143,7 +1143,7 @@ For the games on this card: yes 412 Only what starts: no Show the games - 1-4 of 8 + 1-4 of 9 Enter Install Esc Back o Options === plan-games 40x15 RetroBIOS: RetroArch @@ -1151,13 +1151,13 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - For the 412 games on this card. > Install For the games on this card: yes 412 Only what starts: no Show the games Choose systems Copy from an old card or a folder + Details Options Quit Enter Install Esc Back o Options @@ -1167,11 +1167,6 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system For the 412 games on this card. - One region where the games prove it: nintendo-gba Europe. - 2 games get every BIOS of their system: Show the games. - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install For the games on this card: yes 412 games @@ -1179,12 +1174,17 @@ Show the games Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit + + + + Enter Install Esc Back o Options ? Help === games 40x12 RetroBIOS: Games on this card @@ -1239,4 +1239,112 @@ + Enter Back +=== plan-details 40x12 + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old + one kept in a backup folder. + Free space: 50.0 GB + + + + + + Enter Back +=== plan-details 40x15 + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old + one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + Enter Back +=== plan-details 80x24 + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + + + + + + + + + + + Enter Back +=== 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. + Same choice without questions: + python3 install.py \ + --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. + Same choice without questions: + python3 install.py \ + --platform retroarch \ + --dest .../retroarch/system + Shortened to fit this window. + + + + 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: + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system + + + + + + + + + + + + + + + Enter Back diff --git a/tests/fixtures/tui/route-miyoo.txt b/tests/fixtures/tui/route-miyoo.txt index ab456df22..9c0dc475a 100644 --- a/tests/fixtures/tui/route-miyoo.txt +++ b/tests/fixtures/tui/route-miyoo.txt @@ -436,10 +436,10 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. > Install Choose systems Copy from an old card or a folder + Details Options Quit A Install R2 Back L2 Options @@ -449,28 +449,26 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. - 1 in place, 3 missing, 1 wrong copy - Free space: 50.0 GB > Install Choose systems Copy from an old card or a folder + Details Options Quit + + A Install R2 Back L2 Options === plan 80x24 RetroBIOS: RetroArch 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -483,6 +481,8 @@ + + A Install R2 Back L2 Options === plan-in-place 40x12 RetroBIOS: RetroArch @@ -490,12 +490,12 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB + > Close + Details Options + + A Close R2 Back L2 Options === plan-in-place 40x15 RetroBIOS: RetroArch @@ -503,26 +503,24 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options + + + A Close R2 Back L2 Options === plan-in-place 80x24 RetroBIOS: RetroArch Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options @@ -537,6 +535,8 @@ + + A Close R2 Back L2 Options === systems 40x12 RetroBIOS: Choose systems @@ -808,39 +808,37 @@ RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + A Install R2 Back === confirm 40x15 RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + A Install R2 Back === confirm 80x24 RetroBIOS: Confirm - Counted again for these choices. Nothing is written before Install. + Counted again for these choices. 1 file to download, 16.0 KB BIOS folder: ~/.config/retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back @@ -857,6 +855,8 @@ + + A Install R2 Back === installing 40x12 RetroBIOS: Installing @@ -868,7 +868,7 @@ R2 stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + R2 Stop === installing 40x15 @@ -881,7 +881,7 @@ R2 stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -897,7 +897,7 @@ R2 stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -918,39 +918,38 @@ 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 - Details: /tmp/retrobios-x/install.log - A Exit + Log: /tmp/retrobios-x/install.log + + A Exit L2 Details === result 40x15 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 - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log - A Exit + + 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 - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log @@ -965,7 +964,8 @@ - A Exit + + A Exit L2 Details === error 40x12 RetroBIOS: Something went wrong @@ -1080,12 +1080,12 @@ D-pad, Up/Down Move A, Start, Enter Install L2, Tab, o Options + d Details R2, Esc, Backspace Back q, Ctrl-C Quit B Sends no key here - BIOS folder: - /home/user/.config/retroarch/system + Any key closes this help === help 40x15 RetroBIOS: Keys @@ -1093,6 +1093,7 @@ D-pad, Up/Down Move A, Start, Enter Install L2, Tab, o Options + d Details R2, Esc, Backspace Back q, Ctrl-C Quit B Sends no key here @@ -1101,7 +1102,6 @@ /home/user/.config/retroarch/system - Any key closes this help === help 80x24 RetroBIOS: Keys @@ -1109,6 +1109,7 @@ D-pad, Up/Down Move A, Start, Enter Install L2, Tab, o Options + d Details R2, Esc, Backspace Back q, Ctrl-C Quit B Sends no key here @@ -1126,7 +1127,6 @@ - Any key closes this help === plan-games 40x12 RetroBIOS: RetroArch @@ -1139,7 +1139,7 @@ For the games on this card: yes 412 Only what starts: no Show the games - 1-4 of 8 + 1-4 of 9 A Install R2 Back L2 Options === plan-games 40x15 RetroBIOS: RetroArch @@ -1147,13 +1147,13 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - For the 412 games on this card. > Install For the games on this card: yes 412 Only what starts: no Show the games Choose systems Copy from an old card or a folder + Details Options Quit A Install R2 Back L2 Options @@ -1163,11 +1163,6 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system For the 412 games on this card. - One region where the games prove it: nintendo-gba Europe. - 2 games get every BIOS of their system: Show the games. - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install For the games on this card: yes 412 games @@ -1175,12 +1170,17 @@ Show the games Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit + + + + A Install R2 Back L2 Options === games 80x24 RetroBIOS: Games on this card @@ -1206,4 +1206,83 @@ + A Back +=== plan-details 80x24 + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + + + + + + + + + + + A Back +=== result-details 40x12 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + A Back +=== result-details 40x15 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + + + + A Back +=== result-details 80x24 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + + + + + + + + + + + + A Back diff --git a/tests/fixtures/tui/route-muxterm.txt b/tests/fixtures/tui/route-muxterm.txt index 2f19f13e7..f118e37e6 100644 --- a/tests/fixtures/tui/route-muxterm.txt +++ b/tests/fixtures/tui/route-muxterm.txt @@ -436,10 +436,10 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. > Install Choose systems Copy from an old card or a folder + Details Options Quit Start Install B Back @@ -449,28 +449,26 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 wrong copy: backed up, replaced. - 1 in place, 3 missing, 1 wrong copy - Free space: 50.0 GB > Install Choose systems Copy from an old card or a folder + Details Options Quit + + Start Install B Back === plan 80x24 RetroBIOS: RetroArch 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit @@ -483,6 +481,8 @@ + + Start Install B Back === plan-in-place 40x12 RetroBIOS: RetroArch @@ -490,12 +490,12 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB + > Close + Details Options + + Start Close B Back === plan-in-place 40x15 RetroBIOS: RetroArch @@ -503,26 +503,24 @@ Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, - the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options + + + Start Close B Back === plan-in-place 80x24 RetroBIOS: RetroArch Every file on the list is in place BIOS folder: ~/.config/retroarch/system - 5 in place, 0 missing, 0 wrong copies - 1 of the 0 wrong copies is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Close + Details Options @@ -537,6 +535,8 @@ + + Start Close B Back === systems 40x12 RetroBIOS: Choose systems @@ -866,39 +866,37 @@ RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + Start Install B Back === confirm 40x15 RetroBIOS: Confirm Counted again for these choices. - Nothing is written before Install. 1 file to download, 16.0 KB BIOS folder: ~/.co.../retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back + + + Start Install B Back === confirm 80x24 RetroBIOS: Confirm - Counted again for these choices. Nothing is written before Install. + Counted again for these choices. 1 file to download, 16.0 KB BIOS folder: ~/.config/retroarch/system - 0 in place, 1 missing, 0 wrong copies - Free space: 50.0 GB > Install Back @@ -915,6 +913,8 @@ + + Start Install B Back === installing 40x12 RetroBIOS: Installing @@ -926,7 +926,7 @@ B stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + B Stop === installing 40x15 @@ -939,7 +939,7 @@ B stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -955,7 +955,7 @@ B stops. What is done stays done. FAILED scph5500.bin (HTTP 404) - ok gba_bios.bin + @@ -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 - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log + Start Exit === result 40x15 RetroBIOS: Finished with errors @@ -989,12 +989,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 - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log + @@ -1004,11 +1004,11 @@ 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 - Details: /tmp/retrobios-x/install.log + Log: /tmp/retrobios-x/install.log + @@ -1138,12 +1138,12 @@ D-pad, Up/Down Move Start, Enter Install o, Tab Options + d Details B, Esc, Backspace Back q, Ctrl-C Quit Select Keyboard on or off Menu Close the terminal - Any key closes this help === help 40x15 RetroBIOS: Keys @@ -1151,6 +1151,7 @@ D-pad, Up/Down Move Start, Enter Install o, Tab Options + d Details B, Esc, Backspace Back q, Ctrl-C Quit Select Keyboard on or off @@ -1159,7 +1160,6 @@ BIOS folder: /home/user/.config/retroarch/system - Any key closes this help === help 80x24 RetroBIOS: Keys @@ -1167,6 +1167,7 @@ D-pad, Up/Down Move Start, Enter Install o, Tab Options + d Details B, Esc, Backspace Back q, Ctrl-C Quit Select Keyboard on or off @@ -1184,7 +1185,6 @@ - Any key closes this help === plan-games 40x12 RetroBIOS: RetroArch @@ -1197,7 +1197,7 @@ For the games on this card: yes 412 Only what starts: no Show the games - 1-4 of 8 + 1-4 of 9 Start Install B Back === plan-games 40x15 RetroBIOS: RetroArch @@ -1205,13 +1205,13 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system - For the 412 games on this card. > Install For the games on this card: yes 412 Only what starts: no Show the games Choose systems Copy from an old card or a folder + Details Options Quit Start Install B Back @@ -1221,11 +1221,6 @@ 4 files to download, 1.0 MB BIOS folder: ~/.config/retroarch/system For the 412 games on this card. - One region where the games prove it: nintendo-gba Europe. - 2 games get every BIOS of their system: Show the games. - 1 in place, 3 missing, 1 wrong copy - The 1 wrong copy is replaced, the old one kept in a backup folder. - Free space: 50.0 GB > Install For the games on this card: yes 412 games @@ -1233,12 +1228,17 @@ Show the games Choose systems Copy the BIOS files from an old card or a folder + Details Options Quit + + + + Start Install B Back === games 40x12 RetroBIOS: Games on this card @@ -1293,4 +1293,112 @@ + Start Back +=== plan-details 40x12 + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old + one kept in a backup folder. + Free space: 50.0 GB + + + + + + Start Back +=== plan-details 40x15 + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old + one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + Start Back +=== plan-details 80x24 + RetroBIOS: Details + + 1 in place, 3 missing, 1 wrong copy + The 1 wrong copy is replaced, the old one kept in a backup folder. + Free space: 50.0 GB + + + + + + + + + + + + + + + + + + + Start Back +=== result-details 40x12 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + Start Back +=== result-details 40x15 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + + + + Start Back +=== result-details 80x24 + RetroBIOS: Details + + failed: scph5500.bin (HTTP 404) + + + + + + + + + + + + + + + + + + + + Start Back diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index bcfda2ef0..d8a558358 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -187,7 +187,7 @@ class InstallerSamplesSayWhatTheCodePrints(unittest.TestCase): page = _page("installer.md") refused = _generated(page, "check-refused") - self.assertRegex(refused, r"\n \d+ files? [a-z ]+:\n") + self.assertRegex(refused, r"\n \d+ files? [a-z ]+\.\n") self.assertIn("read by beetle_psx, pcsx_rearmed at", refused) self.assertNotRegex(page, r"opened at /\S+ by ") self.assertIn('"read by ', inspect.getsource(install.opened_line)) diff --git a/tests/test_install.py b/tests/test_install.py index eba661cfa..41e7aab15 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -5668,7 +5668,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun): proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest), "--check", "--no-input", "--verbose") self.assertEqual(proc.returncode, 3, proc.stderr) - listing = proc.stdout.split("to download:\n", 1)[1].splitlines() + listing = proc.stdout.split("to download.\n", 1)[1].splitlines() self.assertEqual(listing[0].strip(), "wrong copy: scph5501.bin") def test_a_dry_run_counts_the_data_trees_and_ends_on_its_last_line(self): @@ -5716,7 +5716,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun): "--manifest", str(REPO_ROOT / "install" / "retroarch.json")) self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) missing = re.search(r"0 in place, (\d+) missing", proc.stdout).group(1) - self.assertIn(f"\n {missing} files not in place:\n", proc.stdout) + self.assertIn(f"\n {missing} files not in place.\n", proc.stdout) self.assertIn(f" {missing} files need the internet", proc.stdout) def test_a_cores_folder_can_be_named(self): diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index 785e379f8..b0aaa73ce 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -83,6 +83,11 @@ def keys(*names: str) -> list: return [Key("char", name[5:]) if name.startswith("char:") else Key(name) for name in names] +# The screens as --verbose draws them, every Details line where it belongs: +# the findings marked with it read lines the default leaves to Details. +in_full = mock.patch.object(install, "tui_verbose", lambda _state: True) + + def _args(**given) -> argparse.Namespace: """The namespace main() parses, every option at its default.""" values = dict( @@ -1002,19 +1007,7 @@ def golden_states() -> dict: "bytes_total": 1064964} installing.finished = [("gba_bios.bin", True, ""), ("scph5500.bin", False, "HTTP 404")] states["installing"] = installing - result = checked_state() - result.stack.append("result") - result.code = 1 - result.log = "/tmp/retrobios-x/install.log" - result.download = installing.download - result.summary = { - "event": "summary", "mode": "install", "installed": 3, "up_to_date": 1, "pending": 1, - "failed": [{"dest": "scph5500.bin", "reason": "HTTP 404"}], - "skipped": [{"reason": "not_found", "label": "not found", "files": 2, "required": 1}], - "left_out": [], "kept": [{"folder": str(HOME / ".config/retroarch/retrobios-backup/" - "2026-10-23_1200"), "files": 1, "bytes": 4}], - "interrupted": False} - states["result"] = result + states["result"] = golden_states_result() error = fresh_state() error.stack, error.run = ["setup", "error"], "check" error.error = [" Could not fetch the file list for retroarch after 3 attempts: " @@ -1032,9 +1025,33 @@ def golden_states() -> dict: listed = games_state() listed.stack.append("games") states["games"] = listed + # What the plan and the result leave out by default, one press away. + details = checked_state() + details.stack.append("details") + states["plan-details"] = details + told = golden_states_result() + told.stack.append("details") + states["result-details"] = told return states +def golden_states_result() -> install.TuiState: + """The result golden_states draws, a file failed and one kept.""" + result = checked_state() + result.stack.append("result") + result.code = 1 + result.log = "/tmp/retrobios-x/install.log" + result.download = {"event": "download", "files": 4, "bytes": 1064964} + result.summary = { + "event": "summary", "mode": "install", "installed": 3, "up_to_date": 1, "pending": 1, + "failed": [{"dest": "scph5500.bin", "reason": "HTTP 404"}], + "skipped": [{"reason": "not_found", "label": "not found", "files": 2, "required": 1}], + "left_out": [], "kept": [{"folder": str(HOME / ".config/retroarch/retrobios-backup/" + "2026-10-23_1200"), "files": 1, "bytes": 4}], + "interrupted": False} + return result + + def result_states() -> dict: """The result of a run that installed everything, of one with a file failed, of one --offline left files out of, and of one stopped.""" @@ -1159,8 +1176,11 @@ class Views(unittest.TestCase): def test_each_role_paints_a_status_word_on_the_result_screens(self): """Done, a part done, files left offline, a stop: between them the result screens use every role, each on a word that says it.""" + # The failed files are listed on the result's Details. + told = result_states()["partial"] + told.stack.append("details") drawn = "\n".join(install.tui_paint(str(line), line.style, True) - for state in result_states().values() + for state in [*result_states().values(), told] for line in install.tui_view(state, 80, 24)) for role, word in (("accent", "RetroBIOS"), ("strong+success", "4 files installed"), ("strong+success", "Done"), ("failure", "1 failed"), @@ -1573,6 +1593,7 @@ class SmallFixes(unittest.TestCase): self.assertTrue(state.installed_only) self.assertNotIn("--installed-cores", [f for f, _v in install.tui_flags(state)]) + @in_full def test_the_end_of_a_wrapped_pager_is_reachable(self): state = checked_state() state.stack.append("result") @@ -2081,7 +2102,7 @@ class WorkFolders(unittest.TestCase): def test_a_failure_keeps_the_log_it_names(self): work, out = self.run_to(1) self.assertTrue(work.exists()) - self.assertIn(f"Details: {work / 'install.log'}", out) + self.assertIn(f"Log: {work / 'install.log'}", out) def test_a_stop_leaves_nothing(self): work, out = self.run_to(130) @@ -2238,6 +2259,7 @@ class CopyFromAnOldCard(unittest.TestCase): self.assertEqual(state.from_folders, [Path("/media/old")]) self.assertIn(("--from", "/media/old"), install.tui_flags(state)) + @in_full def test_files_in_the_wrong_place_are_said_and_listed(self): state = checked_state() state.platform, state.dest = "retroarch", SYSTEM_DIR @@ -2778,6 +2800,7 @@ class CardsSpaceAndConfirm(unittest.TestCase): if row.action == "game_data") self.assertEqual(label, "Game data: left out") + @in_full def test_the_plan_says_which_config_named_the_folder(self): config = HOME / ".config/retroarch/retroarch.cfg" with mock.patch.dict(install._RETROARCH_CONFIGS, {SYSTEM_DIR: (config, config.parent)}): @@ -2803,6 +2826,7 @@ class CardsSpaceAndConfirm(unittest.TestCase): # 1 MB from the network in 2 s, whatever came from the old card. self.assertIn("512.0 KB/s, 0:02 left", text) + @in_full def test_the_plan_says_what_the_scan_left_out(self): state = checked_state() state.from_folders = [Path("E:\\")] @@ -3009,6 +3033,7 @@ class SaveDirectoryLeftOut(unittest.TestCase): install.tui_receive(state, ("ended", state.run_id, 1, [])) return state, summary + @in_full def test_a_list_that_holds_none_of_them_offers_no_folder(self): """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.""" @@ -3024,6 +3049,7 @@ class SaveDirectoryLeftOut(unittest.TestCase): self.assertEqual([text for text in install.tui_outcome(state)[1] if "save" in text], [line]) + @in_full def test_files_left_out_of_the_save_directory_are_said(self): state = fresh_state() state.stack = ["setup"] @@ -3673,6 +3699,7 @@ class DataFilesHaveASize(unittest.TestCase): """"8179 files to download, 6.6 GB, and 2636 data files", the data files with no size.""" + @in_full def test_the_plan_gives_the_data_files_size(self): state = fresh_state() state.stack = ["setup"] @@ -3720,6 +3747,7 @@ class ThePagerCountsLines(unittest.TestCase): self.assertTrue(any(re.fullmatch(rf" \d+-{len(wrapped)} of {len(wrapped)}", line) for line in body), body) + @in_full def test_the_counter_moves_one_line_per_press(self): state = self.result_state(40, 12) first = [line for line in install.tui_view(state, 40, 12) if " of " in line][-1] @@ -3862,6 +3890,7 @@ class BackTicksAndSaveFolder(unittest.TestCase): state = install.TuiState() self.assertLess(abs(state.now - time.monotonic()), 5) + @in_full def test_a_pack_installs_from_the_pack(self): state = checked_state() state.found = [{"event": "found", "platform": "retroarch", "dest": FILES[0]["dest"], @@ -4152,6 +4181,7 @@ class TheSaveFolderLineFollowsTheChoice(unittest.TestCase): self.assertEqual(state.screen, "result") self.assertNotIn("save directory", self.said(install.tui_outcome(state)[1])) + @in_full def test_kept_the_confirm_screen_and_the_result_name_it(self): state = self.run_to_result(confirm_saves=True, install_saves=True) self.assertIn(self.LINE, self.confirm) @@ -4882,6 +4912,11 @@ class EndToEnd(unittest.TestCase): screen = term.wait("RetroBIOS is on the KNULLI card") self.assertIn("start RetroBIOS from Ports", screen) self.assertNotIn(install.APP_OFFER, screen) + # The command, a Details line, is said after Exit once they were opened. + term.send("d") + term.wait("RetroBIOS: Details") + term.send("\x1b") + term.wait("RetroBIOS is on the KNULLI card") term.send("\r") self.assertEqual(term.close(), 0) self.assertAppOnCard(root, "KNULLI") @@ -4904,7 +4939,9 @@ class EndToEnd(unittest.TestCase): after = term.after() self.assertIn("RetroBIOS: Done", after) self.assertIn("5 files installed.", after) - self.assertIn("--platform retroarch --dest", after) + # The command is a Details line: said after Exit once they were opened. + self.assertIn("Next: start RetroArch and play.", after) + self.assertNotIn("--platform retroarch --dest", after) # The cursor and the main screen come back before the result is printed. self.assertIn(b"\x1b[?25h\x1b[?1049l", term.raw) @@ -4943,15 +4980,24 @@ class EndToEnd(unittest.TestCase): term.send("\r") screen = term.wait("files to download") self.assertIn("6 files to download", screen) - # The second folder the run writes to, named before - # anything is written, and again on the result. + # The second folder the run writes to, named on the + # plan's Details before anything is written, and on the + # result's. + term.send("d") + screen = term.wait("RetroBIOS: Details") self.assertIn("1 file read from RetroArch's save directory goes to a " "folder per core under:\n ~/MySaves", screen) + term.send("\x1b") + term.wait("files to download") term.send("\r") - screen = term.wait("6 files installed.") + term.wait("6 files installed.") + term.send("d") + screen = term.wait("RetroBIOS: Details") # Spelled in full on the result, as its other folders. joined = "".join(line.strip() for line in screen.splitlines()) self.assertIn("folder per core under:" + str(saves), joined) + term.send("\x1b") + term.wait("6 files installed.") term.send("\r") self.assertEqual(term.close(), 0) results.append(written(repo, saves)) @@ -4978,6 +5024,10 @@ class EndToEnd(unittest.TestCase): self.assertIn("3 files to download", screen) term.send("\r") term.wait("3 files installed.") + term.send("d") + term.wait("RetroBIOS: Details") + term.send("\x1b") + term.wait("3 files installed.") term.send("\r") self.assertEqual(term.close(), 0) self.assertEqual(repo.installed(), ["pcsx/extra.bin", "scph5500.bin", "scph5501.bin"]) @@ -4999,9 +5049,13 @@ class EndToEnd(unittest.TestCase): screen = term.wait("4 files to download") self.assertIn("> Install", screen) term.send("\r") - screen = term.wait("installed.") + term.wait("installed.") + term.send("o") + screen = term.wait("RetroBIOS: Details") self.assertIn("1 game data file left out.", screen) term.send("\r") + term.wait("installed.") + term.send("\r") self.assertEqual(term.close(), 0) self.assertEqual(repo.installed(), ["gba_bios.bin", "mgba/extra.bin", "pcsx/extra.bin", "scph5501.bin"]) @@ -5090,6 +5144,10 @@ class EndToEnd(unittest.TestCase): self.assertIn("5 files installed.", screen) self.assertIn("3 files copied from", screen) self.assertIn("2 downloaded.", screen) + term.send("o") + term.wait("RetroBIOS: Details") + term.send("\r") + term.wait("files installed.") term.send("\r") self.assertEqual(term.close(), 0) self.assertEqual(len(repo.installed()), 5) @@ -5808,6 +5866,7 @@ class ThePlanNumbersAddUp(unittest.TestCase): """"47 files to download" sat above "49 missing, 1 wrong copy": the 3 found on this machine were the difference, left to the reader.""" + @in_full def test_the_copied_files_are_the_difference(self): state = checked_state() state.found = [found("scph5500.bin", str(SYSTEM_DIR), "one folder too deep")] @@ -6441,6 +6500,7 @@ class ConfirmRefusalsAndCardRows(unittest.TestCase): if row.action == "platform"} self.assertEqual(labels["knulli"], "KNULLI") + @in_full def test_offline_plan_counts_agree(self): """"3 downloaded" under "3 need the internet"; the data files read as if on hand.""" @@ -6871,6 +6931,7 @@ class WhatEachScreenOffers(unittest.TestCase): rows = install.tui_rows(state) self.assertEqual(rows[state.focus["browse"]].value, stick) + @in_full def test_offline_plan_names_the_folder_already_copied_from(self): state = checked_state() state.from_folders = [self.root / "from1"] @@ -6979,6 +7040,7 @@ class WhatEachScreenOffers(unittest.TestCase): "not known. Name it with --save-dir next time.", text) self.assertNotIn("the collection does not hold", text) + @in_full def test_plural_game_data_and_the_backup_line_at_40x12(self): state = checked_state() games = [{"dest": f"g{index}.dat", "system": "", "category": install.GAME_DATA} @@ -7759,6 +7821,7 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings, self.assertIn("1 game data file left out (--no-game-data)", after) self.assertNotIn("doom1.wad would", after) + @in_full def test_the_plan_counts_game_data_apart_and_one_press_leaves_it_out(self): state = checked_state() manifest = json.loads(json.dumps(state.manifest)) @@ -7890,6 +7953,7 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings, if split: self.assertEqual(int(split[1]) + int(split[2]), int(head[1])) + @in_full def test_the_screen_divides_the_count_it_shows(self): state = checked_state() manifest = json.loads(json.dumps(state.manifest)) @@ -9017,6 +9081,7 @@ class PacksWordingAndDocs(unittest.TestCase): install.LOCAL.pack = mock.Mock(name="pack") self.assertIn("to install from the pack", install._pager_items(state)[0]) + @in_full def test_scroll_is_named_only_where_the_text_scrolls(self): """"Up/Down Scroll" over nine lines that fit.""" state = checked_state() @@ -10082,6 +10147,7 @@ class CardNamesAndHelp(_CardFixtures, unittest.TestCase): self.assertNotIn("A/Enter", bar) self.assertNotIn("B/Esc", bar) + @in_full def test_the_folder_its_config_named_is_said_under_it(self): state = checked_state() long_dest = Path("/home/user/" + "a-long-folder-name/" * 5 + "system") @@ -10227,6 +10293,7 @@ class TheAndroidResultReadsWhole(unittest.TestCase): state.cols, state.rows = cols, 40 return install.tui_view(state, cols, 40) + @in_full def test_a_path_or_a_command_is_never_split_inside_a_word(self): full = "/sdcard/RetroArch/retrobios-backup/20261027-223524" for cols in (40, 50, 80): @@ -10948,6 +11015,7 @@ class TapsGameDataAndExitCodes(_CardFixtures, unittest.TestCase): plain.help = True self.assertNotIn("Tap", " ".join(install.tui_view(plain, 80, 24))) + @in_full def test_the_result_is_left_by_a_tap(self): """The result gave the mouse back, and on Termux leaving it took the keyboard. A tap on its Exit closes it, and its @@ -10964,6 +11032,7 @@ class TapsGameDataAndExitCodes(_CardFixtures, unittest.TestCase): # Root cause 2: one narrowed list drives every count. + @in_full def test_the_game_data_row_and_the_save_line_follow_the_choice(self): """With only the GBA ticked, the plan said 1 game data file and a save file of the PlayStation.""" @@ -11001,6 +11070,7 @@ class TapsGameDataAndExitCodes(_CardFixtures, unittest.TestCase): word = "left out" if left else "kept" self.assertRegex(line, rf"^ Game data: {word} +51(, | files, )2\.4 GB$") + @in_full def test_data_files_are_named_apart(self): """Data files beside BIOS files and game data, the parts adding up to the count above, each kind said once.""" @@ -11013,6 +11083,7 @@ class TapsGameDataAndExitCodes(_CardFixtures, unittest.TestCase): "other cores. Data files: the files an emulator reads from a data " "folder of its own, such as Dolphin's Sys and PPSSPP's assets.", text) + @in_full def test_confirm_counts_the_kinds_apart(self): """Extra criteria: the plan split BIOS files and game data, Confirm did not.""" @@ -11292,6 +11363,7 @@ class TapsGameDataAndExitCodes(_CardFixtures, unittest.TestCase): rows = install.tui_rows(state) self.assertIn(sides[0][1], [row.value for row in rows if row.action == "drive"]) + @in_full def test_named_by_sits_under_its_folder(self): """The offline remedy and the narrowing came between the folder and what named it.""" @@ -11421,7 +11493,7 @@ class TapsOnAPty(unittest.TestCase): bar = term.screen.display[-1] self.click(term, bar.index("Exit"), len(term.screen.display) - 1) self.assertEqual(term.close(), 0) - self.assertIn("Same choice without questions:", term.after()) + self.assertIn("Next: start RetroArch and play.", term.after()) self.assertGreater(term.raw.rfind(b"\x1b[?1000l"), mark) @@ -11742,6 +11814,7 @@ class OneChoicePerRun(unittest.TestCase): state.exit_code, state.quit_asked = install.QUIT_CODE, True self.assertIn(note, install.tui_farewell(state)) + @in_full def test_the_help_defines_game_data_at_40_columns(self): """The row was a choice nothing explained on the Miyoo's screen.""" @@ -11807,6 +11880,7 @@ class OneChoicePerRun(unittest.TestCase): whole="command") self.assertIn(f" --from-pack {path}", lines) + @in_full def test_the_definition_is_said_once_and_never_spliced(self): state = checked_state() state.systems = [] @@ -11819,6 +11893,7 @@ class OneChoicePerRun(unittest.TestCase): self.assertIn("Not available here: jazz2/Source/Anims.j2a (Jazz Jackrabbit 2), game " "data its game reads.", text) + @in_full def test_the_headline_counts_every_file_once(self): """"8358 files and 2636 data files", and the definition dropped at 80 columns.""" @@ -11830,6 +11905,7 @@ class OneChoicePerRun(unittest.TestCase): self.assertNotIn("and 7 data files", lines[2]) self.assertIn("Data files: the files an emulator reads", text) + @in_full def test_the_save_line_never_ends_on_its_colon(self): """"...goes to a folder per core under:" with no folder at 60x20.""" @@ -11927,6 +12003,7 @@ class OneChoicePerRun(unittest.TestCase): said = " ".join(install._needed_lines(entries, "Needed")) self.assertIn("... and 1 more system (--check --verbose lists them all)", said) + @in_full def test_the_farewell_names_the_option_not_the_screen(self): state = _installed_state(SYSTEM_DIR) state.manifest = {**state.manifest, "save_directory": {"folder": "saves", @@ -12674,6 +12751,7 @@ class ChoiceStateInBothModes(unittest.TestCase): _title, lines = install.tui_outcome(state) self.assertIn("7 data files not installed: not in RetroArch_BIOS_Pack.zip.", lines) + @in_full def test_a_command_at_40_columns_breaks_between_shell_words(self): """The terminal folded commands inside a word, "--platform" ended a line with its value on the next.""" @@ -12707,6 +12785,7 @@ class ChoiceStateInBothModes(unittest.TestCase): farewell = "\n".join(install.tui_farewell(state)) self.assertIn("/home/moi/retroarch_system/tmp/tui_audit14/sb/a/b/bios", farewell) + @in_full def test_a_folder_on_its_own_line_has_one_indent(self): """The save folder at the label's indent, the BIOS folder under it, a path's last piece without its indent.""" @@ -13341,6 +13420,7 @@ class OneVocabularyOneCount(unittest.TestCase): self.assertIn("1 in place, 3 missing, 0 wrong copies", text) self.assertIn("Nothing installed: 3 files need the internet", text) + @in_full def test_a_pack_holding_none_offers_no_install(self): """A data tree whose folder is the BIOS folder itself (blueMSX) made any pack hold a data file: Install focused, @@ -13533,6 +13613,7 @@ class TheFromPlanSaysItsKinds(unittest.TestCase): """Every file found in the folder, the plan named the Game data row and said neither the kinds nor what game data is.""" + @in_full def test_the_kinds_and_the_definition_stay(self): audit = OneChoicePerRun() state = audit.game_plan() @@ -14542,6 +14623,7 @@ class CoresListAndFolderFailures(unittest.TestCase): # Root cause 6: every other item. + @in_full def test_offline_the_plan_promises_no_replacement_it_cannot_make(self): """Offline, the plan said a wrong copy no folder holds "is replaced".""" @@ -14668,6 +14750,7 @@ class GamesOnTheCard(unittest.TestCase): """For the games on this card: the first choice where the card holds games, the whole list one press away (--for-games).""" + @in_full def test_a_card_with_games_opens_on_them(self): state = games_state() self.assertTrue(state.for_games) @@ -14845,5 +14928,149 @@ class GamesEndToEnd(unittest.TestCase): written = sorted(p.name for p in card.bios.rglob("*") if p.is_file()) self.assertEqual(written, ["bios_CD_E.bin", "cd_extra.bin"]) +# The most words each screen may show by default, one count per size of +# SIZES (owner, 2026-11-09: a beginner lost the important line in text). A +# 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. +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), + "create": (23, 23, 21, 21), "checking": (18, 18, 17, 17), "plan": (31, 31, 31, 31), + "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), + "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), +} + + +def screen_words(lines: list) -> int: + """Words of a drawn screen, its title and bar included, a path left out.""" + return sum(1 for line in lines for token in str(line).split() + if re.search(r"[A-Za-z0-9]", token) and "/" not in token) + + +class ScreenWordBudgets(unittest.TestCase): + def setUp(self): + patcher = mock.patch.object(install, "_mounted_cards", return_value=[]) + patcher.start() + self.addCleanup(patcher.stop) + + def test_every_screen_keeps_to_its_budget(self): + states = golden_states() + self.assertEqual(sorted(states), sorted(SCREEN_WORDS), + "a screen without a word budget, or a budget without its screen") + for name, state in states.items(): + for (cols, rows), budget in zip(SIZES, SCREEN_WORDS[name]): + with self.subTest(screen=name, size=f"{cols}x{rows}"): + state.cols, state.rows = cols, rows + words = screen_words(install.tui_view(state, cols, rows)) + self.assertLessEqual(words, budget) + + def test_the_default_path_says_what_where_and_next_first(self): + """The plan opens on what it installs and where, the result on what + happened, where and the next step: nothing between them.""" + plan = [str(line).strip() for line in install.tui_view(checked_state(), 80, 24)] + self.assertEqual(plan[2:4], ["4 files to download, 1.0 MB", + "BIOS folder: ~/.config/retroarch/system"]) + self.assertEqual(plan[4], "") + done = result_states()["done"] + lines = [str(line).strip() for line in install.tui_view(done, 80, 24)] + self.assertEqual(lines[2:5], ["4 files installed. 1 file already in place.", + f"BIOS folder: {SYSTEM_DIR}", + "Next: start RetroArch and play."]) + + +class DetailsScreen(unittest.TestCase): + """What the plan, Confirm and the result leave out by default, one key + or one row away, and every line of --verbose in one place or the other.""" + + def setUp(self): + patcher = mock.patch.object(install, "_mounted_cards", return_value=[]) + patcher.start() + self.addCleanup(patcher.stop) + + def text(self, state: install.TuiState, cols: int = 80, rows: int = 24) -> str: + return "\n".join(str(line) for line in install.tui_view(state, cols, rows)) + + def test_d_opens_the_plans_details_and_b_comes_back(self): + state = checked_state() + self.assertNotIn("Free space", self.text(state)) + press(state, "d") + self.assertEqual(state.screen, "details") + shown = self.text(state) + for line in ("1 in place, 3 missing, 1 wrong copy", "Free space: 50.0 GB", + "The 1 wrong copy is replaced"): + self.assertIn(line, shown) + press(state, "esc") + self.assertEqual(state.screen, "plan") + + def test_a_controller_reaches_them_by_the_row(self): + state = checked_state() + rows = install.tui_rows(state) + state.focus["plan"] = [row.action for row in rows].index("details") + self.assertIn("A/Enter Show", self.text(state)) + press(state, "enter") + self.assertEqual(state.screen, "details") + press(state, "enter") + self.assertEqual(state.screen, "plan") + + def test_the_results_details_are_said_after_exit_once_opened(self): + state = golden_states_result() + self.assertIn("Y/o Details", self.text(state)) + self.assertNotIn("Same choice without questions", "\n".join(install.tui_farewell(state))) + press(state, "o") + self.assertEqual(state.screen, "details") + self.assertIn("Same choice without questions:", self.text(state)) + press(state, "esc") + self.assertEqual(state.screen, "result") + press(state, "enter") + farewell = "\n".join(install.tui_farewell(state)) + self.assertIn("Same choice without questions:", farewell) + self.assertIn("failed: scph5500.bin (HTTP 404)", farewell) + + def test_confirm_has_its_details_too(self): + state = golden_states()["confirm"] + self.assertNotIn("Free space", self.text(state)) + press(state, "d") + self.assertEqual(state.screen, "details") + self.assertIn("Nothing is written before Install.", self.text(state)) + self.assertIn("Free space: 50.0 GB", self.text(state)) + + def test_every_line_is_on_the_screen_or_its_details(self): + """The screen and its Details together say what --verbose says on + the screen alone: no line is lost.""" + for name in ("plan", "plan-games", "plan-in-place", "confirm", "result"): + with self.subTest(screen=name): + state = golden_states()[name] + whole = (install._plan_text(state) if state.screen == "plan" + else install._confirm_text(state) if state.screen == "confirm" + else install.tui_outcome(state)[1]) + shown = install._shown(state, whole) + state.stack.append("details") + told = install.details_lines(state) + self.assertEqual(sorted(map(str, whole)), + sorted(map(str, [*shown, *[line for line in told + if install._is_detail(line)]]))) + + def test_verbose_draws_them_in_place_and_offers_no_row(self): + state = checked_state() + state.passthrough = [("--verbose", None)] + text = self.text(state) + self.assertIn("Free space: 50.0 GB", text) + self.assertNotIn("details", [row.action for row in install.tui_rows(state)]) + + def test_installing_lists_the_files_that_went_well_with_verbose(self): + state = golden_states()["installing"] + self.assertNotIn("gba_bios.bin", self.text(state)) + self.assertIn("FAILED scph5500.bin (HTTP 404)", self.text(state)) + state.passthrough = [("--verbose", None)] + self.assertIn("ok gba_bios.bin", self.text(state)) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_no_game_data.py b/tests/test_no_game_data.py index 2bb59c2fc..42a9d571f 100644 --- a/tests/test_no_game_data.py +++ b/tests/test_no_game_data.py @@ -287,7 +287,7 @@ class InstallerLeavesGameDataOut(unittest.TestCase): def test_install_leaves_it_out_and_names_it(self): self.serve() - proc = self.run_installer("--no-game-data") + proc = self.run_installer("--no-game-data", "--verbose") self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertTrue((self.bios / "a.bin").exists()) self.assertFalse((self.bios / "game.wad").exists()) @@ -297,10 +297,10 @@ class InstallerLeavesGameDataOut(unittest.TestCase): def test_check_and_dry_run_leave_it_out(self): self.serve() - for mode, listed in (("--check", "1 file to download:"), + for mode, listed in (("--check", "1 file to download."), ("--dry-run", "Would download 1 file")): with self.subTest(mode=mode): - proc = self.run_installer("--no-game-data", mode) + proc = self.run_installer("--no-game-data", mode, "--verbose") self.assertIn( "2 game data files left out (--no-game-data), 1 of them not installable: " "game.wad, level.dat", diff --git a/wiki/installer.md b/wiki/installer.md index 5ce100f68..55c2899a9 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -171,8 +171,9 @@ Enter checks the BIOS folder when nothing can be copied. Run at a terminal, the installer opens a full screen that a keyboard or a controller drives. Three presses of A or Enter install everything the setup reads: the setup found here, the plan, then the result. The plan screen says -how many files are missing, how much they weigh, how many are in place and how -much space is free, before anything is written. +what it installs, its size and where, before anything is written. **Details** +(`d`) adds the rest: how many files are in place, the free space, the files +left out and why. The other choices sit one press away. **Choose systems** ticks the consoles to keep, with the total to download under the list. It opens on its first system, @@ -207,6 +208,7 @@ install starts. | Tick, untick | Space, Right, Left | X, D-pad Right and Left (X alone inside muxterm, Y with muxterm's own keys) | | Back | Esc, Backspace | B, Select (B alone with muxterm's own keys, where Select shows its keyboard) | | Options | `o`, Tab | Y | +| Details | `d`, and `o` or Tab on the result | the Details row, Y on the result | | Filter a long list | `/` then type, Enter on a lone match chooses it | muxterm: Select shows a keyboard. Elsewhere none (the setup list puts the SD cards after RetroArch) | | Keys of the screen | `?`, F1 (F1 alone while typing a filter, where the help lists the keys that do not type) | | | Stop an install | Esc, `q`, Ctrl+C | B, Select+Start (B alone with muxterm's own keys) | @@ -229,11 +231,12 @@ regions screen, Left and Right move the region under the cursor up and down the order of preference. During the download, a stop waits for the files in progress, and a second one stops at once. Select+Start on a handheld ends the installer with a signal (SIGTERM), which stops it the same way. Either way the -result says how many files were installed, and it stays printed in the terminal -once the screen closes, with the command that repeats the run. A run that failed -keeps its log and names it, and every other run leaves nothing behind. A path -on the result is never split inside a word: one longer than the window takes -a line of its own, which the terminal folds. A command breaks only between its +result says how many files were installed and the next step, and it stays +printed in the terminal once the screen closes. Its **Details** hold the +command that repeats the run, printed in the terminal too once they were +opened. A run that failed keeps its log and names it, and every other run +leaves nothing behind. A path on the result is never split inside a word: one +longer than the window takes a line of its own, which the terminal folds. A command breaks only between its arguments, an option kept with its value, each line but the last ending with the continuation its shell reads (a backslash in sh, a backtick in PowerShell), so the lines copied run as one command. Where an option and its path do not fit @@ -315,8 +318,9 @@ them back on every way out, a stop with Ctrl+Z included. On the command line screen, any screen showing a command and the screen where a path is typed, the terminal keeps the mouse, so text is selected, copied and pasted as usual. The result takes taps, so a touch screen can leave it: it is printed again in -the terminal once the screen closes, its commands with it, and its last line -says so. Elsewhere, and on the result, hold Shift while selecting. `--plain` +the terminal once the screen closes, with its commands when its Details were +opened, and the last line of the Details says so. Elsewhere, and on the +result, hold Shift while selecting. `--plain` never asks for clicks. Clicks and taps were checked on 2026-10-27 in xterm, tmux, GNU screen 4.09, Windows Terminal (WSL) and Termux on Android 17. macOS Terminal and iTerm2 are not checked yet, nor the key @@ -618,7 +622,7 @@ against a list of four files: ``` - 3 files to download: + 3 files to download. 2 of these files are in the BIOS folder in the wrong place: a run without --check copies them where the cores open them, and leaves the copies found as they are. wrong copy: scph5501.bin another region: the contents of scph5500.bin (japan) From d57120ec0ee2762ff9da696667c800c33a48756d Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 01:12:39 +0100 Subject: [PATCH 3/4] chore: give each journey a word budget The full-screen journeys sum the words of every screen they stop on, and one screen of them may show 90 at most. The same journeys in line mode keep to their lines and words, print every default line again with --verbose, and run to their end with no terminal and no --yes. A path is no word: its length is the machine's. --- tests/test_install_journeys.py | 151 +++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/tests/test_install_journeys.py b/tests/test_install_journeys.py index f73cd6ce6..653784f64 100644 --- a/tests/test_install_journeys.py +++ b/tests/test_install_journeys.py @@ -19,6 +19,7 @@ import hashlib import http.server import json import os +import re import shutil import subprocess import sys @@ -51,6 +52,46 @@ JOURNEY_KEYS = { "another_device": (19, 20), } +# Words each journey's screens show by default, summed over the screens it +# stops on, and the most one screen of the default path may show (owner, +# 2026-11-09: a beginner lost the important line in text). A path is no +# word: its length is the machine's. The counts from before the Details +# screen took the plan's and the result's notes are in claudedocs/ +# user_research/installer_ux/phase1_measure.md. A change that adds words +# raises a number here and says why. +JOURNEY_WORDS = { + "first_install": 63, + "card_with_games": 106, + "update": 79, + "one_system": 193, + "one_system_again": 68, + "what_is_missing": 223, + "another_device": 230, +} +# Which setup? names every setup, Preview every file: lists, not prose. +SCREEN_CEILING = 90 +# The same journeys in line mode, run without questions: (lines, words) of +# what happens, where, the result and the next step. +LINE_TEXT = { + "first_install": (4, 18), + "card_with_games": (4, 29), + "update": (5, 36), + "one_system": (4, 18), + "what_is_missing": (3, 15), + "another_device": (4, 18), +} + + +def words(text: str) -> int: + """Words a reader reads, a path left out.""" + return sum(1 for token in text.split() if re.search(r"[A-Za-z0-9]", token) + and "/" not in token) + + +def lines(text: str) -> int: + return sum(1 for line in text.splitlines() if line.strip()) + + # Runs install.py with the mounted drives a reader would show, none or the # card named: the drives of the machine running the tests (WSL's /mnt/d) # changed the screens. Only the screen's own process looks for cards: the @@ -139,6 +180,17 @@ class Journeys(unittest.TestCase): term = Pty(list(args), {**repo.env, **(env or {})}, 80, 24, program=program) self.addCleanup(lambda: term.status is None and os.kill(term.pid, 9)) self.keys = 0 + self.screens: list[str] = [] + waiting = term.wait + + def wait(needle: str, timeout: float = 20.0) -> str: + # Each screen the journey stops on, read once drawn whole. + waiting(needle, timeout) + screen = term.settle(0.3) + self.screens.append(screen) + return screen + + term.wait = wait # type: ignore[method-assign] return term def press(self, term: Pty, *keys: str) -> None: @@ -156,6 +208,11 @@ class Journeys(unittest.TestCase): self.assertEqual((started, total), JOURNEY_KEYS[journey], f"{journey} takes {(started, total)} keys now: longer is a step " "back, shorter lowers JOURNEY_KEYS") + said = [words(screen) for screen in self.screens] + self.assertLessEqual(sum(said), JOURNEY_WORDS[journey], + f"{journey} shows {said} words: more is a step back") + self.assertLessEqual(max(said), SCREEN_CEILING, said) + self.screens = [] def finish(self, term: Pty) -> int: term.wait("A/Enter Exit", timeout=60) @@ -302,5 +359,99 @@ class Journeys(unittest.TestCase): sorted(e["dest"] for e in repo.listed["files"])) +@unittest.skipUnless(pty, "the journeys need a pty") +class LineModeJourneys(unittest.TestCase): + """The journeys in line mode without questions: what happens, where, the + result and the next step by default, every hidden line given back by + --verbose, and nothing waiting for an answer.""" + + def run_line(self, repo: Repository, *args: str, card: Path | None = None + ) -> subprocess.CompletedProcess: + program = [sys.executable, "-c", + _WITH_CARDS.format(script=str(REPO_ROOT / "install.py"))] + env = {**repo.env, **({"JOURNEY_CARD": str(card)} if card else {})} + return subprocess.run([*program, *args], env=env, capture_output=True, text=True, + timeout=120, check=False, stdin=subprocess.DEVNULL) + + def budget(self, journey: str, proc: subprocess.CompletedProcess) -> None: + text = proc.stdout + proc.stderr + most_lines, most_words = LINE_TEXT[journey] + self.assertLessEqual(lines(text), most_lines, text) + self.assertLessEqual(words(text), most_words, text) + + def test_each_journey_says_what_where_result_and_next(self): + repo = Repository(self) + dest = repo.retroarch_here() + # No --yes and no terminal: nothing is asked, nothing waits. + proc = self.run_line(repo) + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.budget("first_install", proc) + said = [line for line in proc.stdout.splitlines() if line.strip()] + self.assertEqual([line.split(" ", 1)[0] for line in said], + ["Installing", "BIOS", "Done.", "Next:"]) + changed = next(e for e in repo.listed["files"] if e["dest"] == "md_eu.bin") + changed["size"] += 1 + repo.write_list() + proc = self.run_line(repo, "--yes") + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.budget("update", proc) + shutil.rmtree(dest) + dest.mkdir() + proc = self.run_line(repo, "--system", "nintendo-gba", "--yes") + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.budget("one_system", proc) + shutil.rmtree(dest) + dest.mkdir() + proc = self.run_line(repo, "--check") + self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr) + self.budget("what_is_missing", proc) + proc = self.run_line(repo, "--platform", "batocera", "--dest", str(repo.home / "bios"), + "--yes") + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.budget("another_device", proc) + + def test_a_card_with_games(self): + repo = Repository(self) + card = repo.root / "MUOS" + (card / "MUOS" / "bios").mkdir(parents=True) + (card / "ROMS" / "GBA").mkdir(parents=True) + (card / "ROMS" / "GBA" / "unknown.gba").write_bytes(b"not in any catalog") + proc = self.run_line(repo, "--yes", card=card) + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.budget("card_with_games", proc) + self.assertIn("what the games need", proc.stdout) + + def test_verbose_gives_back_every_line(self): + """Each line a run prints by default, --verbose prints too, with + more: the same runs into two folders, read as one.""" + repo = Repository(self) + folders = {name: repo.root / name for name in ("default", "verbose")} + runs: dict[str, list] = {} + for name, folder in folders.items(): + extra = ["--verbose"] if name == "verbose" else [] + base = ["--platform", "retroarch", "--dest", str(folder)] + runs[name] = [self.run_line(repo, *base, "--yes", *extra)] + # A later run replaces a changed file and keeps the old one. + (folder / "md_eu.bin").write_bytes(b"changed") + runs[name].append(self.run_line(repo, *base, "--yes", *extra)) + (folder / "md_eu.bin").write_bytes(b"changed") + runs[name].append(self.run_line(repo, *base, "--check", *extra)) + + def read(text: str, folder: Path) -> set: + # The backup folder is named for the second it was made in. + text = re.sub(r"retrobios-backup\S*", "retrobios-backup", text) + return {line.strip().replace(str(folder), "") + for line in text.splitlines() if line.strip()} + + for index in range(3): + with self.subTest(run=index): + quiet, full = runs["default"][index], runs["verbose"][index] + self.assertEqual(quiet.returncode, full.returncode, quiet.stdout + full.stdout) + shown = read(quiet.stdout + quiet.stderr, folders["default"]) + said = read(full.stdout + full.stderr, folders["verbose"]) + self.assertLessEqual(shown, said, sorted(shown - said)) + self.assertGreater(len(said), len(shown)) + + if __name__ == "__main__": unittest.main() 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 4/4] 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. ```