diff --git a/install.ps1 b/install.ps1 index d887d5509..5b2422aed 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 = "d3bfabb88e93d8ddf991965cd69929e73b0a254550f8f4c86a11ad34fe710a0b" + $defaultInstallSha256 = "e7df99e3ac458c7717bd2ca415bc7d3d8701545652cd303ed19de67077531ab2" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index c31c9288f..83b327f20 100644 --- a/install.py +++ b/install.py @@ -6625,6 +6625,8 @@ def installer_id() -> str: PLAN_FILES_PER_SYSTEM = 8 PLAN_LARGEST_SYSTEMS = 10 PLAN_FRAME_LINES = 6 +# Past this many systems a group sharing files is named by its first two. +PLAN_GROUP_TITLES = 3 # Answers that stop: every menu before this one taught q as quit. _REFUSALS = ("n", "q", "quit", "exit", "stop", "abort") @@ -6692,22 +6694,34 @@ def _print_plan( if not parts: print(f"\n Nothing to {verb}.") by_system: dict[str, list[dict]] = {} + titles: dict[str, str] = {} for entry in to_download: - system = (", ".join(system_title(name, manifest) for name in entry_systems(entry)) - or OTHER_TITLE) + names = [system_title(name, manifest) for name in entry_systems(entry)] + system = ", ".join(names) or OTHER_TITLE by_system.setdefault(system, []).append(entry) + # Batocera's group of systems sharing MAME's device ROMs was one line + # of 732 characters, ten rows on an 80-column terminal. + titles[system] = (system if verbose or len(names) <= PLAN_GROUP_TITLES + else f"{', '.join(names[:2])} and {len(names) - 2} more systems") shown = None if verbose else PLAN_FILES_PER_SYSTEM - listing: list[str] = [] + # (line, its text without styles), the second measuring the rows it takes. + listing: list[tuple[str, str]] = [] for system in sorted(by_system): entries = sorted(by_system[system], key=lambda e: e["dest"]) - listing.append(f" {term.bold(system)} {_sized(entries)}") - listing += [f" {entry['dest']} {term.dim(format_size(entry.get('size', 0)))}" + listing.append((f" {term.bold(titles[system])} {_sized(entries)}", + f" {titles[system]} {_sized(entries)}")) + listing += [(f" {entry['dest']} {term.dim(format_size(entry.get('size', 0)))}", + f" {entry['dest']} {format_size(entry.get('size', 0))}") for entry in entries[:shown]] if shown is not None and len(entries) > shown: - listing.append(f" ... and {len(entries) - shown} more") - screen = terminal_size()[1] - if verbose or len(listing) + len(trees) + PLAN_FRAME_LINES <= screen: - for line in listing: + more = f" ... and {len(entries) - shown} more" + listing.append((more, more)) + columns, screen = terminal_size() + # A line wider than the terminal wraps: counted as one, the total the + # listing must leave in view scrolled out of sight. + rows = sum(-(-max(1, cell_width(plain)) // max(1, columns)) for _line, plain in listing) + if verbose or rows + len(trees) + PLAN_FRAME_LINES <= screen: + for line, _plain in listing: print(line) else: largest = sorted(by_system, key=lambda s: (-_bytes(by_system[s]), s)) @@ -6719,7 +6733,7 @@ def _print_plan( else _plural(len(largest), 'system')) print(f" {groups}, the largest first:") for system in largest[:PLAN_LARGEST_SYSTEMS]: - print(f" {term.bold(system)} {_sized(by_system[system])}") + print(f" {term.bold(titles[system])} {_sized(by_system[system])}") if len(largest) > PLAN_LARGEST_SYSTEMS: # Counted as the line above counts: the groups, when it names them. unit = "group" if "groups" in groups else "system" diff --git a/install.sh b/install.sh index 86584bdd3..b66e4eb87 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="d3bfabb88e93d8ddf991965cd69929e73b0a254550f8f4c86a11ad34fe710a0b" +DEFAULT_INSTALL_SHA256="e7df99e3ac458c7717bd2ca415bc7d3d8701545652cd303ed19de67077531ab2" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/launchers/index.json b/install/launchers/index.json index b34898f37..3084dc556 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": 1086137, - "sha256": "d3bfabb88e93d8ddf991965cd69929e73b0a254550f8f4c86a11ad34fe710a0b", + "size": 1087102, + "sha256": "e7df99e3ac458c7717bd2ca415bc7d3d8701545652cd303ed19de67077531ab2", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1139764, - "sha256": "f9b58890f89971a8c0b9850ece3d132955cbc4823230e59a9224f04305055fba" + "size": 1140729, + "sha256": "5091f7a52ce8e2db24d3afa23a2d3e4a035fd3166d0a0389fb207dcc07fa0d3f" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1086137, - "sha256": "d3bfabb88e93d8ddf991965cd69929e73b0a254550f8f4c86a11ad34fe710a0b", + "size": 1087102, + "sha256": "e7df99e3ac458c7717bd2ca415bc7d3d8701545652cd303ed19de67077531ab2", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1086137, - "sha256": "d3bfabb88e93d8ddf991965cd69929e73b0a254550f8f4c86a11ad34fe710a0b", + "size": 1087102, + "sha256": "e7df99e3ac458c7717bd2ca415bc7d3d8701545652cd303ed19de67077531ab2", "executable": false }, { diff --git a/tests/test_install.py b/tests/test_install.py index f155ebaeb..c1036f4a5 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -4397,6 +4397,37 @@ class AQuestionLeftWithCtrlDOrCtrlCIsAStop(unittest.TestCase): self.assertIn("Stopped", out) +class APlanGroupFitsItsLine(unittest.TestCase): + """Batocera's group of systems sharing MAME's device ROMs was one plan + line of 732 characters: counted as one row, it wrapped to ten and the + total scrolled out of sight.""" + + def _plan(self, files: list[dict], size: tuple[int, int], verbose: bool = False) -> str: + out = io.StringIO() + with unittest.mock.patch.object(install.shutil, "get_terminal_size", + return_value=os.terminal_size(size)), \ + contextlib.redirect_stdout(out): + install._print_plan(files, Path("dest"), install.Terminal(io.StringIO()), + verbose=verbose) + return out.getvalue() + + def test_a_wide_group_is_named_by_its_first_two(self): + shared = [f"system-number-{n:02d}" for n in range(40)] + text = self._plan([{"dest": "dev.rom", "size": 1, "systems": shared}], (80, 40)) + self.assertIn("system-number-00, system-number-01 and 38 more systems", text) + self.assertLess(max(map(len, text.splitlines())), 80) + self.assertIn("system-number-39", self._plan( + [{"dest": "dev.rom", "size": 1, "systems": shared}], (80, 40), verbose=True)) + + def test_wrapped_rows_count_against_the_screen(self): + # Six lines that each wrap to three rows on a 20-column terminal of + # 20 rows: 18 rows of listing, past what the screen leaves it. + files = [{"dest": f"{'x' * 50}{n}.bin", "size": 1, "systems": [f"s{n}"]} + for n in range(3)] + text = self._plan(files, (20, 20)) + self.assertIn("the largest first", text) + + class ReadersAreThePlatformsCores(unittest.TestCase): """--check said bbcb.zip is "read by clk, mame, mame0258, mame0278" on Batocera: system_cores reaches every profile, Recalbox's builds too."""