mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
chore: merge one tally for every count
This commit is contained in:
commit
4408716adc
7 files changed
+354
-119
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "7998082004788f8d97177492ec371d8bdc507c5f132983ae05dc43be82d7e62a"
|
||||
$defaultInstallSha256 = "aba3edfbad94300545c951a5b7fd78ce7ce56c1640930c9291be69cbc5e34e02"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+197
-92
@@ -51,7 +51,7 @@ import weakref
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
import zlib
|
||||
from collections.abc import Callable, Collection, Iterator, Mapping
|
||||
from collections.abc import Callable, Collection, Iterable, Iterator, Mapping
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any, BinaryIO, ContextManager, NamedTuple, TextIO
|
||||
|
||||
@@ -508,6 +508,8 @@ class Progress:
|
||||
# Files written to their place, which a stopped run counts: the
|
||||
# queue's length said "118 files installed" over 8 on disk.
|
||||
self.installed = 0
|
||||
# The entries of those files, which the end of the run counts apart.
|
||||
self.landed: list[dict] = []
|
||||
self.start = time.monotonic()
|
||||
self._drawn = 0.0
|
||||
self._evented = self.start
|
||||
@@ -862,30 +864,60 @@ def leave_out_game_data(
|
||||
)
|
||||
|
||||
|
||||
def category_split(entries: list[dict]) -> tuple[int, int, int, int]:
|
||||
"""(BIOS files, their bytes, game data files, their bytes) of entries,
|
||||
each with its size and the category the list gives it."""
|
||||
games = [entry for entry in entries if entry.get("category") == GAME_DATA]
|
||||
bios = [entry for entry in entries if entry.get("category") != GAME_DATA]
|
||||
return (len(bios), sum(int(entry.get("size") or 0) for entry in bios),
|
||||
len(games), sum(int(entry.get("size") or 0) for entry in games))
|
||||
class Tally(NamedTuple):
|
||||
"""Files and bytes of one list, BIOS files and game data apart. bios
|
||||
and games add up to files, their bytes to size: every count a run
|
||||
prints of a list comes from here."""
|
||||
|
||||
files: int
|
||||
size: int
|
||||
bios: int
|
||||
bios_size: int
|
||||
games: int
|
||||
game_size: int
|
||||
|
||||
|
||||
def kinds_words(bios: int, games: int) -> str:
|
||||
"""BIOS files and game data files, counted apart."""
|
||||
return f"{_plural(bios, 'BIOS file')} and {_plural(games, 'game data file')}"
|
||||
def tally(entries: Iterable[dict]) -> Tally:
|
||||
"""The counts of entries, each with its size and the category the list
|
||||
gives it, anything not game data being a BIOS file.
|
||||
|
||||
The run said 8069 files (6.5 GB) while its menu said 8348 (7.1 GB) and
|
||||
3259 BIOS files and 5089 game data files: three counts of three lists.
|
||||
"""
|
||||
bios = bios_size = games = game_size = 0
|
||||
for entry in entries:
|
||||
size = int(entry.get("size") or 0)
|
||||
if entry.get("category") == GAME_DATA:
|
||||
games += 1
|
||||
game_size += size
|
||||
else:
|
||||
bios += 1
|
||||
bios_size += size
|
||||
return Tally(bios + games, bios_size + game_size, bios, bios_size, games, game_size)
|
||||
|
||||
|
||||
def split_words(entries: list[dict]) -> str:
|
||||
def sized_words(counts: Tally) -> str:
|
||||
"""The count and the size, as in 8358 files, 6.6 GB."""
|
||||
return f"{_plural(counts.files, 'file')}, {format_size(counts.size)}"
|
||||
|
||||
|
||||
def kinds_words(bios: int, games: int, data: int = 0) -> str:
|
||||
"""BIOS files and game data files, counted apart, and the files of the
|
||||
data trees when some were installed."""
|
||||
parts = [_plural(bios, "BIOS file"), _plural(games, "game data file")]
|
||||
if data:
|
||||
parts.append(_plural(data, "data file"))
|
||||
return f"{', '.join(parts[:-1])} and {parts[-1]}"
|
||||
|
||||
|
||||
def split_words(counts: Tally) -> str:
|
||||
"""BIOS files and game data apart, in words, "" when there is no game
|
||||
data: the installer tells them apart (--no-game-data), and no count
|
||||
showed it. RetroArch's list is 3259 BIOS files (4.4 GB) and 5089 game
|
||||
data files (2.7 GB)."""
|
||||
bios, bios_bytes, games, game_bytes = category_split(entries)
|
||||
if not games:
|
||||
showed it. Both parts come from the count they divide."""
|
||||
if not counts.games:
|
||||
return ""
|
||||
return (f"{_plural(bios, 'BIOS file')} ({format_size(bios_bytes)}) and "
|
||||
f"{_plural(games, 'game data file')} ({format_size(game_bytes)})")
|
||||
return (f"{_plural(counts.bios, 'BIOS file')} ({format_size(counts.bios_size)}) and "
|
||||
f"{_plural(counts.games, 'game data file')} ({format_size(counts.game_size)})")
|
||||
|
||||
|
||||
def keep_declared(files: list[dict], omitted: list[dict]) -> tuple[list[dict], list[dict]]:
|
||||
@@ -932,13 +964,19 @@ def declared_copy(entry: dict) -> dict:
|
||||
def _left_out_lines(left_out: list[dict], every: bool) -> list[str]:
|
||||
"""What --no-game-data left out, named per category, ten each unless every."""
|
||||
lines = []
|
||||
# The omitted ones apart: "leave out the 5089 game data files" was
|
||||
# followed by "Left out as game data: 5099 files", the other ten being
|
||||
# files the list cannot install anyway.
|
||||
for group in _left_out_events(left_out):
|
||||
dests = group["dests"]
|
||||
shown = dests if every else dests[:10]
|
||||
more = (f" ... and {len(dests) - len(shown)} more (--verbose lists them all)"
|
||||
if len(shown) < len(dests) else "")
|
||||
omitted = sum(1 for entry in left_out if entry.get("reason")
|
||||
and entry.get("category", "") == group["category"])
|
||||
aside = f", {omitted} of them not installable" if omitted else ""
|
||||
lines.append(f" Left out as {group['label']} (--no-game-data): "
|
||||
f"{_plural(len(dests), 'file')}: {', '.join(shown)}{more}")
|
||||
f"{_plural(len(dests), 'file')}{aside}: {', '.join(shown)}{more}")
|
||||
return lines
|
||||
|
||||
|
||||
@@ -5737,12 +5775,13 @@ def _report_outcome(
|
||||
hints: list[str] | None = None,
|
||||
sources: str = "",
|
||||
offline: int = 0,
|
||||
kinds: list[int] | None = None,
|
||||
kinds: tuple[Tally, int] | None = None,
|
||||
same_choice: str = "",
|
||||
term: Terminal | None = None,
|
||||
) -> None:
|
||||
"""Close the run on what happened, and leave with a status that says so.
|
||||
kinds is [BIOS files, game data files] installed, said apart.
|
||||
kinds is the count of the files installed (tally) and of the data
|
||||
tree files, given for a list marking its game data, said apart.
|
||||
|
||||
A run that installed nothing used to end on the word Done and a zero exit,
|
||||
which reads as success to a person and to a script alike. offline counts
|
||||
@@ -5884,8 +5923,8 @@ def _report_outcome(
|
||||
|
||||
say(f"\nDone. {_plural(downloaded, 'file')} installed, "
|
||||
f"{_plural(up_to_date, 'file')} already up to date.")
|
||||
if kinds and kinds[1]:
|
||||
say(f" Installed: {kinds_words(*kinds)}.")
|
||||
if kinds and kinds[0].games:
|
||||
say(f" Installed: {kinds_words(kinds[0].bios, kinds[0].games, kinds[1])}.")
|
||||
if sources:
|
||||
say(f" {sources}")
|
||||
say(f"Location: {_locations(paths)}")
|
||||
@@ -6074,15 +6113,15 @@ def _print_plan(
|
||||
total out of sight (599 lines for RetroArch): it gives way to the
|
||||
largest systems, and --verbose lists every file.
|
||||
"""
|
||||
total = sum(f.get("size", 0) for f in to_download)
|
||||
counts = tally(to_download)
|
||||
trees = [(entry, needed) for entry, needed in trees if needed]
|
||||
tree_files = sum(len(needed) for _entry, needed in trees)
|
||||
tree_size = sum(meta.get("size", 0) for _entry, needed in trees for meta in needed.values())
|
||||
tree_counts = tally(meta for _entry, needed in trees for meta in needed.values())
|
||||
tree_files = tree_counts.files
|
||||
parts = []
|
||||
if to_download:
|
||||
parts.append(f"{_plural(len(to_download), 'file')} ({format_size(total)})")
|
||||
parts.append(f"{_plural(counts.files, 'file')} ({format_size(counts.size)})")
|
||||
if trees:
|
||||
parts.append(f"{_plural(tree_files, 'data file')} ({format_size(tree_size)}, "
|
||||
parts.append(f"{_plural(tree_files, 'data file')} ({format_size(tree_counts.size)}, "
|
||||
f"extracted from {_plural(len(trees), 'archive')})")
|
||||
# A pack is copied from, not downloaded: "Would download 23 files" was
|
||||
# said of a run that makes no network request.
|
||||
@@ -6255,10 +6294,11 @@ def download_files(
|
||||
print("\n Stopping. Files already installed are kept.")
|
||||
# The pool has run every future by now: a file that landed after Ctrl+C
|
||||
# is on disk and counts.
|
||||
progress.installed = sum(
|
||||
1 for future in future_map
|
||||
progress.landed = [
|
||||
entry for future, entry in future_map.items()
|
||||
if not future.cancelled() and future.exception() is None and future.result()[1]
|
||||
)
|
||||
]
|
||||
progress.installed = len(progress.landed)
|
||||
progress.finish()
|
||||
progress.emit_transfer()
|
||||
return failed, interrupted
|
||||
@@ -7226,7 +7266,7 @@ def _quit() -> None:
|
||||
|
||||
|
||||
def _sized(files: list[dict]) -> str:
|
||||
return f"{_plural(len(files), 'file')}, {format_size(sum(f.get('size', 0) for f in files))}"
|
||||
return sized_words(tally(files))
|
||||
|
||||
|
||||
# A core whose files are all the platform's own. "pcsx_rearmed 0 files,
|
||||
@@ -7490,10 +7530,12 @@ def _prompt_custom_selection(
|
||||
current = given
|
||||
toggled = False
|
||||
while True:
|
||||
cutting = bool(cut and cut[0])
|
||||
shown, shown_omitted = ((leave_out_game_data(files, omitted)[:2])
|
||||
if cut and cut[0] else (files, omitted))
|
||||
if cutting else (files, omitted))
|
||||
_print_menu_head(plat, shown, shown_omitted, installed, name, trees, bios_path,
|
||||
current, object() if toggled else given, manifest, narrowed)
|
||||
current, object() if toggled else given, manifest, narrowed,
|
||||
menu_choice(files, omitted, installed, current, cutting))
|
||||
if (current is not given or toggled) and command is not None:
|
||||
print(f" Same choice without questions: {screen_text(command(current))}")
|
||||
offline = _offline_only()
|
||||
@@ -7528,15 +7570,20 @@ def _prompt_custom_selection(
|
||||
copying = not dry_run
|
||||
if copying:
|
||||
print(" f) copy from a folder or an old card")
|
||||
if cut is not None:
|
||||
games = [entry for entry in files if entry.get("category") == GAME_DATA]
|
||||
# The game data of the choice the head counts, which g takes out of
|
||||
# it or puts back: counted over the whole list, it said 5089 files
|
||||
# under a head counting 12 for one system. Not offered for a
|
||||
# choice that holds none.
|
||||
games = tally(menu_choice(files, omitted, installed, current, False)[0]
|
||||
if cut is not None else [])
|
||||
if cut is not None and games.games:
|
||||
print(f" g) {'keep' if cut[0] else 'leave out'} the "
|
||||
f"{_plural(len(games), 'game data file')} ({format_size(_bytes(games))}), "
|
||||
f"{_plural(games.games, 'game data file')} ({format_size(games.game_size)}), "
|
||||
f"{GAME_DATA_WORDS}")
|
||||
print(" q) quit")
|
||||
keys = ["", "q", *(["c"] if choosable else []),
|
||||
*(["i"] if installed is not None and current != INSTALLED_ONLY else []),
|
||||
*(["f"] if copying else []), *(["g"] if cut is not None else [])]
|
||||
*(["f"] if copying else []), *(["g"] if games.games else [])]
|
||||
answer = _ask("> ").lower()
|
||||
while answer not in keys:
|
||||
print(f" Type {', '.join(['Enter', *keys[2:]])} or q.")
|
||||
@@ -7545,7 +7592,7 @@ def _prompt_custom_selection(
|
||||
return current
|
||||
if answer == "q":
|
||||
_quit()
|
||||
if answer == "g" and cut is not None:
|
||||
if answer == "g" and cut is not None and games.games:
|
||||
cut[0] = not cut[0]
|
||||
toggled = True
|
||||
elif answer == "i":
|
||||
@@ -7560,15 +7607,13 @@ def _print_menu_head(
|
||||
plat: str, files: list[dict], omitted: list[dict],
|
||||
installed: tuple[list[dict], list[dict]] | None, name: str, trees: list[dict],
|
||||
bios_path: Path | None, current: object, given: object, manifest: dict | None,
|
||||
narrowed: bool,
|
||||
narrowed: bool, chosen: tuple[list[dict], list[dict]] | None = None,
|
||||
) -> None:
|
||||
"""The plan above the line menu: what the choice holds, its folder, what
|
||||
stands in its way and where the save files go, as the screen's plan."""
|
||||
shown, kept_omitted = files, omitted
|
||||
if current == INSTALLED_ONLY and installed is not None:
|
||||
shown, kept_omitted = installed
|
||||
elif isinstance(current, tuple):
|
||||
shown, kept_omitted = narrow(files, omitted, *current)
|
||||
stands in its way and where the save files go, as the screen's plan.
|
||||
chosen is what the choice keeps (menu_choice), the list every count of
|
||||
the head is taken from."""
|
||||
shown, kept_omitted = chosen or menu_choice(files, omitted, installed, current, False)
|
||||
# The trees of the systems kept: 2636 data files of blueMSX, Dolphin
|
||||
# and PPSSPP were counted for every --system tried.
|
||||
kept_trees = select_data_directories({"data_directories": trees}, shown,
|
||||
@@ -7591,10 +7636,11 @@ def _print_menu_head(
|
||||
named = "" if named == ", " else named
|
||||
if current is not given:
|
||||
print("\n Counted again for your choices. Nothing is written before Enter.")
|
||||
print(f"\n{name or display_name(plat)}: {_sized(shown)}"
|
||||
counts = tally(shown)
|
||||
print(f"\n{name or display_name(plat)}: {sized_words(counts)}"
|
||||
+ (f", and {_plural(data, 'data file')} ({format_size(data_size)})" if data else "")
|
||||
+ named + ".")
|
||||
split = split_words(shown) if (manifest or {}).get("categorized") else ""
|
||||
split = split_words(counts) if (manifest or {}).get("categorized") else ""
|
||||
if split:
|
||||
print(f" {split}.")
|
||||
if bios_path is None:
|
||||
@@ -7611,6 +7657,21 @@ def _print_menu_head(
|
||||
print(f" {line}")
|
||||
|
||||
|
||||
def menu_choice(
|
||||
files: list[dict], omitted: list[dict],
|
||||
installed: tuple[list[dict], list[dict]] | None, current: object, cut: bool,
|
||||
) -> tuple[list[dict], list[dict]]:
|
||||
"""The files and omissions a choice at the line menu keeps, in the order
|
||||
the run applies it: the systems, cores and regions chosen, or what the
|
||||
installed cores read (i), then the game data left out (g)."""
|
||||
kept, kept_omitted = files, omitted
|
||||
if current == INSTALLED_ONLY and installed is not None:
|
||||
kept, kept_omitted = installed
|
||||
elif isinstance(current, tuple):
|
||||
kept, kept_omitted = narrow(files, omitted, *current)
|
||||
return leave_out_game_data(kept, kept_omitted)[:2] if cut else (kept, kept_omitted)
|
||||
|
||||
|
||||
def narrowing_words(files: list[dict], omitted: list[dict]) -> str:
|
||||
"""What c chooses among for these files, in words, "" for nothing: a
|
||||
question with one answer is not asked (tui_audit_11 P3-10)."""
|
||||
@@ -11881,8 +11942,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
failures: list[tuple[str, str]] = []
|
||||
installed_paths: list[Path] = []
|
||||
bulk_lines: list[str] = []
|
||||
# BIOS files and game data files installed, of a list marking them.
|
||||
kinds_done = [0, 0]
|
||||
# The entries installed and the data tree files: the end line counts
|
||||
# BIOS files and game data among them, the data files apart.
|
||||
landed: list[dict] = []
|
||||
data_landed = 0
|
||||
categorized = False
|
||||
# The BIOS folders read for misplaced files, one per platform.
|
||||
bios_indexes: list[FolderIndex] = []
|
||||
|
||||
@@ -11942,6 +12006,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
else:
|
||||
print(f"\nFetching file index for {plat_name}...")
|
||||
manifest = fetch_manifest(plat_name)
|
||||
categorized = categorized or bool(manifest.get("categorized"))
|
||||
files = manifest.get("files", [])
|
||||
omitted_files = manifest.get("omitted_files", [])
|
||||
variants = manifest.get("case_variants", [])
|
||||
@@ -12089,6 +12154,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
# What declining the size question points back at, when it was asked.
|
||||
smaller = ""
|
||||
given = selection
|
||||
# g at the menu: whether it left the game data out.
|
||||
cut: list[bool] | None = None
|
||||
menu = interactive and not args.check and not listing
|
||||
if menu:
|
||||
installed = (
|
||||
@@ -12137,19 +12204,21 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
_INVOCATION.chosen[plat_name] = chosen_flags(selection)
|
||||
else:
|
||||
_INVOCATION.chosen.pop(plat_name, None)
|
||||
if cut and cut[0]:
|
||||
# Left out at g, as --no-game-data leaves it out.
|
||||
files, omitted_files, left_out = leave_out_game_data(files, omitted_files)
|
||||
left_out_seen.extend(left_out)
|
||||
_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)
|
||||
if selection is not None:
|
||||
before = len(files)
|
||||
files, omitted_files = narrow(files, omitted_files, *selection)
|
||||
unplaced = narrow([], unplaced, *selection)[1]
|
||||
print(f" Narrowed {before} to {len(files)} files")
|
||||
if cut and cut[0]:
|
||||
# Left out at g, as --no-game-data leaves it out, from what the
|
||||
# choice kept: g offered the game data of the systems chosen,
|
||||
# and the line below counted the whole list's.
|
||||
files, omitted_files, left_out = leave_out_game_data(files, omitted_files)
|
||||
left_out_seen.extend(left_out)
|
||||
_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)
|
||||
if unplaced:
|
||||
total_omitted += len(unplaced)
|
||||
omitted_seen.extend(unplaced)
|
||||
@@ -12159,8 +12228,13 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
_print_choices(plat_name, files, omitted_files, args.list_systems, manifest)
|
||||
continue
|
||||
|
||||
total_size = sum(f.get("size", 0) for f in files)
|
||||
print(f" {len(files)} files ({format_size(total_size)})")
|
||||
# The count every later line of the platform divides: the files
|
||||
# checked, planned, offered and installed are these.
|
||||
run_counts = tally(files)
|
||||
print(f" {_plural(run_counts.files, 'file')} ({format_size(run_counts.size)})")
|
||||
split = split_words(run_counts) if manifest.get("categorized") else ""
|
||||
if split:
|
||||
print(f" {split}.")
|
||||
if omitted_files:
|
||||
required_omitted = sum(
|
||||
1 for entry in omitted_files if entry.get("required", True)
|
||||
@@ -12205,7 +12279,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
)
|
||||
present = len(up_to_date) + len(mismatched)
|
||||
print(
|
||||
f" {present}/{len(files)} present "
|
||||
f" {present}/{run_counts.files} present "
|
||||
f"({len(up_to_date)} verified, {len(mismatched)} wrong hash)"
|
||||
)
|
||||
|
||||
@@ -12421,13 +12495,10 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
)
|
||||
bulk_lines += hint
|
||||
total_downloaded += progress.installed
|
||||
if not interrupted and manifest.get("categorized"):
|
||||
# The end line tells BIOS files from game data, as the
|
||||
# menu counted them.
|
||||
gone = [str(dest) for dest, _reason in failed]
|
||||
for entry in to_download:
|
||||
if not any(dest.endswith(str(entry.get("dest", ""))) for dest in gone):
|
||||
kinds_done[entry.get("category") == GAME_DATA] += 1
|
||||
# The end line tells BIOS files from game data, as the menu
|
||||
# counted them, over the files that landed: matched by the end
|
||||
# of a failed path, a.bin was dropped when x/a.bin failed.
|
||||
landed.extend(progress.landed)
|
||||
files_pending += len(to_download) - progress.installed if interrupted \
|
||||
else len(failed)
|
||||
total_errors += len(failed)
|
||||
@@ -12450,6 +12521,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
for entry in data_dirs:
|
||||
counts = install_data_directory(entry, bios_path)
|
||||
total_downloaded += counts["installed"]
|
||||
data_landed += counts["installed"]
|
||||
total_up_to_date += counts["present"]
|
||||
if isinstance(counts["error"], OfflineReason):
|
||||
# Left out as the BIOS files above are: counted missing,
|
||||
@@ -12541,7 +12613,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
omitted_entries=omitted_seen,
|
||||
platform_of={path: plat for plat, path in platforms},
|
||||
hints=bulk_lines,
|
||||
kinds=kinds_done,
|
||||
kinds=((tally(landed), data_landed) if categorized else None),
|
||||
same_choice=(_INVOCATION.command(*platforms[0])
|
||||
if interactive and len(platforms) == 1 else ""),
|
||||
sources=_run_sources(bios_indexes, total_downloaded,
|
||||
@@ -13821,32 +13893,62 @@ def _plan_rows(state: TuiState) -> list[TuiRow]:
|
||||
|
||||
def _tui_categories(state: TuiState) -> dict[str, str]:
|
||||
"""Each destination's category, from the list the screen holds, for a
|
||||
list that marks its game data."""
|
||||
list that marks its game data. The second spellings a folder that keeps
|
||||
case installs are in it too: counted as nothing, the result's BIOS and
|
||||
game data fell short of its total."""
|
||||
manifest = state.manifest or {}
|
||||
if not manifest.get("categorized"):
|
||||
return {}
|
||||
return {str(entry.get("dest")): str(entry.get("category") or "")
|
||||
for entry in manifest.get("files", [])}
|
||||
for entry in [*manifest.get("files", []), *manifest.get("case_variants", [])]}
|
||||
|
||||
|
||||
def _tui_split(state: TuiState, dests: list[tuple[str, int]]) -> str:
|
||||
"""BIOS files and game data apart, for (dest, size) pairs, "" when the
|
||||
list marks none or holds no game data among them."""
|
||||
def _tui_tally(state: TuiState, events: Iterable[dict]) -> Tally:
|
||||
"""The count of file events (dest and size), each with the category the
|
||||
list gives its destination: the one count the plan, its game data row
|
||||
and the result divide."""
|
||||
categories = _tui_categories(state)
|
||||
if not categories:
|
||||
return tally({"size": event["size"], "category": categories.get(event["dest"], "")}
|
||||
for event in events)
|
||||
|
||||
|
||||
def _tui_split(state: TuiState, events: Iterable[dict]) -> str:
|
||||
"""BIOS files and game data apart, for file events, "" when the list
|
||||
marks none or holds no game data among them."""
|
||||
if not _tui_categories(state):
|
||||
return ""
|
||||
return split_words([{"size": size, "category": categories.get(dest, "")}
|
||||
for dest, size in dests])
|
||||
return split_words(_tui_tally(state, events))
|
||||
|
||||
|
||||
def _tui_estimating(state: TuiState) -> bool:
|
||||
"""Whether the plan counts the systems ticked since the check, not the
|
||||
check itself."""
|
||||
return bool(_choice_line(state) and state.systems and state.check is not None
|
||||
and not _offline_only())
|
||||
|
||||
|
||||
def _plan_files(state: TuiState) -> list[dict]:
|
||||
"""The file events the plan's headline counts: the systems ticked when
|
||||
it estimates them, the check's own otherwise."""
|
||||
return _ticked_files(state) if _tui_estimating(state) else state.files
|
||||
|
||||
|
||||
def _game_data_row(state: TuiState) -> TuiRow | None:
|
||||
"""The plan's game data row, one press to leave it out or keep it, for
|
||||
a list that marks game data: inside Options alone, it went unseen."""
|
||||
games = [entry for entry in (state.manifest or {}).get("files", [])
|
||||
if entry.get("category") == GAME_DATA]
|
||||
if not _tui_categories(state) or not games:
|
||||
a list that marks game data: inside Options alone, it went unseen.
|
||||
|
||||
Counted over the files the plan counts, as g at the line menu: over
|
||||
the whole list, it said 5089 files beside a plan of 12. Once left out
|
||||
the check holds none, and the list's own game data is counted."""
|
||||
if not _tui_categories(state):
|
||||
return None
|
||||
sized = f"{_plural(len(games), 'file')}, {format_size(_bytes(games))}"
|
||||
games = _tui_tally(state, _plan_files(state))
|
||||
if not games.games:
|
||||
games = tally(entry for entry in (state.manifest or {}).get("files", [])
|
||||
if entry.get("category") == GAME_DATA)
|
||||
if not games.games:
|
||||
return None
|
||||
sized = f"{_plural(games.games, 'file')}, {format_size(games.game_size)}"
|
||||
return TuiRow("gamedata", f"Game data ({sized}): "
|
||||
+ ("left out, change" if state.no_game_data else "kept, leave out"))
|
||||
|
||||
@@ -16434,7 +16536,7 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple:
|
||||
check = state.check or {}
|
||||
lines = _plan_lines(check, state.dest, state)
|
||||
choice = _choice_line(state)
|
||||
if choice and state.systems and check is state.check and not _offline_only():
|
||||
if _tui_estimating(state):
|
||||
# The headline counts the systems ticked, as the list just did: at
|
||||
# 40x12 the line below it was dropped, and the plan showed the
|
||||
# total of all of them.
|
||||
@@ -16463,8 +16565,10 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple:
|
||||
lines[at:at] = [*([state.card_note] if state.card_note
|
||||
and state.card_note != state.status else []),
|
||||
*([f"{said[0].upper()}{said[1:]}."] if said else []), *choice]
|
||||
split = _tui_split(state, [(event["dest"], event["size"]) for event in state.files
|
||||
if event["state"] != "present"])
|
||||
# Of the files the headline counts: those ticked when it estimates
|
||||
# them, those to install otherwise.
|
||||
split = _tui_split(state, _ticked_files(state) if _tui_estimating(state) else [
|
||||
event for event in state.files if event["state"] != "present"])
|
||||
if split or (state.no_game_data and _game_data_row(state) is not None):
|
||||
# BIOS files and game data apart, and what game data is, beside the
|
||||
# row that leaves it out.
|
||||
@@ -16890,13 +16994,14 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]:
|
||||
title = "Done"
|
||||
head = "Everything is in place."
|
||||
lines = [head]
|
||||
categories = _tui_categories(state)
|
||||
done = [categories.get(dest, "") for dest, good, _why in state.finished
|
||||
if good and dest in categories]
|
||||
if GAME_DATA in done:
|
||||
# As the plan counted them, and as line mode ends.
|
||||
games = done.count(GAME_DATA)
|
||||
lines.append(f"Installed: {kinds_words(len(done) - games, games)}.")
|
||||
if _tui_categories(state) and title != "Stopped":
|
||||
# As the plan counted them, and as line mode ends: the data tree
|
||||
# files are the installed ones no file event names.
|
||||
done = _tui_tally(state, [{"dest": dest, "size": 0}
|
||||
for dest, good, _why in state.finished if good])
|
||||
if done.games:
|
||||
lines.append("Installed: " + kinds_words(
|
||||
done.bios, done.games, max(0, installed - done.files)) + ".")
|
||||
sources = _tui_sources(state, installed, len(failed) + left)
|
||||
if left and installed and sources:
|
||||
# One count, then the remedy, as line mode says it: the count came
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ esac
|
||||
TEMP_INSTALLER=""
|
||||
TEMP_DIRECTORY=""
|
||||
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
DEFAULT_INSTALL_SHA256="7998082004788f8d97177492ec371d8bdc507c5f132983ae05dc43be82d7e62a"
|
||||
DEFAULT_INSTALL_SHA256="aba3edfbad94300545c951a5b7fd78ce7ce56c1640930c9291be69cbc5e34e02"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 829229,
|
||||
"sha256": "7998082004788f8d97177492ec371d8bdc507c5f132983ae05dc43be82d7e62a",
|
||||
"size": 833938,
|
||||
"sha256": "aba3edfbad94300545c951a5b7fd78ce7ce56c1640930c9291be69cbc5e34e02",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 881778,
|
||||
"sha256": "7e5cc8505ce9c810888baf62260b7ac2ff1cfcbce2e13c44b99d15458969f38a"
|
||||
"size": 886487,
|
||||
"sha256": "2c0bfadad268416608e9ada4e5b77934ac997208bca9f6546a4827c165a131b8"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 829229,
|
||||
"sha256": "7998082004788f8d97177492ec371d8bdc507c5f132983ae05dc43be82d7e62a",
|
||||
"size": 833938,
|
||||
"sha256": "aba3edfbad94300545c951a5b7fd78ce7ce56c1640930c9291be69cbc5e34e02",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 829229,
|
||||
"sha256": "7998082004788f8d97177492ec371d8bdc507c5f132983ae05dc43be82d7e62a",
|
||||
"size": 833938,
|
||||
"sha256": "aba3edfbad94300545c951a5b7fd78ce7ce56c1640930c9291be69cbc5e34e02",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
+24
-2
@@ -98,8 +98,30 @@ class InstallerOutputExample(unittest.TestCase):
|
||||
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)
|
||||
menu = re.search(r"RetroArch: (\d+) files, ([\d.]+ [KMG]B)\.", text)
|
||||
self.assertEqual(menu.groups(), (count, size))
|
||||
self.assertSplitAddsUp(run, count, size)
|
||||
menu = re.search(r"RetroArch: (\d+) files, ([\d.]+ [KMG]B)[.,]([^`]*)", text)
|
||||
self.assertEqual(menu.groups()[:2], (count, size))
|
||||
self.assertSplitAddsUp(menu.group(3), count, size)
|
||||
games = re.search(r"g\) leave out the (\d+) game data files \(([\d.]+ [KMG]B)\)",
|
||||
menu.group(3))
|
||||
self.assertIsNotNone(games, "the menu offers to leave the game data out")
|
||||
split = re.search(r"(\d+) BIOS files \(([\d.]+ [KMG]B)\) and (\d+) game data files "
|
||||
r"\(([\d.]+ [KMG]B)\)", menu.group(3))
|
||||
self.assertEqual(games.groups(), split.groups()[2:])
|
||||
|
||||
def assertSplitAddsUp(self, text: str, count: str, size: str) -> None:
|
||||
"""The run counted 8069 files where its menu counted 3259 BIOS files
|
||||
and 5089 game data files: the parts add up to the total they divide,
|
||||
the sizes within the rounding of their three figures."""
|
||||
split = re.search(r"(\d+) BIOS files \(([\d.]+) ([KMG]B)\) and (\d+) game data "
|
||||
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))
|
||||
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])
|
||||
self.assertLessEqual(abs(added - float(total) * units[unit]), 0.15 * units[unit])
|
||||
|
||||
|
||||
class OneResolutionChain(unittest.TestCase):
|
||||
|
||||
+103
-3
@@ -7456,8 +7456,15 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings,
|
||||
*state.manifest["files"],
|
||||
{"dest": "doom/doom1.wad", "size": 30, "category": "game_data", "cores": None}]}
|
||||
state.finished = [("scph5500.bin", True, ""), ("doom/doom1.wad", True, "")]
|
||||
state.summary = {**state.summary, "installed": 2}
|
||||
_title, lines = install.tui_outcome(state)
|
||||
self.assertIn("Installed: 1 BIOS file and 1 game data file.", lines)
|
||||
# The data tree files are installed without a file event: the parts
|
||||
# still add up to the head's count.
|
||||
state.summary = {**state.summary, "installed": 5}
|
||||
_title, lines = install.tui_outcome(state)
|
||||
self.assertIn("5 files installed.", lines[0])
|
||||
self.assertIn("Installed: 1 BIOS file, 1 game data file and 3 data files.", lines)
|
||||
listing, _files = self.game_list()
|
||||
backup = self.root / "old"
|
||||
(backup / "doom").mkdir(parents=True)
|
||||
@@ -7476,6 +7483,98 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings,
|
||||
self.assertIn("Done. 2 files installed", real.getvalue())
|
||||
self.assertIn("Installed: 1 BIOS file and 1 game data file.", real.getvalue())
|
||||
|
||||
# 2026-10-27, CI run 40541886959: the run said 8069 files, the menu
|
||||
# 8348 and 3259 BIOS files and 5089 game data files, the real dry run
|
||||
# 8358. Every count of a run is one tally of one list.
|
||||
|
||||
def counted_list(self) -> Path:
|
||||
files = [self.entry("gba_bios.bin", b"gba", ["nintendo-gba"]),
|
||||
self.entry("doom/doom1.wad", b"wad" * 10, ["doom"], category="game_data"),
|
||||
self.entry("doom/doom2.wad", b"two" * 20, ["doom"], category="game_data")]
|
||||
variant = {**self.entry("DOOM/DOOM1.WAD", b"WAD" * 10, ["doom"]),
|
||||
"category": "game_data"}
|
||||
omitted = [{"dest": "doom/doom3.wad", "name": "doom3.wad", "system": "doom",
|
||||
"systems": ["doom"], "required": False, "reason": "not_found",
|
||||
"category": "game_data"}]
|
||||
return self.listing("retroarch", files, categorized=True, case_variants=[variant],
|
||||
omitted_files=omitted)
|
||||
|
||||
def test_every_count_of_a_line_run_is_one_list(self):
|
||||
args = dict(manifest=self.counted_list(), dest=self.here, declared_only=False,
|
||||
dry_run=True)
|
||||
text, _asked = self.line_run(_args(**args), [""])
|
||||
# The second spelling a folder keeping case installs is counted
|
||||
# everywhere or nowhere: here, everywhere.
|
||||
self.assertIn("RetroArch: 4 files, 123 B.\n 1 BIOS file (3 B) and 3 game data files "
|
||||
"(120 B).", text)
|
||||
self.assertIn("g) leave out the 3 game data files (120 B)", 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("4 files (123 B)", text[text.index("Checking existing files"):])
|
||||
|
||||
def test_g_counts_the_systems_chosen_and_leaves_out_what_it_said(self):
|
||||
args = dict(manifest=self.counted_list(), dest=self.here, declared_only=False,
|
||||
dry_run=True)
|
||||
text, _asked = self.line_run(_args(**args), ["c", "2", ""])
|
||||
after = text[text.index("Counted again"):]
|
||||
self.assertIn("RetroArch: 1 file, 3 B, for 1 of 2 systems.", after)
|
||||
# No game data among the systems chosen: nothing for g to leave out.
|
||||
self.assertNotIn("g) leave out", after)
|
||||
text, _asked = self.line_run(_args(**args), ["c", "1", "g", ""])
|
||||
after = text[text.index("Counted again"):]
|
||||
self.assertIn("g) leave out the 3 game data files (120 B)", after)
|
||||
last = after[after.rindex("Counted again"):]
|
||||
self.assertIn("g) keep the 3 game data files (120 B)", last)
|
||||
self.assertIn("Left out as game data (--no-game-data): 4 files, 1 of them not "
|
||||
"installable", text)
|
||||
|
||||
def test_the_parts_add_up_on_every_committed_list(self):
|
||||
for path in sorted((REPO_ROOT / "install").glob("*.json")):
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not manifest.get("categorized"):
|
||||
continue
|
||||
with self.subTest(path.name):
|
||||
for files in (manifest.get("files", []),
|
||||
manifest.get("files", []) + manifest.get("case_variants", [])):
|
||||
counts = install.tally(files)
|
||||
self.assertEqual(counts.bios + counts.games, counts.files)
|
||||
self.assertEqual(counts.files, len(files))
|
||||
self.assertEqual(counts.bios_size + counts.game_size, counts.size)
|
||||
self.assertEqual(counts.size, sum(e.get("size") or 0 for e in files))
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out):
|
||||
install._print_menu_head(
|
||||
manifest["platform"], manifest["files"], [], None, "", [], None,
|
||||
None, None, manifest, False)
|
||||
head = re.search(r"(\d+) files?, ", out.getvalue())
|
||||
split = re.search(r"(\d+) BIOS files? \(.*?\) and (\d+) game data files?",
|
||||
out.getvalue())
|
||||
if split:
|
||||
self.assertEqual(int(split[1]) + int(split[2]), int(head[1]))
|
||||
|
||||
def test_the_screen_divides_the_count_it_shows(self):
|
||||
state = checked_state()
|
||||
manifest = json.loads(json.dumps(state.manifest))
|
||||
manifest["case_variants"] = [{"dest": "PCSX/EXTRA.BIN", "size": 4,
|
||||
"category": "game_data", "cores": ["pcsx_rearmed"],
|
||||
"systems": ["sony-playstation"]}]
|
||||
for entry in manifest["files"]:
|
||||
if entry["dest"] == "pcsx/extra.bin":
|
||||
entry["category"] = "game_data"
|
||||
state.manifest = manifest
|
||||
state.files = [*state.files, {
|
||||
"event": "file", "platform": "retroarch", "dest": "PCSX/EXTRA.BIN",
|
||||
"systems": ["sony-playstation"], "size": 4, "state": "missing"}]
|
||||
state.check = {**state.check, "missing": state.check["missing"] + 1,
|
||||
"files": state.check["files"] + 1,
|
||||
"bytes_needed": state.check["bytes_needed"] + 4}
|
||||
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
|
||||
self.assertIn("5 files to download", text)
|
||||
self.assertIn("3 BIOS files (1.0 MB) and 2 game data files (8 B).", text)
|
||||
row = next(row for row in install.tui_rows(state) if row.action == "gamedata")
|
||||
self.assertIn("Game data (2 files, 8 B)", row.label)
|
||||
|
||||
# code_audit_18 P3-5, tui_audit_11 P3-10 and P3-21: line mode wording.
|
||||
|
||||
def test_one_row_says_type_1(self):
|
||||
@@ -8443,13 +8542,14 @@ class AuditSeventeenAndTen(unittest.TestCase):
|
||||
def test_the_docs_say_what_the_installer_does(self):
|
||||
"""code_audit_17 P3-7."""
|
||||
page = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
|
||||
menu = page[page.index("RetroArch: 8348 files, 7.1 GB."):]
|
||||
menu = page[re.search(r"RetroArch: \d+ files, [\d.]+ [KMG]B[.,]", page).start():]
|
||||
menu = menu[:menu.index("```")]
|
||||
self.assertIn("BIOS folder:", menu)
|
||||
self.assertIn("f) copy from a folder or an old card", menu)
|
||||
# The owner's request of 2026-10-27: game data counted apart, g.
|
||||
self.assertIn("3259 BIOS files (4.4 GB) and 5089 game data files (2.7 GB).", menu)
|
||||
self.assertIn("g) leave out the 5089 game data files", menu)
|
||||
# test_doc_pages checks that the figures add up.
|
||||
self.assertRegex(menu, r"\d+ BIOS files \([\d.]+ [KMG]B\) and \d+ game data files")
|
||||
self.assertRegex(menu, r"g\) leave out the \d+ game data files")
|
||||
env = page[page.index("## Environment overrides"):]
|
||||
self.assertIn("`TERMINFO`", env)
|
||||
self.assertIn("`TERMINFO_DIRS`", env)
|
||||
|
||||
+20
-12
@@ -307,17 +307,22 @@ names `~/Emulation/saves/retroarch/saves`. RetroPie, ROCKNIX, AmberELEC and
|
||||
EmuELEC keep saves beside each game, and RomM keeps them in the browser: their
|
||||
lists record these files as omitted.
|
||||
|
||||
A RetroArch run into an empty folder printed this on 2026-10-21:
|
||||
A RetroArch dry run into an empty folder printed this on 2026-10-27:
|
||||
|
||||
```
|
||||
Fetching file index for retroarch...
|
||||
8069 files (6.5 GB)
|
||||
Not installable: 11 files (1 required, not found: 11).
|
||||
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).
|
||||
Not installable: 37 files (1 required, not found: 37).
|
||||
|
||||
Checking existing files...
|
||||
0/8069 present (0 verified, 0 wrong hash)
|
||||
0/8358 present (0 verified, 0 wrong hash)
|
||||
```
|
||||
|
||||
The second line divides the first, and every later count of the run (the
|
||||
menu, the plan, `--dry-run`, the result) is taken from the same list.
|
||||
|
||||
When the manifest leaves entries out (a file the collection cannot supply, or
|
||||
one that failed its hash check), a `Not installable:` line under the index count
|
||||
says how many and why. `--check` and the closing report also name the required
|
||||
@@ -615,25 +620,28 @@ By default the installer fetches everything the platform runs. At a terminal,
|
||||
the plan screen of the [full-screen interface](#the-full-screen-interface)
|
||||
offers **Install**, **Choose systems** and **Options**. With `--plain`, or where
|
||||
the full screen cannot open (a terminal smaller than 40x12, `TERM=dumb`), the
|
||||
same choice comes as questions after detection:
|
||||
same choice comes as questions after detection. A RetroArch run with `--plain`
|
||||
printed this on 2026-10-27:
|
||||
|
||||
```
|
||||
RetroArch: 8348 files, 7.1 GB.
|
||||
3259 BIOS files (4.4 GB) and 5089 game data files (2.7 GB).
|
||||
RetroArch: 8358 files, 6.6 GB, and 2636 data files (38.5 MB).
|
||||
3269 BIOS files (4.1 GB) and 5089 game data files (2.5 GB).
|
||||
BIOS folder: /home/me/.config/retroarch/system
|
||||
106 files read from RetroArch's save directory go to a folder per core under /home/me/.config/retroarch/saves
|
||||
Enter) install everything
|
||||
i) only what the installed cores read: 100 files, 44.2 MB
|
||||
i) only what the installed cores read: 87 files, 43.8 MB
|
||||
c) choose systems, cores and regions
|
||||
f) copy from a folder or an old card
|
||||
g) leave out the 5089 game data files (2.7 GB), game data: files a game or a game engine reads, of no use to the other cores
|
||||
g) leave out the 5089 game data files (2.5 GB), game data: files a game or a game engine reads, of no use to the other cores
|
||||
q) quit
|
||||
```
|
||||
|
||||
A list that marks its game data counts it apart from the BIOS files, on the
|
||||
plan, in this menu and on the result (`Installed: 3259 BIOS files and 5089 game
|
||||
data files.`). `g` leaves it out, as `--no-game-data` does, and counts again
|
||||
plan, in this menu and on the result (`Installed: 3269 BIOS files, 5089 game
|
||||
data files and 2636 data files.`), the parts adding up to the total. `g` leaves
|
||||
the game data of the choice out, as `--no-game-data` does, and counts again
|
||||
(`g` then keeps it). The plan screen has the same choice as a row of its own,
|
||||
**Game data (5089 files, 2.7 GB): kept, leave out**, one press toggling it,
|
||||
**Game data (5089 files, 2.5 GB): kept, leave out**, one press toggling it,
|
||||
and Options keeps its **Game data** row too.
|
||||
|
||||
`i` appears for RetroArch, Lakka and RetroPie when cores are found installed,
|
||||
|
||||
Reference in new issue
Block a user