From 23804a8da31586e9fedbb97ef722fcf67edec780 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:19:50 +0100 Subject: [PATCH] fix: one total and the same words in both modes Commands break between shell words with a continuation the shell reads, an option kept with its value. The 40x12 setup and plan keep a note or a folder whole. Line mode counts files in place and missing, and names the BIOS folder, as the screen does. The systems question fits its terminal. --- install.ps1 | 2 +- install.py | 257 +++++++++++++++--- install.sh | 2 +- install/launchers/index.json | 16 +- tests/fixtures/tui/colour-120x40.txt | 8 +- tests/fixtures/tui/colour-40x12.txt | 12 +- tests/fixtures/tui/colour-40x15.txt | 16 +- tests/fixtures/tui/colour-80x24.txt | 12 +- tests/fixtures/tui/command-120x40.txt | 4 +- tests/fixtures/tui/command-40x12.txt | 8 +- tests/fixtures/tui/command-40x15.txt | 8 +- tests/fixtures/tui/command-80x24.txt | 4 +- tests/fixtures/tui/result-120x40.txt | 4 +- tests/fixtures/tui/result-40x12.txt | 2 +- tests/fixtures/tui/result-40x15.txt | 8 +- tests/fixtures/tui/result-80x24.txt | 8 +- tests/fixtures/tui/route-evmapy.txt | 6 +- tests/fixtures/tui/route-gptokeyb.txt | 6 +- tests/fixtures/tui/route-gptokeyb_muxterm.txt | 6 +- tests/fixtures/tui/route-keys.txt | 40 +-- tests/fixtures/tui/route-miyoo.txt | 6 +- tests/fixtures/tui/route-muxterm.txt | 22 +- tests/fixtures/tui/setup-40x12.txt | 2 +- tests/test_doc_pages.py | 14 +- tests/test_install.py | 6 +- tests/test_install_experience.py | 4 +- tests/test_install_selection.py | 2 +- tests/test_install_tui.py | 199 +++++++++++++- wiki/first-install.md | 2 +- wiki/installer.md | 50 ++-- wiki/troubleshooting.md | 8 +- 31 files changed, 558 insertions(+), 186 deletions(-) diff --git a/install.ps1 b/install.ps1 index 5313743a8..eaf69702f 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 = "48a5fca25303757fc2874262c1d176992bd7340da5c59f58c6e911383bf54817" + $defaultInstallSha256 = "67cbe64d1de7b2f38edd25cab1b34384c55cac77e13f22f4c302fa2b04a809d7" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index b525f9ac4..47ddc9a84 100644 --- a/install.py +++ b/install.py @@ -5999,7 +5999,7 @@ def _report_outcome( kept = _kept_lines() + ([ f"{_plural(len(unplaced), 'file')} read from " f"{unplaced[0].get('frontend') or 'the frontend'}'s save directory left out: " - "its place is not known. Name it with --save-dir next time."] if unplaced else []) + f"its place is not known. {LINE_SAVE_REMEDY}"] if unplaced else []) kept += list(notes or []) if interrupted: say(f"\nStopped. {_plural(downloaded, 'file')} installed.") @@ -6068,7 +6068,7 @@ def _report_outcome( # Under the files it is about: after the save-directory line, "They" # read as that one file. say(f" {remedy}") - say(f"Location: {_locations(paths)}") + say(f"BIOS folder: {_locations(paths)}") for line in kept + advice: say(line) sys.exit(1 if downloaded else 2) @@ -6079,9 +6079,12 @@ def _report_outcome( for _d, r in failures): # The folder was never the problem: naming it as the place being # written to sent people looking at their disk. - say(f"Destination: {_locations(paths)}") + say(f"BIOS folder: {_locations(paths)}") + elif _retry_words(failures): + # Nothing was written: every file is one the pack lacks. + say(f"BIOS folder: {_locations(paths)}") else: - say(f"Location that was being written to: {_locations(paths)}") + say(f"BIOS folder that was being written to: {_locations(paths)}") for line in kept + advice: say(line) say(_retry_words(failures) or "Run the same command again once that is resolved. " @@ -6095,7 +6098,7 @@ def _report_outcome( say(f" {sources}") if installed_kinds: say(installed_kinds) - say(f"Location: {_locations(paths)}") + say(f"BIOS folder: {_locations(paths)}") for line in kept + advice: say(line) retry = _retry_words(failures) @@ -6105,13 +6108,14 @@ def _report_outcome( say("Run the same command again to retry only those.") sys.exit(1) - say(f"\nDone. {_plural(downloaded, 'file')} installed, " - f"{_plural(up_to_date, 'file')} already up to date.") + # No zero count: "0 files already up to date" said nothing. + say(f"\nDone. {_plural(downloaded, 'file')} installed" + + (f", {_plural(up_to_date, 'file')} already up to date." if up_to_date else ".")) if sources: say(f" {sources}") if installed_kinds: say(installed_kinds) - say(f"Location: {_locations(paths)}") + say(f"BIOS folder: {_locations(paths)}") for line in kept: say(line) if omitted > len(unplaced): @@ -7653,11 +7657,20 @@ def _pick( print(f"\n{title}") width = len(str(len(rows))) shown = {name: screen_text((labels or {}).get(name, name)) for name, _files in rows} - # The sizes in one column, whatever the longest label: cut at 40, the - # longer names pushed theirs right (tui_audit_13 P3-13). - name_width = max(cell_width(label) for label in shown.values()) - for index, (name, files) in enumerate(rows, 1): - print(f" {index:>{width}}) {_pad(shown[name], name_width)} {sizer(files)}") + sizes = {name: sizer(files) for name, files in rows} + # The sizes in one column, as wide as the terminal leaves the names: the + # longest label set it for every row, and 53 of 66 rows were folded at + # 80 columns (code_audit_21 P3-9). A longer label takes its own line. + lead = 2 + width + 2 + room = (shutil.get_terminal_size((80, 24)).columns - 1 - lead - 2 + - max(cell_width(size) for size in sizes.values())) + name_width = min(max(cell_width(label) for label in shown.values()), max(16, room)) + for index, (name, _files) in enumerate(rows, 1): + if cell_width(shown[name]) > name_width: + print(f" {index:>{width}}) {shown[name]}") + print(f"{' ' * (lead + name_width)} {sizes[name]}") + continue + print(f" {index:>{width}}) {_pad(shown[name], name_width)} {sizes[name]}") if note: print(f"\n{note}") hint = "order of preference, e.g. 2,1" if ordered else "e.g. 1,3-5" @@ -12259,9 +12272,13 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: hint = Invocation() hint.os_type, hint.options = os_type, _INVOCATION.options example = _default_dest(os_type, args.platform) + # The run's own way of answering, kept: a script copying the + # example stopped at a question (tui_audit_14 P3-12). + answered = "".join(f" {flag}" for flag, on in ( + ("--yes", args.yes), ("--no-input", args.no_input)) if on) print(" Name its folder with --dest, for example:\n " + hint.command(args.platform, example if example is not None - else ""), file=sys.stderr) + else "") + answered, file=sys.stderr) sys.exit(1) elif args.platform in PORTABLE_ONLY: # A portable emulator reads the folder beside its executable: @@ -12805,10 +12822,17 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: files, bios_path, jobs=args.jobs, term=term ) present = len(up_to_date) + len(mismatched) - print( - f" {present}/{run_counts.files} present " - f"({len(up_to_date)} verified, {len(mismatched)} wrong hash)" - ) + # The screen's words: "0/22 present (0 verified, 0 wrong hash)" + # beside its "0 in place, 22 missing, 0 wrong copies" (tui_audit_14 + # P3-10). The data files the head counts are among them, as the + # screen counts them (tui_audit_14 P3-7). + wrong = len(mismatched) + trees_missing = sum(len(data_directory_state(entry, bios_path)[1]) + for entry in data_dirs) + 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"{wrong} wrong {'copy' if wrong == 1 else 'copies'}") # A partial copy left by an interrupted run is useless once the file # is in place: it goes. @@ -13587,6 +13611,94 @@ def _glued_words(text: str) -> list[str]: _SHELL_WORD = re.compile(r"""(?:'[^']*'|"[^"]*"|[^\s'"]+|['"])+""") +def _path_tail(text: str, width: int) -> str: + """A path cut to its last folders that fit after ".../", as a person + reads it on a narrow screen (".../retroarch/system"), a word that is no + path cut in its middle.""" + if cell_width(text) <= width: + return text + separator = "\\" if "\\" in text and "/" not in text else "/" + parts = text.rstrip(separator).split(separator) + tail = "" + for part in reversed(parts): + longer = f"{part}{separator}{tail}" if tail else part + if cell_width(f"...{separator}{longer}") > width: + break + tail = longer + return f"...{separator}{tail}" if tail else middle(text, width) + + +def _command_groups(text: str) -> list[str]: + """A command's shell words as they stay together on a line: a long + option with its value. "--platform" ended a line, its value on the next + (tui_audit_14 P3-8).""" + groups: list[str] = [] + for word in _SHELL_WORD.findall(text): + last = groups[-1] if groups else "" + if (last.startswith("--") and len(last) > 2 and " " not in last and "=" not in last + and not word.startswith("-") and word != "|"): + groups[-1] = f"{last} {word}" + else: + groups.append(word) + return groups + + +def _continuation() -> str: + """The character that ends a line of a command the shell reads on: a + backtick in PowerShell, a backslash in sh.""" + return "`" if os.environ.get("RETROBIOS_BOOTSTRAP") == "ps1" or os.name == "nt" else "\\" + + +def _command_wrap(text: str, width: int, indent: str = " ") -> list[str]: + """A command too wide for width, broken between its shell words, each + line but the last ending with the continuation the shell accepts, so a + copy of the lines runs as the command does. A line one group wide that + fits only without the indent loses it. A path still wider keeps its + last folders (_path_tail), said by TUI_SHORTENED: folded by the + terminal, it read as broken inside a word (tui_audit_14 P3-8). A web + address is never cut.""" + lead = text[:len(text) - len(text.lstrip(" "))] + tail = " " + _continuation() + groups = _command_groups(text) + lines: list[str] = [] + line = "" + + def close(done: str, last: bool) -> str: + end = "" if last else tail + prefix = lead + indent if lines else lead + for fewer in (lead, ""): + if cell_width(prefix + done + end) > width >= cell_width(fewer + done + end): + prefix = fewer + return prefix + done + end + + for number, group in enumerate(groups): + end = 0 if number == len(groups) - 1 else len(tail) + room = width - cell_width(lead + indent) - end + # A group is an option and its value: split at the first space, a + # quoted value keeping its own. + option, _space, value = (group.partition(" ") if group.startswith("--") and " " in group + else ("", "", group)) + if cell_width(group) > room and "://" not in value and _is_path_word(value.strip("'\"")): + keep = cell_width(option) + 1 if option else 0 + quote = value[0] if value[:1] in "'\"" and value[-1:] == value[:1] else "" + bare = value[1:-1] if quote else value + group = (f"{option} " if option else "") + quote + _path_tail( + bare, max(8, room - keep - 2 * len(quote))) + quote + prefix = lead + indent if lines else lead + longer = f"{line} {group}" if line else group + if not line or cell_width(prefix + longer + (tail if end else "")) <= width: + line = longer + continue + lines.append(close(line, False)) + line = group + lines.append(close(line, True)) + return lines + + +# Said under a command a path of which was cut to fit (_command_wrap). +TUI_SHORTENED = "Shortened to fit this window." + + def _wrap(text: str, width: int, indent: str = " ", whole: str = "") -> list[str]: """Lines of at most width cells, the ones after the first indented. @@ -13597,10 +13709,12 @@ def _wrap(text: str, width: int, indent: str = " ", whole: str = "") -> list[st and a backup folder's dated name fell on a line of its own.""" if cell_width(text) <= width: return [text] + if whole == "command": + return _command_wrap(text, width, indent) lead = text[:len(text) - len(text.lstrip(" "))] lines: list[str] = [] line = "" - words = (_SHELL_WORD.findall(text) if whole == "command" else _glued_words(text)) + words = _glued_words(text) def close(done: str) -> str: # A line one word wide that fits without the indent loses it: one @@ -13636,8 +13750,12 @@ def _wrap(text: str, width: int, indent: str = " ", whole: str = "") -> list[st # The rest of a word broken at a slash joins it without a space: # a path was drawn "retroarch/ system", and copied wrong. longer = f"{line}{'' if index else ' '}{piece}" if line else piece + # A path's pieces keep the indent, which they are cut to fit: + # joined past it, the last piece of a folder lost its indent + # (tui_audit_14 P3-9). if not line or cell_width(prefix + longer) <= width or ( - lines and " " not in longer and cell_width(lead + longer) <= width): + lines and " " not in longer and cell_width(lead + longer) <= width + and not _is_path_word(word)): line = longer else: lines.append(close(line)) @@ -17294,7 +17412,16 @@ def _text_lines(items: list, width: int, whole: bool = False) -> list[TuiLine]: command = style == "command" or getattr(item, "command", False) kind = "command" if command else "text" if whole else "" folder = str(item).lstrip(" ").startswith(_FOLDER_LABEL) - for number, piece in enumerate(_wrap(str(item), width, whole=kind)): + # A folder on its own line wraps at its own indent. + pieces = _wrap(str(item), width, "" if style == "path" and str(item).startswith(" ") + else " ", whole=kind) + if command and "..." in "".join(pieces) and "..." not in str(item): + # Its path cut to fit: said under it, never as part of it. + pieces.append(TUI_SHORTENED) + for number, piece in enumerate(pieces): + if piece is TUI_SHORTENED: + lines.append(_styled(fit(piece, width), "dim")) + continue if cell_width(piece) <= width: # The rest of a BIOS folder's line keeps its weight. line = _styled(fit(piece, width), "folder" if folder and number @@ -17395,8 +17522,15 @@ def _compose( budget -= len(top) gap = [_styled("")] if top and row_lines and budget > 0 else [] budget -= len(gap) - tail = _text_lines(list(after), width) - bottom = ([_styled("")] + tail)[:budget] if tail and budget > 1 else [] + # Whole items only: cut to the room left, a note ended in the + # middle of its sentence at 40x12 (tui_audit_14 P3-5). + tail: list[TuiLine] = [] + for item in after: + lines = _text_lines([item], width) + if 1 + len(tail) + len(lines) > budget: + break + tail += lines + bottom = [_styled(""), *tail] if tail else [] body = top + gap + row_lines + bottom body += [_styled("")] * (room - len(body)) return body + footer_lines @@ -17755,9 +17889,11 @@ def _saves_lines(state: TuiState, saves: dict | None, whole: bool = False) -> li words, folder = save_where_words(saves["folder"]) # The folder alone is cut in the middle: cut with the sentence, it # lost "directory goes to" even at 80 columns. + # The folder on its own line at the BIOS folder's indent: at the + # label's, the two folders of a plan sat at two (tui_audit_14 P3-9). return [f"{_plural(count, 'file')} read from {name}'s save directory " f"{'goes' if count == 1 else 'go'} to{' ' + words if words else ''}:", - shown_path(folder) if whole else _short_path(Path(folder), state.home)] + " " + (shown_path(folder) if whole else _short_path(Path(folder), state.home))] def _unplaced_lines(summary: dict | None, state: TuiState, @@ -17773,6 +17909,13 @@ def _unplaced_lines(summary: dict | None, state: TuiState, f"its place is not known. {remedy or TUI_SAVE_REMEDY}"] +# Said when ES-DE is found, and in fewer words where the long form would +# give way: at 40x12 it ended in the middle of its sentence (tui_audit_14 +# P3-5). +TUI_ESDE_NOTE = "ES-DE found: its BIOS files go to each emulator, not to ES-DE itself." +TUI_ESDE_SHORT = "ES-DE: files go to each emulator." + + def _view_setup(state: TuiState, width: int, height: int) -> tuple: rows = tui_rows(state) focus = rows[min(state.focus.get("setup", 0), len(rows) - 1)] @@ -17782,10 +17925,15 @@ def _view_setup(state: TuiState, width: int, height: int) -> tuple: else "A drive here could be either of these:" if state.doubts else "Nothing was found here."] status = [state.status] if state.status else [] - return ("Where do you play?", - _compose(state, width, height, before, rows, after + status + state.notes, - pinned=state.refused), - ("A/Enter Continue", "B/Esc Quit", "? Help")) + body = _compose(state, width, height, before, rows, after + status + state.notes, + pinned=state.refused) + if TUI_ESDE_NOTE in state.notes and not any( + str(line).strip().startswith("ES-DE found") for line in body): + # The note gave way whole: its short form, where it fits. + notes = [TUI_ESDE_SHORT if note == TUI_ESDE_NOTE else note for note in state.notes] + body = _compose(state, width, height, before, rows, after + status + notes, + pinned=state.refused) + return ("Where do you play?", body, ("A/Enter Continue", "B/Esc Quit", "? Help")) def _view_platforms(state: TuiState, width: int, height: int) -> tuple: @@ -18102,7 +18250,8 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple: # one way on the plan and another on the result (tui_audit_13 P3-3). lines = [piece for line in lines for piece in ( [_styled(part, "folder" if index and str(line).startswith(_FOLDER_LABEL) - else "") for index, part in enumerate(_wrap(str(line), width))] + else "") for index, part in enumerate(_wrap( + str(line), width, "" if str(line).startswith(" ") else " "))] if getattr(line, "style", "") == "path" and cell_width(str(line)) > width else [line])] rows = tui_rows(state) @@ -18127,10 +18276,38 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple: system = detected_os(state.platform, state.dest) if state.dest is not None else "" name = display_name(state.platform, state.manifest) return (f"{system} ({name} files)" if system and system != name else name, - _compose(state, width, height, lines, rows), + _plan_body(state, width, height, lines, rows), ("A/Enter Install", "B/Esc Back", "Y/o Options", "? Help")) +def _plan_body(state: TuiState, width: int, height: int, lines: list, + rows: list[TuiRow]) -> list[TuiLine]: + """The plan's text and rows. Where both do not fit, the headline, the + whole BIOS folder and the version note stay and the rows scroll under + them: at 40x12 the plan named ~/.var/app/ as the folder, and dropped the + note that the list is for another version (tui_audit_14 P3-6).""" + body = _compose(state, width, height, lines, rows) + at = next((index for index, line in enumerate(lines) + if str(line).startswith(_FOLDER_LABEL.rstrip())), None) + kept = list(lines[:1]) + if at is not None: + # The folder line and the rest of the folder wrapped after it. + end = at + 1 + while end < len(lines) and getattr(lines[end], "style", "") == "folder": + end += 1 + kept += lines[at:end] + kept += [line for line in lines if state.version and str(line) == state.version] + top = _text_lines(kept, width) + drawn = [str(line).strip() for line in body] + # Three lines at least for the rows: two of them and where the list is, + # the blank line above them given up first. + if (all(str(line).strip() in drawn for line in top) + or len(top) + min(len(rows), 3) > height): + return body + gap = [_styled("")] if len(top) + 1 + min(len(rows), 3) <= height else [] + return top + gap + _compose(state, width, height - len(top) - len(gap), [], rows) + + def _plan_data(state: TuiState, check: dict) -> tuple[int, int | None]: """The data files the plan counts and their size: the check's, or, while the plan estimates the systems ticked, those of the data folders @@ -18598,6 +18775,11 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]: # twice, the remedy between them (tui_audit_11 P3-18). lines[0] = sources sources = "" + elif title == "Done" and sources and "," not in sources and not summary.get("kept"): + # Every file from one place: "22 files installed." over "22 files + # copied from packx." said one count twice (tui_audit_14 P3-10). + lines[0] = sources + head[head.index(".") + 1:] + sources = "" if sources: lines.append(sources) if left: @@ -18697,13 +18879,16 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]: 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 (tui_audit_13 P3-10). - lines.append(_styled(TUI_RESULT_COPY, "dim")) + lines.append(_styled(TUI_RESULT_AGAIN if state.os_type == "android" + else TUI_RESULT_COPY, "dim")) return title, lines # How to copy from the result, under its command while the mouse is on. -TUI_RESULT_COPY = ("Shift and a drag select text. All of this is shown again in the " - "terminal after Exit.") +# Termux has no Shift key under a finger: there the second sentence alone +# (tui_audit_14 P3-3). +TUI_RESULT_AGAIN = "All of this is shown again in the terminal after Exit." +TUI_RESULT_COPY = f"Shift and a drag select text. {TUI_RESULT_AGAIN}" def _one_kind(state: TuiState, installed: int) -> bool: @@ -18877,6 +19062,7 @@ def _tap_help(state: TuiState) -> list[tuple[str, str]]: if state.screen in _PAGERS and not tui_rows(state): return [("Tap", "A bottom line entry presses its key"), *([("Shift+drag", "Select text to copy")] if state.screen == "result" + and state.os_type != "android" and state.pad not in TUI_PAD_ROUTES else [])] # Said at every size, 40x12 included: Install below waits for a second # tap, and the help is where a reader looks when nothing happened. @@ -19256,8 +19442,7 @@ def tui_start( for folder, why in _DETECTION_REFUSED] unsaid = {refused_words(*refused) for refused in _DETECTION_REFUSED} if "esde" in frontends: - state.notes.append("ES-DE found: its BIOS files go to each emulator, " - "not to ES-DE itself.") + state.notes.append(TUI_ESDE_NOTE) state.notes += [line.strip() for line in captured.getvalue().splitlines() if line.strip() and line.strip() not in unsaid] if plat and args.dest is not None: @@ -19774,7 +19959,7 @@ LINE_SAVE_REMEDY = "Name it with --save-dir next time." 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 - if line != TUI_RESULT_COPY] + if line not in (TUI_RESULT_COPY, TUI_RESULT_AGAIN)] def tui_quit_chosen(state: TuiState) -> bool: diff --git a/install.sh b/install.sh index 6b3f40947..cb52eff70 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="48a5fca25303757fc2874262c1d176992bd7340da5c59f58c6e911383bf54817" +DEFAULT_INSTALL_SHA256="67cbe64d1de7b2f38edd25cab1b34384c55cac77e13f22f4c302fa2b04a809d7" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/launchers/index.json b/install/launchers/index.json index 3fe893f19..4f0d7be40 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": 929922, - "sha256": "48a5fca25303757fc2874262c1d176992bd7340da5c59f58c6e911383bf54817", + "size": 939291, + "sha256": "67cbe64d1de7b2f38edd25cab1b34384c55cac77e13f22f4c302fa2b04a809d7", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 983099, - "sha256": "78658671fe962a5a4efb8ea4a3a50960cc5680b5109391e8536951b5f0a97d14" + "size": 992468, + "sha256": "06761ebb41ef22fa2c4b64c5e27fab5ed5508dbcd63c5a634527d03a7cdaa7d1" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 929922, - "sha256": "48a5fca25303757fc2874262c1d176992bd7340da5c59f58c6e911383bf54817", + "size": 939291, + "sha256": "67cbe64d1de7b2f38edd25cab1b34384c55cac77e13f22f4c302fa2b04a809d7", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 929922, - "sha256": "48a5fca25303757fc2874262c1d176992bd7340da5c59f58c6e911383bf54817", + "size": 939291, + "sha256": "67cbe64d1de7b2f38edd25cab1b34384c55cac77e13f22f4c302fa2b04a809d7", "executable": false }, { diff --git a/tests/fixtures/tui/colour-120x40.txt b/tests/fixtures/tui/colour-120x40.txt index 43e542aa8..376973b2e 100644 --- a/tests/fixtures/tui/colour-120x40.txt +++ b/tests/fixtures/tui/colour-120x40.txt @@ -618,8 +618,8 @@ The same choice without questions: - python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system --system - nintendo-gba + python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system \ + --system nintendo-gba @@ -743,8 +743,8 @@ BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m ^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404) 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 + 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 diff --git a/tests/fixtures/tui/colour-40x12.txt b/tests/fixtures/tui/colour-40x12.txt index de15134f9..b1eac105c 100644 --- a/tests/fixtures/tui/colour-40x12.txt +++ b/tests/fixtures/tui/colour-40x12.txt @@ -9,7 +9,7 @@ BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m - ES-DE found: its BIOS files go to each + ES-DE: files go to each emulator. ^[[0;1;35mA^[[0m Continue ^[[0;2mB^[[0m Quit ^[[0;2m?^[[0m Help === platforms ^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which setup?^[[0m @@ -198,11 +198,11 @@ The same choice without questions: - python3 install.py --platform - retroarch --dest - /home/user/.config/retroarch/system + python3 install.py \ + --platform retroarch \ + --dest .../retroarch/system \ --system nintendo-gba - +^[[0;2m Shortened to fit this window.^[[0m ^[[0;1;35mA^[[0m Back @@ -242,7 +242,7 @@ The files it replaced are kept in /home/user/.config/retroarch/ retrobios-backup/2026-10-23_1200 - To put them back: python3 install.py + To put them back: python3 install.py \ ^[[0;2m 1-8 of 16^[[0m ^[[0;1;35mA^[[0m Exit ^[[0;2m^v^[[0m Scroll === error diff --git a/tests/fixtures/tui/colour-40x15.txt b/tests/fixtures/tui/colour-40x15.txt index 1b2064684..a1914ccc5 100644 --- a/tests/fixtures/tui/colour-40x15.txt +++ b/tests/fixtures/tui/colour-40x15.txt @@ -243,11 +243,11 @@ The same choice without questions: - python3 install.py --platform - retroarch --dest - /home/user/.config/retroarch/system + python3 install.py \ + --platform retroarch \ + --dest .../retroarch/system \ --system nintendo-gba - +^[[0;2m Shortened to fit this window.^[[0m @@ -296,10 +296,10 @@ 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 - + To put them back: python3 install.py \ + --restore .../2026-10-23_1200 +^[[0;2m Shortened to fit this window.^[[0m + Same choice without questions: ^[[0;2m 1-11 of 16^[[0m ^[[0;1;35mA^[[0m Exit ^[[0;2m^v^[[0m Scroll === error diff --git a/tests/fixtures/tui/colour-80x24.txt b/tests/fixtures/tui/colour-80x24.txt index 8bf355418..2501614e0 100644 --- a/tests/fixtures/tui/colour-80x24.txt +++ b/tests/fixtures/tui/colour-80x24.txt @@ -378,8 +378,8 @@ The same choice without questions: - python3 install.py --platform retroarch --dest - /home/user/.config/retroarch/system --system nintendo-gba + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system --system nintendo-gba @@ -456,11 +456,11 @@ ^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404) 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 + 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 + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system Details: /tmp/retrobios-x/install.log diff --git a/tests/fixtures/tui/command-120x40.txt b/tests/fixtures/tui/command-120x40.txt index 37fee17e2..45ddf97fc 100644 --- a/tests/fixtures/tui/command-120x40.txt +++ b/tests/fixtures/tui/command-120x40.txt @@ -2,8 +2,8 @@ The same choice without questions: - python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system --system - nintendo-gba + python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system \ + --system nintendo-gba diff --git a/tests/fixtures/tui/command-40x12.txt b/tests/fixtures/tui/command-40x12.txt index 1b955d059..09b184eac 100644 --- a/tests/fixtures/tui/command-40x12.txt +++ b/tests/fixtures/tui/command-40x12.txt @@ -2,11 +2,11 @@ The same choice without questions: - python3 install.py --platform - retroarch --dest - /home/user/.config/retroarch/system + python3 install.py \ + --platform retroarch \ + --dest .../retroarch/system \ --system nintendo-gba - + Shortened to fit this window. A Back diff --git a/tests/fixtures/tui/command-40x15.txt b/tests/fixtures/tui/command-40x15.txt index 3e8530cf0..67a248f0c 100644 --- a/tests/fixtures/tui/command-40x15.txt +++ b/tests/fixtures/tui/command-40x15.txt @@ -2,11 +2,11 @@ The same choice without questions: - python3 install.py --platform - retroarch --dest - /home/user/.config/retroarch/system + python3 install.py \ + --platform retroarch \ + --dest .../retroarch/system \ --system nintendo-gba - + Shortened to fit this window. diff --git a/tests/fixtures/tui/command-80x24.txt b/tests/fixtures/tui/command-80x24.txt index 1b0a8b3ef..7fe74c7b9 100644 --- a/tests/fixtures/tui/command-80x24.txt +++ b/tests/fixtures/tui/command-80x24.txt @@ -2,8 +2,8 @@ The same choice without questions: - python3 install.py --platform retroarch --dest - /home/user/.config/retroarch/system --system nintendo-gba + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system --system nintendo-gba diff --git a/tests/fixtures/tui/result-120x40.txt b/tests/fixtures/tui/result-120x40.txt index de7a4e732..246502f05 100644 --- a/tests/fixtures/tui/result-120x40.txt +++ b/tests/fixtures/tui/result-120x40.txt @@ -4,8 +4,8 @@ BIOS folder: /home/user/.config/retroarch/system failed: scph5500.bin (HTTP 404) 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 + 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 diff --git a/tests/fixtures/tui/result-40x12.txt b/tests/fixtures/tui/result-40x12.txt index aacffd558..9b9270843 100644 --- a/tests/fixtures/tui/result-40x12.txt +++ b/tests/fixtures/tui/result-40x12.txt @@ -7,6 +7,6 @@ The files it replaced are kept in /home/user/.config/retroarch/ retrobios-backup/2026-10-23_1200 - To put them back: python3 install.py + To put them back: python3 install.py \ 1-8 of 16 A Exit ^v Scroll diff --git a/tests/fixtures/tui/result-40x15.txt b/tests/fixtures/tui/result-40x15.txt index 3b7775d28..caf7d87d0 100644 --- a/tests/fixtures/tui/result-40x15.txt +++ b/tests/fixtures/tui/result-40x15.txt @@ -7,9 +7,9 @@ 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 - + To put them back: python3 install.py \ + --restore .../2026-10-23_1200 + Shortened to fit this window. + Same choice without questions: 1-11 of 16 A Exit ^v Scroll diff --git a/tests/fixtures/tui/result-80x24.txt b/tests/fixtures/tui/result-80x24.txt index 9ec8aec5b..172cd1c52 100644 --- a/tests/fixtures/tui/result-80x24.txt +++ b/tests/fixtures/tui/result-80x24.txt @@ -5,11 +5,11 @@ failed: scph5500.bin (HTTP 404) 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 + 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 + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system Details: /tmp/retrobios-x/install.log diff --git a/tests/fixtures/tui/route-evmapy.txt b/tests/fixtures/tui/route-evmapy.txt index d43544f91..dea4a502d 100644 --- a/tests/fixtures/tui/route-evmapy.txt +++ b/tests/fixtures/tui/route-evmapy.txt @@ -9,7 +9,7 @@ BIOS folder: /home/user/.config/retroarch/system - ES-DE found: its BIOS files go to each + ES-DE: files go to each emulator. A Continue B Quit === setup 40x15 RetroBIOS: Where do you play? @@ -755,8 +755,8 @@ The same choice without questions: - python3 install.py --platform retroarch --dest - /home/user/.config/retroarch/system --system nintendo-gba + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system --system nintendo-gba diff --git a/tests/fixtures/tui/route-gptokeyb.txt b/tests/fixtures/tui/route-gptokeyb.txt index 4e688a03b..be42befe4 100644 --- a/tests/fixtures/tui/route-gptokeyb.txt +++ b/tests/fixtures/tui/route-gptokeyb.txt @@ -9,7 +9,7 @@ BIOS folder: /home/user/.config/retroarch/system - ES-DE found: its BIOS files go to each + ES-DE: files go to each emulator. A Continue B Quit === setup 40x15 RetroBIOS: Where do you play? @@ -755,8 +755,8 @@ The same choice without questions: - python3 install.py --platform retroarch --dest - /home/user/.config/retroarch/system --system nintendo-gba + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system --system nintendo-gba diff --git a/tests/fixtures/tui/route-gptokeyb_muxterm.txt b/tests/fixtures/tui/route-gptokeyb_muxterm.txt index fec9d3208..cd39e4a38 100644 --- a/tests/fixtures/tui/route-gptokeyb_muxterm.txt +++ b/tests/fixtures/tui/route-gptokeyb_muxterm.txt @@ -9,7 +9,7 @@ BIOS folder: /home/user/.config/retroarch/system - ES-DE found: its BIOS files go to each + ES-DE: files go to each emulator. A Continue B Quit === setup 40x15 RetroBIOS: Where do you play? @@ -755,8 +755,8 @@ The same choice without questions: - python3 install.py --platform retroarch --dest - /home/user/.config/retroarch/system --system nintendo-gba + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system --system nintendo-gba diff --git a/tests/fixtures/tui/route-keys.txt b/tests/fixtures/tui/route-keys.txt index 74169d344..189f0f71e 100644 --- a/tests/fixtures/tui/route-keys.txt +++ b/tests/fixtures/tui/route-keys.txt @@ -9,7 +9,7 @@ BIOS folder: /home/user/.config/retroarch/system - ES-DE found: its BIOS files go to each + ES-DE: files go to each emulator. Enter Continue Esc Quit ? Help === setup 40x15 RetroBIOS: Where do you play? @@ -759,11 +759,11 @@ The same choice without questions: - python3 install.py --platform - retroarch --dest - /home/user/.config/retroarch/system + python3 install.py \ + --platform retroarch \ + --dest .../retroarch/system \ --system nintendo-gba - + Shortened to fit this window. Enter Back @@ -772,11 +772,11 @@ The same choice without questions: - python3 install.py --platform - retroarch --dest - /home/user/.config/retroarch/system + python3 install.py \ + --platform retroarch \ + --dest .../retroarch/system \ --system nintendo-gba - + Shortened to fit this window. @@ -788,8 +788,8 @@ The same choice without questions: - python3 install.py --platform retroarch --dest - /home/user/.config/retroarch/system --system nintendo-gba + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system --system nintendo-gba @@ -926,7 +926,7 @@ The files it replaced are kept in /home/user/.config/retroarch/ retrobios-backup/2026-10-23_1200 - To put them back: python3 install.py + To put them back: python3 install.py \ 1-8 of 16 Enter Exit ^v Scroll === result 40x15 @@ -939,10 +939,10 @@ 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 - + To put them back: python3 install.py \ + --restore .../2026-10-23_1200 + Shortened to fit this window. + Same choice without questions: 1-11 of 16 Enter Exit ^v Scroll === result 80x24 @@ -953,11 +953,11 @@ failed: scph5500.bin (HTTP 404) 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 + 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 + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system Details: /tmp/retrobios-x/install.log diff --git a/tests/fixtures/tui/route-miyoo.txt b/tests/fixtures/tui/route-miyoo.txt index ca3a944af..5b9098ff5 100644 --- a/tests/fixtures/tui/route-miyoo.txt +++ b/tests/fixtures/tui/route-miyoo.txt @@ -9,7 +9,7 @@ BIOS folder: /home/user/.config/retroarch/system - ES-DE found: its BIOS files go to each + ES-DE: files go to each emulator. A Continue R2 Quit === setup 40x15 RetroBIOS: Where do you play? @@ -784,8 +784,8 @@ The same choice without questions: - python3 install.py --platform retroarch --dest - /home/user/.config/retroarch/system --system nintendo-gba + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system --system nintendo-gba diff --git a/tests/fixtures/tui/route-muxterm.txt b/tests/fixtures/tui/route-muxterm.txt index d02b5a181..05260a80f 100644 --- a/tests/fixtures/tui/route-muxterm.txt +++ b/tests/fixtures/tui/route-muxterm.txt @@ -9,7 +9,7 @@ BIOS folder: /home/user/.config/retroarch/system - ES-DE found: its BIOS files go to each + ES-DE: files go to each emulator. Start Continue B Quit === setup 40x15 RetroBIOS: Where do you play? @@ -813,11 +813,11 @@ The same choice without questions: - python3 install.py --platform - retroarch --dest - /home/user/.config/retroarch/system + python3 install.py \ + --platform retroarch \ + --dest .../retroarch/system \ --system nintendo-gba - + Shortened to fit this window. Start Back @@ -826,11 +826,11 @@ The same choice without questions: - python3 install.py --platform - retroarch --dest - /home/user/.config/retroarch/system + python3 install.py \ + --platform retroarch \ + --dest .../retroarch/system \ --system nintendo-gba - + Shortened to fit this window. @@ -842,8 +842,8 @@ The same choice without questions: - python3 install.py --platform retroarch --dest - /home/user/.config/retroarch/system --system nintendo-gba + python3 install.py --platform retroarch \ + --dest /home/user/.config/retroarch/system --system nintendo-gba diff --git a/tests/fixtures/tui/setup-40x12.txt b/tests/fixtures/tui/setup-40x12.txt index 2d87a3413..29dd46547 100644 --- a/tests/fixtures/tui/setup-40x12.txt +++ b/tests/fixtures/tui/setup-40x12.txt @@ -8,5 +8,5 @@ BIOS folder: /home/user/.config/retroarch/system - ES-DE found: its BIOS files go to each + ES-DE: files go to each emulator. A Continue B Quit ? Help diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index fd8742547..2a262004f 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -97,7 +97,7 @@ class InstallerOutputExample(unittest.TestCase): for part in omitted.group(1).split(", "): self.assertIn(part.rsplit(":", 1)[0], install.OMISSION_REASONS.values()) count, size = re.search(r"\n (\d+) files \(([\d.]+ [KMG]B)\)", run).groups() - self.assertIn(f"0/{count} present", run) + self.assertIn(f"0 in place, {count} missing", run) self.assertSplitAddsUp(run, count, size) menu = re.search(r"RetroArch: (\d+) files, ([\d.]+ [KMG]B)[.,]([^`]*)", text) self.assertEqual(menu.groups()[:2], (count, size)) @@ -118,10 +118,14 @@ class InstallerOutputExample(unittest.TestCase): r"files \(([\d.]+) ([KMG]B)\)", text) self.assertIsNotNone(split, "a categorized list says its BIOS files and game data") bios, bios_size, bios_unit, games, game_size, game_unit = split.groups() - self.assertEqual(int(bios) + int(games), int(count)) + # The data files in the one total, the third kind (code_audit_21 P3-8). + data = re.search(r"and (\d+) data files \(([\d.]+) ([KMG]B)\)", text) + data_count, data_size, data_unit = data.groups() if data else ("0", "0", "KB") + self.assertEqual(int(bios) + int(games) + int(data_count), int(count)) units = {"KB": 1, "MB": 1024, "GB": 1024 ** 2} total, unit = size.split() - added = (float(bios_size) * units[bios_unit] + float(game_size) * units[game_unit]) + added = (float(bios_size) * units[bios_unit] + float(game_size) * units[game_unit] + + float(data_size) * units[data_unit]) self.assertLessEqual(abs(added - float(total) * units[unit]), 0.15 * units[unit]) @@ -744,10 +748,10 @@ class FirstInstallWalkthrough(unittest.TestCase): self.assertIn(f'title = "{ending}"', source) self.assertNotRegex(self.text, r"Done\. \d+ files") count, size = re.search(r"\n (\d+) files \(([\d.]+ [KMG]B)\)", step3).groups() - self.assertIn(f"0/{count} present", step3) + self.assertIn(f"0 in place, {count} missing", step3) self.assertIn(f"Downloading {count} files ({size})", step3) self.assertIn(f"Done. {count} files installed", step4) - self.assertIn(f"{count}/{count} present ({count} verified", step5) + self.assertIn(f"{count} in place, 0 missing", step5) manifest = json.loads((ROOT / "install" / "batocera.json").read_text(encoding="utf-8")) omitted = re.search(r"Not installable: (\d+) files \((\d+) required", step3) if manifest.get("omitted_files"): diff --git a/tests/test_install.py b/tests/test_install.py index 991fb37af..de1ec5fc7 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -3550,7 +3550,7 @@ class ARefusedStandaloneCopyIsNamed(unittest.TestCase): install._report_outcome([bios], 0, 1, failed, 0, False, failures=failures) text = out.getvalue() self.assertIn(str(target / "scph5501.bin"), text) - self.assertIn(str(target), text.split("Location")[1]) + self.assertIn(str(target), text.split("BIOS folder")[-1]) self.assertIn("cannot write there", text) @@ -4534,7 +4534,7 @@ class ANetworkFailureNamesTheNetwork(unittest.TestCase): self.assertEqual(raised.exception.code, 2) text = out.getvalue() self.assertIn("HTTPS_PROXY", text) - self.assertIn("Destination: /bios", text) + self.assertIn("BIOS folder: /bios", text) self.assertNotIn("being written to", text) @@ -5225,7 +5225,7 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree): "interrupted": False}, {"event": "end", "code": 1}, ]) - self.assertIn("2/4 present (1 verified, 1 wrong hash)", proc.stderr) + self.assertIn("1 in place, 2 missing, 1 wrong copy", proc.stderr) self.assertFalse((self.bios / "c.bin").exists()) def test_an_install_run(self): diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 8dc06ad8e..de86e7928 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -678,7 +678,7 @@ class AHomeThisUserCannotOpen(unittest.TestCase): with self.subTest(run=name): self.assertNotIn("Traceback", proc.stderr) # A named folder needs no detection and is checked. - self.assertIn("0/5 present", runs["named"].stdout) + self.assertIn("0 in place, 5 missing", runs["named"].stdout) self.assertIn(f"Could not look for setups: this user cannot open {home}", runs["detected"].stderr) @@ -811,7 +811,7 @@ class APartlyClosedHome(unittest.TestCase): self.assertNotIn(str(home / ".config" / "retroarch" / "system"), proc.stdout + proc.stderr) self.assertIn(f"Found RetroArch at {system}", runs["detected"].stdout) - self.assertIn("0/5 present", runs["named"].stdout) + self.assertIn("0 in place, 5 missing", runs["named"].stdout) self.assertNotIn("not detected", runs["named"].stdout) events = [json.loads(line) for line in runs["events"].stdout.splitlines()] kinds = [event["event"] for event in events] diff --git a/tests/test_install_selection.py b/tests/test_install_selection.py index ec313ad02..29d3e2ad7 100644 --- a/tests/test_install_selection.py +++ b/tests/test_install_selection.py @@ -1459,7 +1459,7 @@ class ThePlatformsOwnListAlone(unittest.TestCase): with self.subTest(mode=mode), FakeRepository() as repo: proc = repo.run("--declared-only", mode) self.assertIn("5 to 3 files", proc.stdout, proc.stderr) - self.assertIn("0/3 present", proc.stdout) + self.assertIn("0 in place, 3 missing", proc.stdout) self.assertEqual(repo.installed(), []) def test_it_composes_with_the_other_narrowings(self): diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index a0dfb7a48..1b1430913 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -771,7 +771,8 @@ class PlanWarnings(unittest.TestCase): self.assertIn("You cannot write to this folder: choose another one.", text) # sudo changes the home folder: the advice names this one. self.assertIn("Or run it as root, this folder named: sudo python3 install.py " - f"--platform retroarch --dest {SYSTEM_DIR}", " ".join(text.split())) + f"--platform retroarch --dest {SYSTEM_DIR}", + " ".join(text.replace(" \\", "").split())) rows = install.tui_rows(state) self.assertEqual([row.action for row in rows[:2]], ["folder", "install"]) self.assertEqual(state.focus.get("plan", 0), 0) @@ -3737,8 +3738,9 @@ class AuditFive(unittest.TestCase): saves = {"files": 1, "folder": str(HOME / ".config" / "retroarch" / "saves" / "")} lines = install._saves_lines(state, saves) # In words: "saves/" read as a folder named . + # The folder at the BIOS folder's indent (tui_audit_14 P3-9). self.assertEqual(lines, ["1 file read from RetroArch's save directory goes to a " - "folder per core under:", "~/.config/retroarch/saves"]) + "folder per core under:", " ~/.config/retroarch/saves"]) def test_the_shared_note_is_one_line(self): state = self.long_list() @@ -7578,7 +7580,7 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings, self.assertIn("g) Game data: kept (3 files, 120 B): leave it out", text) self.assertIn(" 4 files (123 B)\n 1 BIOS file (3 B) and 3 game data files (120 B).", text) - self.assertIn("0/4 present", text) + self.assertIn("0 in place, 4 missing, 0 wrong copies", text) self.assertIn("4 files (123 B)", text[text.index("Checking existing files"):]) def test_g_counts_the_systems_chosen_and_leaves_out_what_it_said(self): @@ -9966,8 +9968,12 @@ class TheAndroidResultReadsWhole(unittest.TestCase): lines = [str(line).strip() for line in self.result(cols)] words = [word for line in lines for word in line.split()] self.assertIn("--restore", words) - # The command keeps it whole, to be copied as one word. - self.assertIn(full, words) + # The command keeps it whole where its line holds it, cut to + # its last folders and said so where not (tui_audit_14 P3-8). + if cols >= 60: + self.assertIn(full, words) + else: + self.assertIn(install.TUI_SHORTENED, lines) # Prose breaks it after a separator only, never inside a name # (tui_audit_13 P3-3): glued again, it is whole. self.assertIn(full, re.sub(r"/ ", "/", " ".join(lines))) @@ -9979,8 +9985,11 @@ class TheAndroidResultReadsWhole(unittest.TestCase): state.stack.append("result") pieces = install._text_lines([command], 38, whole=True) joined = [str(piece).strip() for piece in pieces if piece.style != "cont"] - self.assertIn(install.BOOTSTRAP_URL, joined) - self.assertIn("'/storage/emulated/0/a b/20261027-223524'", joined) + # The address never cut, the option with its value, each line but + # the last ending with the shell's continuation. + self.assertIn(f"{install.BOOTSTRAP_URL} \\", joined) + self.assertTrue(any(line.startswith("--restore '") for line in joined), joined) + self.assertTrue(all(len(str(piece)) <= 38 for piece in pieces), pieces) def test_a_long_line_folds_in_the_terminal(self): terminal = install._TuiTerminal.__new__(install._TuiTerminal) @@ -10854,10 +10863,12 @@ class AuditNineteenAndTwelve(_CardFixtures, unittest.TestCase): width = len(path) + 1 lines = install._wrap(f"BIOS folder: {path}", width, whole="text") self.assertEqual(lines, ["BIOS folder:", path]) - command = install._wrap(f" python3 install.py --dest {path}", width + 1, + # A command keeps the option with its value, the path whole where + # the line holds them (tui_audit_14 P3-8). + command = install._wrap(f" python3 install.py --dest {path}", width + 10, whole="command") - self.assertIn(f" {path}", command) - self.assertTrue(all(len(line) <= width + 1 for line in command), command) + self.assertIn(f" --dest {path}", command) + self.assertTrue(all(len(line) <= width + 10 for line in command), command) def test_the_result_spells_each_folder_one_way(self): """tui_audit_12 P3-7: the save folder as ~/ beside full paths.""" @@ -10865,7 +10876,7 @@ class AuditNineteenAndTwelve(_CardFixtures, unittest.TestCase): state.done_saves = {"event": "saves", "files": 2, "folder": "/home/user/.config/retroarch/saves/"} _title, lines = install.tui_outcome(state) - self.assertIn("/home/user/.config/retroarch/saves", lines) + self.assertIn("/home/user/.config/retroarch/saves", [line.strip() for line in lines]) self.assertFalse(any(line.startswith("~") for line in lines)) def test_minui_names_the_folder_of_its_plan(self): @@ -11506,7 +11517,11 @@ class AuditTwentyAndThirteen(unittest.TestCase): while pieces[-1].endswith("/") and lines[at + len(pieces)]: pieces.append(lines[at + len(pieces)]) spelled.append(pieces) - self.assertNotIn("...", " ".join(lines)) + # A command's path may be cut to fit, said so: a folder + # in prose never is. + self.assertNotIn("...", " ".join( + line for line in lines if install.tui_command(state)[:10] not in line + and not line.startswith("--"))) self.assertTrue(all(len(piece) <= cols - 2 for piece in pieces), pieces) self.assertEqual(spelled[0], spelled[1]) self.assertEqual("".join(spelled[0]), f"BIOS folder: {dest}") @@ -11516,9 +11531,9 @@ class AuditTwentyAndThirteen(unittest.TestCase): under an indent of 6.""" path = "/home/moi/retroarch_system/tmp/tui_audit13/pack/MinUI_2025-11_BIOS_Pack.zip" self.assertEqual(len(path), 75) - lines = install._wrap(f" python3 install.py --from-pack {path}", 78, + lines = install._wrap(f" python3 install.py --from-pack {path}", 95, whole="command") - self.assertIn(path, lines) + self.assertIn(f" --from-pack {path}", lines) def test_the_definition_is_said_once_and_never_spliced(self): """tui_audit_13 P3-5.""" @@ -12346,6 +12361,162 @@ class AuditTwentyOneAndFourteen(unittest.TestCase): _title, lines = install.tui_outcome(state) self.assertIn("7 data files not installed: not in RetroArch_BIOS_Pack.zip.", lines) + def test_a_command_at_40_columns_breaks_between_shell_words(self): + """tui_audit_14 P3-8: the terminal folded commands inside a word, + "--platform" ended a line with its value on the next.""" + words = ["--platform", "retroarch", "--dest", "/a/b/c/system", "--manifest", + "/home/moi/x.json", "--system", "nintendo-gba"] + text = "printf '%s\\n' " + " ".join(words) + with mock.patch.dict(os.environ, {"RETROBIOS_BOOTSTRAP": "sh"}): + lines = install._wrap(text, 38, whole="command") + self.assertGreater(len(lines), 1) + self.assertTrue(all(install.cell_width(line) <= 38 for line in lines), lines) + self.assertTrue(all(line.endswith(" \\") for line in lines[:-1]), lines) + for line in lines: + # An option never ends a line without its value. + self.assertFalse(line.removesuffix(" \\").rstrip().endswith( + ("--platform", "--dest", "--manifest", "--system")), lines) + # The shell reads the lines as the command: the same words. + proc = subprocess.run(["sh", "-c", "\n".join(lines)], capture_output=True, text=True, + check=True) + self.assertEqual(proc.stdout.split(), words) + # A path wider than a line keeps its last folders, said so, and the + # address of the one-liner is never cut. + state = _installed_state(Path("/home/moi/retroarch_system/tmp/tui_audit14/sb/a/b/bios")) + state.summary["kept"] = [{"folder": "/home/moi/retroarch_system/tmp/tui_audit14/sb/" + "retrobios-backup/2026", "files": 1, "bytes": 1}] + with mock.patch.dict(os.environ, {"RETROBIOS_BOOTSTRAP": "sh"}): + screen = [str(line) for line in install.tui_view(state, 40, 40)] + self.assertTrue(all(install.cell_width(line) <= 40 for line in screen), screen) + self.assertIn(install.TUI_SHORTENED, " ".join(screen)) + self.assertIn(install.BOOTSTRAP_URL, " ".join(screen)) + state.exit_code = 0 + farewell = "\n".join(install.tui_farewell(state)) + self.assertIn("/home/moi/retroarch_system/tmp/tui_audit14/sb/a/b/bios", farewell) + + def test_a_folder_on_its_own_line_has_one_indent(self): + """tui_audit_14 P3-9: the save folder at the label's indent, the + BIOS folder under it, a path's last piece without its indent.""" + state = checked_state() + state.dest = Path("/home/moi/retroarch_system/tmp/tui_audit14/sb/esmiyoo/home/" + ".config/retroarch/system") + state.saves = {"files": 1, "folder": "/home/moi/retroarch_system/tmp/tui_audit14/" + "sb/and2/storage/RetroArch/saves/"} + for cols in (40, 80): + with self.subTest(cols=cols): + lines = [str(line) for line in install.tui_view(state, cols, 40)] + folder = next(i for i, line in enumerate(lines) if "BIOS folder:" in line) + under = next(i for i, line in enumerate(lines) if "under:" in line) + for at in (folder, under): + pieces = [] + for line in lines[at + 1:]: + if not line.strip() or "/" not in line or " " in line.strip(): + break + pieces.append(line) + indents = {len(piece) - len(piece.lstrip()) for piece in pieces} + self.assertLessEqual(len(indents), 1, pieces) + if pieces: + self.assertEqual(indents, {3}, pieces) + + def test_no_shift_where_there_is_no_shift_key(self): + """tui_audit_14 P3-3: "Shift and a drag" on Android.""" + for os_type, said in (("linux", True), ("android", False)): + with self.subTest(os_type=os_type): + state = _installed_state(SYSTEM_DIR) + state.os_type, state.mouse = os_type, True + _title, lines = install.tui_outcome(state) + self.assertEqual(any("Shift" in line for line in lines), said) + self.assertIn(install.TUI_RESULT_AGAIN, lines[-1]) + state.help = True + text = " ".join(install.tui_view(state, 80, 24)) + self.assertEqual("Shift+drag" in text, said) + + def test_the_40x12_setup_never_ends_mid_sentence(self): + """tui_audit_14 P3-5.""" + state = fresh_state() + state.notes = [install.TUI_ESDE_NOTE] + text = [str(line).strip() for line in install.tui_view(state, 40, 12)] + self.assertIn(install.TUI_ESDE_SHORT, text) + self.assertFalse(any(line.startswith("ES-DE found") for line in text), text) + text = " ".join(" ".join(install.tui_view(state, 80, 24)).split()) + self.assertIn(install.TUI_ESDE_NOTE, text) + + def test_the_40x12_plan_keeps_its_folder_and_version_note(self): + """tui_audit_14 P3-6: the plan named ~/.var/app/ as the folder, and + the version note was absent.""" + state = checked_state() + state.dest = HOME / ".var/app/org.libretro.RetroArch/config/retroarch/system" + state.version = "This list was built for RetroBat 8.3.1, this machine runs 8.2.0.1." + lines = [str(line).strip() for line in install.tui_view(state, 40, 12)] + joined = "".join(line for line in lines if line.endswith("/") or "system" in line) + self.assertIn("~/.var/app/org.libretro.RetroArch/config/retroarch/system", joined) + self.assertIn(state.version, " ".join(" ".join(lines).split())) + self.assertTrue(any(re.fullmatch(r"1-\d+ of \d+", line) for line in lines), lines) + self.assertIn("> Install", lines) + + def test_one_count_said_once_in_both_modes(self): + """tui_audit_14 P3-10: "22 files installed." over "22 files copied + from packx.", and line mode's Location, wrong hash and zero count.""" + state = _installed_state(SYSTEM_DIR) + state.summary["installed"] = 5 + state.finished = [(f"f{n}.bin", True, "") for n in range(5)] + state.local_dests = {f"f{n}.bin": ("/media/packx", "") for n in range(5)} + _title, lines = install.tui_outcome(state) + self.assertEqual(lines[0], "5 files copied from packx.") + self.assertNotIn("5 files installed.", lines) + 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") + 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") + self.assertNotIn('say(f"Location', source) + self.assertNotIn("already up to date.\")\n", source.split("\nDone.", 1)[-1][:300]) + + def test_quit_names_stay_once(self): + """tui_audit_14 P3-11: "A/Enter Stay B/Esc Stay".""" + state = fresh_state() + state.stack.append("quit") + state.focus["quit"] = 1 + self.assertEqual(str(install.tui_view(state, 80, 24)[-1]).count("Stay"), 1) + + def test_the_yes_stop_example_keeps_yes(self): + """tui_audit_14 P3-12: a script copying the example stopped at a + question.""" + files = [self.entry("gba_bios.bin", b"gba", ["nintendo-gba"])] + listing = self.listing("retroarch", files) + proc = install_run("--yes", "--manifest", str(listing), "--offline", + env={"HOME": str(self.root / "home"), "RETROBIOS_OS": "linux"}) + example = next(line for line in proc.stderr.splitlines() + if "--platform retroarch --dest" in line) + self.assertIn(" --yes", example) + + def test_the_systems_question_fits_the_terminal(self): + """code_audit_21 P3-9: 53 of 66 rows wider than 80 columns.""" + rows = [(f"system-{n}", [{"dest": f"{n}.bin", "size": 10 ** n}]) for n in range(6)] + labels = {"system-0": "Sega Dreamcast-based arcade boards, each " + "them sega-dreamcast-arcade", + **{f"system-{n}": f"Short {n}" for n in range(1, 6)}} + with mock.patch.object(install.shutil, "get_terminal_size", + return_value=os.terminal_size((80, 24))), \ + mock.patch.object(install, "_ask", return_value=""), \ + contextlib.redirect_stdout(io.StringIO()) as out: + install._pick("Systems:", rows, labels=labels) + printed = out.getvalue().splitlines() + self.assertTrue(all(install.cell_width(line) <= 80 for line in printed), printed) + # The long label on its own line, its size under it in the column. + at = next(i for i, line in enumerate(printed) if "sega-dreamcast-arcade" in line) + self.assertNotIn("B", printed[at].split("arcade", 1)[1]) + sizes = {line.index("1 file") for line in printed[at + 1:] if "1 file" in line} + self.assertEqual(len(sizes), 1, printed) + + def test_the_save_dir_sentence_is_written_once(self): + """code_audit_21 P3-10.""" + source = (REPO_ROOT / "install.py").read_text(encoding="utf-8") + self.assertEqual(source.count('"Name it with --save-dir next time."'), 1) + self.assertEqual(source.count("args.declared_only or len(files) + game_data_cut"), 1) + def test_a_missing_folder_does_not_exist_yet_on_every_row(self): """code_audit_21 P3-7: line mode said "(created)" of folders nobody created, and the screen said "does not exist yet" of the Windows row diff --git a/wiki/first-install.md b/wiki/first-install.md index 37bd962c5..14413381e 100644 --- a/wiki/first-install.md +++ b/wiki/first-install.md @@ -126,7 +126,7 @@ curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --check All good: ``` - 109/109 present (109 verified, 0 wrong hash) + 109 in place, 0 missing, 0 wrong copies ``` ## Undo diff --git a/wiki/installer.md b/wiki/installer.md index e506ba3f9..6ef13fd26 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -218,9 +218,15 @@ 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 -or a command on the result is never split inside a word: one longer than the -window takes a line of its own, which the terminal folds, and a command breaks -only between its arguments. Every path a run names is spelled the way its BIOS +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 +on one line, the path keeps its last folders after `.../`, the line under it +says it was shortened, and the terminal shows it whole after the screen +closes. On Android and on a handheld route the result does not mention Shift, +which neither has. Every path a run names is spelled the way its BIOS folder was named, the backup folder and the save directory included (`/sdcard/RetroArch` on Android, not the `/storage/emulated/0` it links to). @@ -385,12 +391,12 @@ A RetroArch dry run into an empty folder printed this on 2026-10-27: ``` Fetching file index for retroarch... 106 files read from RetroArch's save directory go to a folder per core under /home/me/.config/retroarch/saves - 8358 files (6.6 GB) - 3269 BIOS files (4.1 GB) and 5089 game data files (2.5 GB). + 10994 files (6.6 GB) + 3269 BIOS files (4.1 GB), 5089 game data files (2.5 GB) and 2636 data files (38.5 MB). Not installable: 37 files (1 required, not found: 37). Checking existing files... - 0/8358 present (0 verified, 0 wrong hash) + 0 in place, 10994 missing, 0 wrong copies ``` The second line divides the first, and every later count of the run (the @@ -445,13 +451,13 @@ Fetching file index for batocera... Not installable: 399 files (79 required, not found: 399). Checking existing files... - 0/14481 present (0 verified, 0 wrong hash) + 0 in place, 14481 missing, 0 wrong copies ``` `Not installable` counts the files of the list that the collection does not hold. They are named at the end of the run. -On a first install the present count is zero. On a later run it counts what +On a first install nothing is in place. On a later run the line counts what is already there, and only the rest is downloaded. A download of 1 GB or more is confirmed first (`Download 9.1 GB to /userdata/bios? [Y/n]`). @@ -470,8 +476,8 @@ command again picks up where it stopped. ### The end of the run ``` -Done. 14481 files installed, 0 files already up to date. -Location: /userdata/bios +Done. 14481 files installed. +BIOS folder: /userdata/bios 399 files were not installed: the collection does not hold them, or its copy fails their check. Needed to start an emulator (79 files): dark-forces (theforceengine): DARK.GOB, SOUNDS.GOB, SPRITES.GOB, TEXTURES.GOB @@ -502,11 +508,11 @@ When some files came from this machine rather than the network, a line under the wrong place in the BIOS folder, 4 downloaded.`). See [Copy from an old card](#copy-from-an-old-card). -The word `Done` and a location are the confirmation. Any other ending +The word `Done` and the BIOS folder are the confirmation. Any other ending (`Stopped`, `Finished with errors`, `Nothing was installed`) means something was left undone, and the run says which case it was. -The lines between `Location:` and the `--check` hint name the files of the +The lines between `BIOS folder:` and the `--check` hint name the files of the list that the collection does not hold. `Needed to start an emulator` gives, for each system, the emulators that read it and the files they cannot start without. On the run above, all 79 belong to games (Dark Forces, Diablo, Doom 3, @@ -520,14 +526,14 @@ The same command with `--check` reads the folder and writes nothing: ``` Checking existing files... - 14481/14481 present (14481 verified, 0 wrong hash) + 14481 in place, 0 missing, 0 wrong copies All files up to date. ``` The files the collection does not hold are listed after it again, as long as -they are absent from the folder. A number before `wrong hash` means a file is -present with contents the platform does not accept. Running the install again +they are absent from the folder. A number before `wrong copies` means a file is +in place with contents the platform does not accept. Running the install again replaces it and keeps the old one, see [Files you already had](#files-you-already-had). @@ -671,7 +677,7 @@ downloaded as usual, or taken from the pack with `--from-pack`. Several file came from: ``` -Done. 44 files installed, 0 files already up to date. +Done. 44 files installed. 38 files copied from E:\, 4 downloaded, 2 still missing. ``` @@ -697,7 +703,7 @@ same choice comes as questions after detection. A RetroArch run with `--plain` printed this on 2026-10-28, `g` then pressed: ``` -RetroArch: 8358 files, 6.6 GB, and 2636 data files (38.5 MB). +RetroArch: 10994 files, 6.6 GB. 3269 BIOS files (4.1 GB), 5089 game data files (2.5 GB) and 2636 data files (38.5 MB). Game data: files a game or a game engine reads, of no use to the 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. BIOS folder: /home/me/.config/retroarch/system @@ -710,7 +716,7 @@ RetroArch: 8358 files, 6.6 GB, and 2636 data files (38.5 MB). > Counted again for your choices. Nothing is written before Enter. -RetroArch: 3269 files, 4.1 GB, and 2636 data files (38.5 MB). +RetroArch: 5905 files, 4.1 GB. 3269 BIOS files (4.1 GB) and 2636 data files (38.5 MB). 5089 game data files left out. ``` @@ -1175,7 +1181,13 @@ muOS, ArkOS and the others) opens the setup list instead, filtered to the setups that read it, RetroArch first: Enter on it asks RetroArch's folder. At that question a drive is numbered by the folder its system reads (`ONION: BIOS`), a drive nothing names by its `bios` folder (`EASYROMS: bios`), never -by its root, which no setup reads. Another system's card or SHARE drive, and a +by its root, which no setup reads. A drive no file names that holds a `bios` +folder and console folders (`nes`, `gba`, `psx`) is a handheld's card whose +own files sit on a part a computer does not open, ArkOS's `EASYROMS` and +AmberELEC's `GAMES`: with nothing found here the first screen and line mode +say `A card is in the reader (EASYROMS): choose its system`, and its `bios` +folder comes first at the folder question, under Enter, before this +computer's usual folder. Another system's card or SHARE drive, and a drive two systems fit, are left out, and a line names them: each is reached through its own setup. A folder typed is read from the current one when it is not a full path, and named in full after. `q` quits there too diff --git a/wiki/troubleshooting.md b/wiki/troubleshooting.md index 9071ec742..96309059f 100644 --- a/wiki/troubleshooting.md +++ b/wiki/troubleshooting.md @@ -28,13 +28,13 @@ curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --check Read the count line: ``` - 1/109 present (0 verified, 1 wrong hash) + 0 in place, 108 missing, 1 wrong copy ``` -- **`wrong hash`**: another region or model, or damaged. -- **Fewer present than the total**: files are missing. +- **`wrong copy`**: another region or model, or damaged. +- **A number before `missing`**: files are missing. - **A line under a file**, like `still zipped`, says the fix. -- **`0 wrong hash`, all present**: go to step 3. +- **`0 missing, 0 wrong copies`**: go to step 3. ## 2. Fix the files