diff --git a/install.ps1 b/install.ps1 index 55444d30e..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 = "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7" + $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 a3ad31f83..13d713275 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,50 @@ 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): - for line in _omission_lines(omitted - len(unplaced), - [entry for entry in omitted_entries or [] - if entry.get("reason") != "no_save_directory"], + 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), 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 []: - 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)) + + +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_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: + 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: @@ -6886,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 @@ -6904,8 +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()}") - 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}.") + # 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 or wrong else "")) def _offline_failure(reason: str) -> bool: @@ -6985,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 = [] @@ -11335,7 +11391,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 +11412,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 @@ -12803,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 "" @@ -12829,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) @@ -14017,6 +14074,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 +14328,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 +15396,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 +15446,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 +15465,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 +15565,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 +15617,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 +15738,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 +15760,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 +15768,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 +15786,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 +15825,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 +15841,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 +15850,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 +15870,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 +15927,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 +15998,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 +16038,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 +16051,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 +16088,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 +16117,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 +16137,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 +16156,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,19 +16223,22 @@ 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.")) for line in check_lines(missing, mismatched, files, manifest, bios_path, args.verbose, up_to_date, bios_index, copies): print(line) for line in unreadable_lines(unread, args.verbose): print(line) + 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 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 @@ -16177,19 +16246,23 @@ 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 - # 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, ): - 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 +16353,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 +16389,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 +16431,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 +16490,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 +16510,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 +16528,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 +16547,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 +16576,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: @@ -17129,6 +17215,10 @@ 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 + # A count standing for Details lines: left out where they are shown. + brief = False def _styled(text: str, style: str = "") -> TuiLine: @@ -17137,6 +17227,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") @@ -17437,6 +17551,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 @@ -18037,7 +18154,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") @@ -18416,9 +18533,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])) @@ -18475,7 +18593,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: @@ -18537,11 +18666,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 @@ -19428,6 +19557,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: @@ -20371,6 +20502,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 [] @@ -20450,6 +20585,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: @@ -21106,7 +21246,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", } @@ -21382,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. @@ -21497,7 +21650,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), @@ -21511,27 +21664,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 @@ -21545,27 +21698,29 @@ 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) if check.get("not_installable"): - lines += _unavailable_lines(state) + lines += _unavailable_brief(state) + _details(_unavailable_lines(state)) if check is state.check: lines += _unplaced_lines(_choice_summary(state), state, "Choose it in Options, Save folder (--save-dir).") @@ -21690,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 @@ -21947,7 +22101,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) @@ -22059,11 +22214,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. @@ -22073,7 +22228,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: @@ -22083,7 +22238,27 @@ 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: 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: + 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 @@ -22375,7 +22550,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))] @@ -22444,18 +22621,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. @@ -22466,6 +22632,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) @@ -22501,7 +22684,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("") @@ -22571,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.""" @@ -22711,13 +22906,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 @@ -22725,14 +22920,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. @@ -22746,7 +22941,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.") @@ -22781,8 +22977,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 @@ -22802,27 +22999,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 += _unavailable_brief(state) + _details(_unavailable_lines(state)) for group in summary.get("left_out") or []: - lines.append(f"{left_out_words(group)}.") + lines.append(_detail(f"{left_out_words(group)}.")) 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. + 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 @@ -22841,10 +23049,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 @@ -22890,6 +23098,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") @@ -22904,7 +23113,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: @@ -22939,6 +23148,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, @@ -22947,7 +23186,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, } @@ -23011,6 +23250,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), @@ -23089,7 +23332,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 @@ -23908,15 +24151,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") @@ -23945,6 +24188,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 c3482d606..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="185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7" +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 3cda2e006..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": 1132111, - "sha256": "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7", + "size": 1143258, + "sha256": "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1185736, - "sha256": "cb245dc33b894a9720ae3734bf55512c2b0fca99e0bd33e8c12a72738085f5cc" + "size": 1196883, + "sha256": "2c84115b87ed4bcd4fdf4d64d56fc1221485cbd9822cfeb0770b07b9b4226e51" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1132111, - "sha256": "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7", + "size": 1143258, + "sha256": "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1132111, - "sha256": "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7", + "size": 1143258, + "sha256": "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738", "executable": false }, { diff --git a/scripts/generate_installer_doc.py b/scripts/generate_installer_doc.py index 8414de2e0..5a1b795e8 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" @@ -598,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..9cf8474af 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 + @@ -744,11 +744,7 @@ ^[[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 +772,11 @@ - ^[[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 @@ + + + + @@ -968,6 +968,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 + + 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..2c14f4510 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 @@ -243,8 +243,8 @@ 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 + + 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..9ec9e5490 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 + @@ -297,11 +297,11 @@ 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 + + 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..64a66ec3e 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 + @@ -457,12 +457,7 @@ 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 +467,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 @@ -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 + + 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..339c84f33 100644 --- a/tests/fixtures/tui/result-120x40.txt +++ b/tests/fixtures/tui/result-120x40.txt @@ -5,11 +5,7 @@ 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 +33,8 @@ - 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..9137c0bd5 100644 --- a/tests/fixtures/tui/result-40x12.txt +++ b/tests/fixtures/tui/result-40x12.txt @@ -8,5 +8,5 @@ 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..ee783f715 100644 --- a/tests/fixtures/tui/result-40x15.txt +++ b/tests/fixtures/tui/result-40x15.txt @@ -8,8 +8,8 @@ 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..d9de0e4ce 100644 --- a/tests/fixtures/tui/result-80x24.txt +++ b/tests/fixtures/tui/result-80x24.txt @@ -6,12 +6,7 @@ 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 +16,9 @@ - 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..f87873e51 --- /dev/null +++ b/tests/fixtures/tui/result-details-120x40.txt @@ -0,0 +1,40 @@ + RetroBIOS: Details + + 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..d2702b407 --- /dev/null +++ b/tests/fixtures/tui/result-details-40x12.txt @@ -0,0 +1,12 @@ + RetroBIOS: Details + + 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..ae7bf6663 --- /dev/null +++ b/tests/fixtures/tui/result-details-40x15.txt @@ -0,0 +1,15 @@ + RetroBIOS: Details + + 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..8486e650c --- /dev/null +++ b/tests/fixtures/tui/result-details-80x24.txt @@ -0,0 +1,24 @@ + RetroBIOS: Details + + 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..d799d290a 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,35 +883,6 @@ 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 @@ -921,7 +892,7 @@ 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 +907,7 @@ - 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 + + Nothing more to say here. + + + + + + + + + A Back +=== result-details 40x15 + RetroBIOS: Details + + Nothing more to say here. + + + + + + + + + + + + A Back +=== result-details 80x24 + RetroBIOS: Details + + Nothing more to say here. + + + + + + + + + + + + + + + + + + + + A Back diff --git a/tests/fixtures/tui/route-gptokeyb.txt b/tests/fixtures/tui/route-gptokeyb.txt index 13e24690f..9e9647ae4 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,35 +883,6 @@ 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 @@ -921,7 +892,7 @@ 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 +907,7 @@ - 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 + + Nothing more to say here. + + + + + + + + + A Back +=== result-details 40x15 + RetroBIOS: Details + + Nothing more to say here. + + + + + + + + + + + + A Back +=== result-details 80x24 + RetroBIOS: Details + + Nothing more to say here. + + + + + + + + + + + + + + + + + + + + A Back diff --git a/tests/fixtures/tui/route-gptokeyb_muxterm.txt b/tests/fixtures/tui/route-gptokeyb_muxterm.txt index 9342b9667..9390cd9a6 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,35 +883,6 @@ 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 @@ -921,7 +892,7 @@ 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 +907,7 @@ - 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 + + Nothing more to say here. + + + + + + + + + A Back +=== result-details 40x15 + RetroBIOS: Details + + Nothing more to say here. + + + + + + + + + + + + A Back +=== result-details 80x24 + RetroBIOS: Details + + Nothing more to say here. + + + + + + + + + + + + + + + + + + + + A Back diff --git a/tests/fixtures/tui/route-keys.txt b/tests/fixtures/tui/route-keys.txt index 4d2e586eb..571fc7b59 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 + @@ -927,8 +927,8 @@ 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 @@ -940,11 +940,11 @@ 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 @@ -954,12 +954,7 @@ 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 +964,12 @@ - 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 + + 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 + + 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 + + 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..d828a7dac 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 + @@ -923,8 +923,8 @@ 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 @@ -936,11 +936,11 @@ 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 @@ -950,7 +950,7 @@ 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 +965,7 @@ - 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 + + Nothing more to say here. + + + + + + + + + A Back +=== result-details 40x15 + RetroBIOS: Details + + Nothing more to say here. + + + + + + + + + + + + A Back +=== result-details 80x24 + RetroBIOS: Details + + Nothing more to say here. + + + + + + + + + + + + + + + + + + + + A Back diff --git a/tests/fixtures/tui/route-muxterm.txt b/tests/fixtures/tui/route-muxterm.txt index 2f19f13e7..397254a58 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 + @@ -981,7 +981,7 @@ 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 @@ -994,7 +994,7 @@ 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 @@ -1008,7 +1008,7 @@ 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 + + Nothing more to say here. + + + + + + + + + Start Back +=== result-details 40x15 + RetroBIOS: Details + + Nothing more to say here. + + + + + + + + + + + + Start Back +=== result-details 80x24 + RetroBIOS: Details + + Nothing more to say here. + + + + + + + + + + + + + + + + + + + + Start Back diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index 0b58a1f26..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)) @@ -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..cd75993d7 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,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.assertIn("wrong copy: lynxboot.img", proc.stdout) self.assertIn("without --check", proc.stdout) @@ -4988,7 +5000,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,9 +5665,9 @@ 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() + 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): @@ -5685,7 +5697,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,10 +5711,11 @@ 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) - 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): @@ -5711,7 +5724,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 +5925,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 +6674,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 +6823,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 +7461,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 +7627,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_journeys.py b/tests/test_install_journeys.py index f73cd6ce6..1582290c6 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,126 @@ 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)) + + 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_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..017a46519 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,21 +3033,26 @@ 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.""" 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): state = fresh_state() state.stack = ["setup"] @@ -3039,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} @@ -3673,6 +3702,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 +3750,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 +3893,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 +4184,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 +4915,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 +4942,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 +4983,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 +5027,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 +5052,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 +5147,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 +5869,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 +6503,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 +6934,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 +7043,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} @@ -7017,6 +7082,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"), \ @@ -7757,6 +7824,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)) @@ -7798,7 +7866,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() @@ -7888,6 +7956,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)) @@ -7965,7 +8034,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 +8116,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: @@ -9012,6 +9084,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() @@ -10077,6 +10150,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") @@ -10222,6 +10296,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): @@ -10943,6 +11018,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 @@ -10959,6 +11035,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.""" @@ -10996,6 +11073,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.""" @@ -11008,6 +11086,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.""" @@ -11287,6 +11366,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.""" @@ -11416,7 +11496,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) @@ -11737,6 +11817,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.""" @@ -11802,6 +11883,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 = [] @@ -11814,6 +11896,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.""" @@ -11825,6 +11908,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.""" @@ -11922,6 +12006,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", @@ -12619,7 +12704,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) @@ -12669,6 +12754,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.""" @@ -12702,6 +12788,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.""" @@ -12774,7 +12861,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 +13319,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_: @@ -13334,6 +13423,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, @@ -13526,6 +13616,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() @@ -13626,7 +13717,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: @@ -14533,6 +14626,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".""" @@ -14659,6 +14753,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) @@ -14836,5 +14931,234 @@ 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: 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), + "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": (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": (30, 30, 19, 19), +} + + +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)) + + +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/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 42c69341a..99a3cc53f 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 @@ -449,6 +453,25 @@ 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. A failure, a wrong copy or a file +an emulator needs that is not available is said too. + + + +``` +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. +``` + + +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 +496,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 +524,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 +560,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. @@ -601,7 +624,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)