fix: offer only what an install can do

Back from the systems list with nothing ticked keeps the ticks it
opened with, and None says First system once nothing is left to untick.
Confirm under --offline with nothing on hand has no Install, and the line
mode no longer asks to download what --offline never fetches. The line
mode counts the data files before its question.

Screen words: the checking clock starts at zero, a pack installs from
the pack, the copies found are said on a line of their own, the screen
of misplaced files says the copies stay, the save folder gets a line of
its own, the shared note is one line, a size keeps its unit, the same
save folder remedy on both screens, A Tick on the Miyoo bar, no Home
key in the help of a pad, and a system name said once with a pack.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 00:17:26 +01:00
1 parent 44f94ab703
commit fe1e98917b
8 files changed
+284 -52

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "8833256124ffdfa38865e7b065f2720ec20997e1a91ba340eab4067e31b20727" $defaultInstallSha256 = "d59defaa5802d2d7b73b4b02e693e9592618803b308eb625a6f436b8a5388464"
$maximumInstallerBytes = 2MB $maximumInstallerBytes = 2MB
$installerExit = 0 $installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+100 -25
View File
@@ -4220,7 +4220,7 @@ def place_save_entries(
if place.by_core: if place.by_core:
where += f"{os.sep}<core>" where += f"{os.sep}<core>"
print(f" {_plural(len(saved), 'file')} read from {display}'s save directory " print(f" {_plural(len(saved), 'file')} read from {display}'s save directory "
f"go to {where}") f"{'goes' if len(saved) == 1 else 'go'} to {where}")
EVENTS.emit("saves", platform=str(manifest.get("platform", "")), EVENTS.emit("saves", platform=str(manifest.get("platform", "")),
files=len(saved), folder=where) files=len(saved), folder=where)
return files, [] return files, []
@@ -6512,7 +6512,7 @@ INSTALLED_ONLY = "installed"
def _prompt_custom_selection( def _prompt_custom_selection(
plat: str, files: list[dict], omitted: list[dict], plat: str, files: list[dict], omitted: list[dict],
installed: tuple[list[dict], list[dict]] | None = None, installed: tuple[list[dict], list[dict]] | None = None,
name: str = "", dry_run: bool = False, name: str = "", dry_run: bool = False, data_trees: list[dict] | None = None,
) -> tuple[list[str], list[str], list[str]] | str | None: ) -> tuple[list[str], list[str], list[str]] | str | None:
"""Offer to narrow the install by system, core and region. """Offer to narrow the install by system, core and region.
@@ -6521,7 +6521,15 @@ def _prompt_custom_selection(
installed read) holds, or the three choices. Each empty list keeps installed read) holds, or the three choices. Each empty list keeps
everything on its axis. everything on its axis.
""" """
print(f"\n{name or display_name(plat)}: {_sized(files)}.") trees = data_trees or []
data = sum(len(entry.get("files") or {}) for entry in trees)
data_size = sum(meta.get("size", 0) for entry in trees
for meta in (entry.get("files") or {}).values())
print(f"\n{name or display_name(plat)}: {_sized(files)}"
+ (f", and {_plural(data, 'data file')} ({format_size(data_size)})" if data else "")
+ ".")
if _offline_only():
print(" --offline: only the files found on this machine are installed.")
# A dry run installs nothing: "install everything" was asked before it. # A dry run installs nothing: "install everything" was asked before it.
print(" Enter) list everything, nothing is written" if dry_run print(" Enter) list everything, nothing is written" if dry_run
else " Enter) install everything") else " Enter) install everything")
@@ -8234,7 +8242,7 @@ def _check_copy_lines(
if fixed: if fixed:
verb = "is" if fixed == 1 else "are" verb = "is" if fixed == 1 else "are"
them = "it" if fixed == 1 else "them" them = "it" if fixed == 1 else "them"
lines.append(f" {fixed} of these files {verb} in the BIOS folder at the wrong place: " lines.append(f" {fixed} of these files {verb} in the BIOS folder in the wrong place: "
f"a run without --check copies {them} where the cores open {them}, and " f"a run without --check copies {them} where the cores open {them}, and "
f"leaves your {'copy' if fixed == 1 else 'copies'} as " f"leaves your {'copy' if fixed == 1 else 'copies'} as "
f"{'it is' if fixed == 1 else 'they are'}.") f"{'it is' if fixed == 1 else 'they are'}.")
@@ -10189,6 +10197,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
selection = _prompt_custom_selection( selection = _prompt_custom_selection(
plat_name, files, omitted_files, installed, plat_name, files, omitted_files, installed,
display_name(plat_name, manifest), args.dry_run, display_name(plat_name, manifest), args.dry_run,
manifest.get("data_directories") or [],
) )
if selection is None: if selection is None:
smaller = (" Nothing downloaded. To install less, run the same command " smaller = (" Nothing downloaded. To install less, run the same command "
@@ -10387,9 +10396,23 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
continue continue
if to_download: if to_download:
dl_size = sum(f.get("size", 0) for f in to_download)
fetched = [f for f in to_download if id(f) not in copies] fetched = [f for f in to_download if id(f) not in copies]
if interactive and not args.yes and _offline_only() and fetched:
# The screen offers no Install for these: the question asked
# to download 6.6 GB that --offline never fetches.
print(f"\n {_plural(len(fetched), 'file')} "
f"{'needs' if len(fetched) == 1 else 'need'} the internet, which "
"--offline does not use. Copy them from your old card or a folder "
"(--from), or run the installer again with a pack (--from-pack) or "
"without --offline.")
files_pending += len(fetched)
to_download = [f for f in to_download if id(f) in copies]
fetched = []
dl_size = sum(f.get("size", 0) for f in to_download)
fetch_size = _bytes(fetched) fetch_size = _bytes(fetched)
if not to_download:
total_up_to_date += len(up_to_date)
continue
if ( if (
interactive interactive
and not args.yes and not args.yes
@@ -10818,6 +10841,21 @@ def _split_word(word: str, room: int) -> list[str]:
return pieces + [word] if word else pieces return pieces + [word] if word else pieces
_UNIT = re.compile(r"(B|KB|MB|GB)\b")
def _glued_words(text: str) -> list[str]:
"""The words of text, a size kept with its unit: "5.1" / "KB" was
broken across two lines of a 40-column headline."""
words: list[str] = []
for word in text.split():
if words and _UNIT.match(word) and re.fullmatch(r"\(?\d+(\.\d+)?", words[-1]):
words[-1] += f" {word}"
else:
words.append(word)
return words
def _wrap(text: str, width: int, indent: str = " ") -> list[str]: def _wrap(text: str, width: int, indent: str = " ") -> list[str]:
"""Lines of at most width cells, the ones after the first indented.""" """Lines of at most width cells, the ones after the first indented."""
if cell_width(text) <= width: if cell_width(text) <= width:
@@ -10825,7 +10863,7 @@ def _wrap(text: str, width: int, indent: str = " ") -> list[str]:
lead = text[:len(text) - len(text.lstrip(" "))] lead = text[:len(text) - len(text.lstrip(" "))]
lines: list[str] = [] lines: list[str] = []
line = "" line = ""
for word in text.split(): for word in _glued_words(text):
for piece in _split_word(word, width - len(lead + indent)): for piece in _split_word(word, width - len(lead + indent)):
prefix = lead + indent if lines else lead prefix = lead + indent if lines else lead
longer = f"{line} {piece}" if line else piece longer = f"{line} {piece}" if line else piece
@@ -10884,7 +10922,9 @@ class TuiState:
self.status: str = "" self.status: str = ""
self.cols: int = 80 self.cols: int = 80
self.rows: int = 24 self.rows: int = 24
self.now: float = 0.0 # A check started before the loop's first tick counted the
# machine's uptime.
self.now: float = time.monotonic()
# (platform, folder, label) for each setup found here. # (platform, folder, label) for each setup found here.
self.setups: list = [] self.setups: list = []
self.notes: list = [] self.notes: list = []
@@ -10964,6 +11004,9 @@ class TuiState:
self.must_confirm: bool = False self.must_confirm: bool = False
# Choices, each one an option of the command line. # Choices, each one an option of the command line.
self.ticked: set = set() self.ticked: set = set()
# The ticks the systems list opened with: Back with none ticked
# brings them back, nothing having been chosen.
self.ticked_before: set | None = None
self.cli_systems: list = [] self.cli_systems: list = []
self.regions: list = [] self.regions: list = []
self.cli_regions: list = [] self.cli_regions: list = []
@@ -11375,6 +11418,9 @@ def tui_rows(state: TuiState) -> list[TuiRow]:
if screen == "confirm": if screen == "confirm":
if state.running or state.dry is None: if state.running or state.dry is None:
return [] return []
if _offline_only() and not _dry_copies(state):
# The plan's rule: Install would only fail every file.
return [TuiRow("back", "Back")]
return [TuiRow("install", "Install"), TuiRow("back", "Back")] return [TuiRow("install", "Install"), TuiRow("back", "Back")]
if screen == "error": if screen == "error":
rows = ([TuiRow("retry", "Try again")] rows = ([TuiRow("retry", "Try again")]
@@ -11429,6 +11475,13 @@ def _plan_copies(state: TuiState, check: dict) -> list[str]:
return [dest for dest in local if dest in sizes] return [dest for dest in local if dest in sizes]
def _dry_copies(state: TuiState) -> list[str]:
"""The files the confirm or preview run would install that this
machine holds."""
local = {event["dest"] for event in state.found}
return [event["dest"] for event in state.dry_files if event["dest"] in local]
def _short_of_space(state: TuiState, check: dict) -> bool: def _short_of_space(state: TuiState, check: dict) -> bool:
free = state.facts.get("free") free = state.facts.get("free")
return free is not None and check.get("bytes_needed", 0) > free return free is not None and check.get("bytes_needed", 0) > free
@@ -11566,6 +11619,10 @@ def _back(state: TuiState) -> list[tuple]:
state.stack.append("quit") state.stack.append("quit")
state.focus["quit"] = 0 state.focus["quit"] = 0
return effects return effects
if state.screen == "systems" and not state.ticked and state.ticked_before:
# Enter on None, then Esc: the plan kept its headline over an
# Install the run refused.
state.ticked = set(state.ticked_before)
state.stack.pop() state.stack.pop()
if state.screen == "checking" and not state.running: if state.screen == "checking" and not state.running:
# Stay after Esc on a check started from the command line: a # Stay after Esc on a check started from the command line: a
@@ -11995,6 +12052,8 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
return _open_from_browser(state) return _open_from_browser(state)
if action in ("misplaced", "unreadable"): if action in ("misplaced", "unreadable"):
return _push(state, action) return _push(state, action)
if action == "systems":
state.ticked_before = set(state.ticked)
if action == "systems" and len(state.systems) > TUI_LONG_LIST: if action == "systems" and len(state.systems) > TUI_LONG_LIST:
# On None: picking one system of 66 began with Up, Up, A. # On None: picking one system of 66 began with Up, Up, A.
return _push(state, action, 1) return _push(state, action, 1)
@@ -12687,6 +12746,9 @@ def _route_hint(state: TuiState, parts: tuple) -> tuple:
named.append(key + sep + what) named.append(key + sep + what)
if state.pad == "muxterm" and state.screen == "path": if state.pad == "muxterm" and state.screen == "path":
named.append("Select Keyboard") named.append("Select Keyboard")
if state.pad == "miyoo" and state.screen in _CHECKLISTS:
tick = [entry for entry in named if entry.startswith("A ")]
named = named[:1] + tick + [entry for entry in named[1:] if entry not in tick]
return tuple(named) return tuple(named)
@@ -12724,6 +12786,7 @@ _ROW_VERBS = {
"jobs": "Change", "retry": "Try again", "back": "Back", "stay": "Stay", "jobs": "Change", "retry": "Try again", "back": "Back", "stay": "Stay",
"fromcard": "Choose", "from": "Add a folder", "misplaced": "Show", "zip": "Copy from it", "fromcard": "Choose", "from": "Add a folder", "misplaced": "Show", "zip": "Copy from it",
"unreadable": "Show", "exit": "Exit", "addapp": "Put it on the card", "unreadable": "Show", "exit": "Exit", "addapp": "Put it on the card",
"cardsystem": "Choose", "rescan": "Look again",
} }
@@ -12955,15 +13018,18 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
lines.append("You cannot write to this folder: choose another one.") lines.append("You cannot write to this folder: choose another one.")
counts = (f"{check['present']} in place, {check['missing']} missing, " counts = (f"{check['present']} in place, {check['missing']} missing, "
f"{wrong} wrong {'copy' if wrong == 1 else 'copies'}") f"{wrong} wrong {'copy' if wrong == 1 else 'copies'}")
lines.append(counts)
if copied and fetch: if copied and fetch:
# 47 to download above 49 missing and 1 wrong copy: the 3 found on # 47 to download above 49 missing and 1 wrong copy: the 3 found on
# this machine are the difference, said where the sum is read. # this machine are the difference, on a line of its own ("0 wrong
counts += (f": {len(copied)} of them {'is' if len(copied) == 1 else 'are'} " # copies: 2 of them" read as two of the zero).
f"copied from this machine, {fetch} downloaded") lines.append(f"{need} to install: {len(copied)} copied from this machine, {fetch} "
lines.append(counts) + ("from the pack" if LOCAL.pack is not None else "downloaded"))
saves = (state.saves if check is state.check after_counts = len(lines)
saves =(state.saves if check is state.check
else state.dry_saves if check is state.dry else None) else state.dry_saves if check is state.dry else None)
lines += [_styled(line, "path") for line in _saves_lines(state, saves)] saves_lines = _saves_lines(state, saves)
lines += [_styled(line, "path" if index else "") for index, line in enumerate(saves_lines)]
found_in = _retroarch_config_at(dest) if dest is not None else None found_in = _retroarch_config_at(dest) if dest is not None else None
if found_in: if found_in:
lines.append(_styled(f"Named by {_short_path(Path(found_in[0]), state.home)}", "path")) lines.append(_styled(f"Named by {_short_path(Path(found_in[0]), state.home)}", "path"))
@@ -12980,7 +13046,7 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
f"{'one' if wrong == 1 else 'ones'} kept in a backup folder.") f"{'one' if wrong == 1 else 'ones'} kept in a backup folder.")
misplaced = _tui_misplaced(state) if check is state.check else [] misplaced = _tui_misplaced(state) if check is state.check else []
if misplaced: if misplaced:
lines.insert(3, f"{_plural(len(misplaced), 'file')} " lines.insert(after_counts, f"{_plural(len(misplaced), 'file')} "
f"{'is' if len(misplaced) == 1 else 'are'} in the wrong place, " f"{'is' if len(misplaced) == 1 else 'are'} in the wrong place, "
f"{'it' if len(misplaced) == 1 else 'they'} will be copied where the " f"{'it' if len(misplaced) == 1 else 'they'} will be copied where the "
"emulators read " "emulators read "
@@ -13003,7 +13069,7 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
lines += _unavailable_lines(state) lines += _unavailable_lines(state)
if check is state.check: if check is state.check:
lines += _unplaced_lines(state.check_summary, state, lines += _unplaced_lines(state.check_summary, state,
"Choose it in Options, Save folder.") "Choose it in Options, Save folder (--save-dir).")
return lines[:2] + offline_lines + lines[2:] return lines[:2] + offline_lines + lines[2:]
@@ -13015,13 +13081,16 @@ def _saves_lines(state: TuiState, saves: dict | None) -> list[str]:
return [] return []
name = display_name(state.platform) if state.platform else "the frontend" name = display_name(state.platform) if state.platform else "the frontend"
count = saves["files"] count = saves["files"]
# The folder alone is cut in the middle: cut with the sentence, it
# lost "directory goes to" even at 80 columns.
return [f"{_plural(count, 'file')} read from {name}'s save directory " return [f"{_plural(count, 'file')} read from {name}'s save directory "
f"{'goes' if count == 1 else 'go'} to " f"{'goes' if count == 1 else 'go'} to:",
f"{_short_path(Path(saves['folder']), state.home)}"] _short_path(Path(saves['folder']), state.home)]
def _unplaced_lines(summary: dict | None, state: TuiState, def _unplaced_lines(summary: dict | None, state: TuiState,
remedy: str = "Name it with --save-dir.") -> list[str]: remedy: str = "Choose it in Options, Save folder (--save-dir), "
"next time.") -> list[str]:
"""The files read from the frontend's save directory that the run left """The files read from the frontend's save directory that the run left
out, that directory being unknown: said, never silent.""" out, that directory being unknown: said, never silent."""
count = sum(group.get("files", 0) for group in (summary or {}).get("skipped") or [] count = sum(group.get("files", 0) for group in (summary or {}).get("skipped") or []
@@ -13244,8 +13313,7 @@ def _view_systems(state: TuiState, width: int, height: int) -> tuple:
footer.insert(0, OTHER_HELP) footer.insert(0, OTHER_HELP)
elif "shared" in focused.right: elif "shared" in focused.right:
count = _shared_needed(state)[focused.value][0] count = _shared_needed(state)[focused.value][0]
footer.insert(0, f"{count} of these files are shared with other systems, " footer.insert(0, f"+{count} shared: counted once")
"counted once in the total.")
needle = state.filters.get("systems", "") needle = state.filters.get("systems", "")
after = [f"No system matches '{needle}'."] if len(rows) == 3 and needle else [] after = [f"No system matches '{needle}'."] if len(rows) == 3 and needle else []
if state.status: if state.status:
@@ -13301,7 +13369,9 @@ _STATE_WORDS = {"missing": "missing", "wrong_hash": "wrong copy"}
def _pager_items(state: TuiState) -> list: def _pager_items(state: TuiState) -> list:
"""The text of a screen that scrolls, the same for its view and its keys.""" """The text of a screen that scrolls, the same for its view and its keys."""
if state.screen == "misplaced": if state.screen == "misplaced":
return [f"{event['dest']}: {event['reason']}" for event in _tui_misplaced(state)] return ["They are copied where the emulators read them. Your copies stay where "
"they are.", ""] + [f"{event['dest']}: {event['reason']}"
for event in _tui_misplaced(state)]
if state.screen == "unreadable": if state.screen == "unreadable":
return [f"{event['path']}" + (f", {event['member']}" if event["member"] else "") return [f"{event['path']}" + (f", {event['member']}" if event["member"] else "")
+ f": {event['reason']}" for event in state.unreadable] + f": {event['reason']}" for event in state.unreadable]
@@ -13338,7 +13408,7 @@ def _view_unreadable(state: TuiState, width: int, height: int) -> tuple:
def _view_misplaced(state: TuiState, width: int, height: int) -> tuple: def _view_misplaced(state: TuiState, width: int, height: int) -> tuple:
return ("Files in the wrong place", _pager(state, width, height, _pager_items(state)), return ("Files in the wrong place", _pager(state, width, height, _pager_items(state), 2),
("A/Enter Back", "Up/Down Scroll")) ("A/Enter Back", "Up/Down Scroll"))
@@ -13451,7 +13521,8 @@ def _tui_sources(state: TuiState, installed: int, failed: int) -> str:
name = _last_name(folder) name = _last_name(folder)
folders[name] = folders.get(name, 0) + 1 folders[name] = folders.get(name, 0) + 1
copied = fixed + sum(folders.values()) copied = fixed + sum(folders.values())
return sources_line(list(folders.items()), fixed, max(0, installed - copied), failed) return sources_line(list(folders.items()), fixed, max(0, installed - copied), failed,
"from the pack" if LOCAL.pack is not None else "downloaded")
def _unavailable(state: TuiState) -> tuple[list[dict], int]: def _unavailable(state: TuiState) -> tuple[list[dict], int]:
@@ -13551,7 +13622,7 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]:
ok = {dest for dest, good, _why in state.finished if good} ok = {dest for dest, good, _why in state.finished if good}
if any(reason and dest in ok for dest, (_folder, reason) in state.local_dests.items()): if any(reason and dest in ok for dest, (_folder, reason) in state.local_dests.items()):
# A fix copies: the misplaced files are the user's, never moved. # A fix copies: the misplaced files are the user's, never moved.
lines.append("Your copies at the wrong place were left as they are.") lines.append("Your copies in the wrong place were left as they are.")
lines.append(f"BIOS folder: {state.dest}") lines.append(f"BIOS folder: {state.dest}")
if installed + up_to_date: if installed + up_to_date:
lines += _saves_lines(state, state.done_saves) lines += _saves_lines(state, state.done_saves)
@@ -13716,7 +13787,8 @@ def _help_lines(state: TuiState) -> list[tuple[str, str]]:
keys = [(names["move"], "Move")] keys = [(names["move"], "Move")]
if len(tui_rows(state)) > TUI_LONG_LIST or screen in _PAGERS: if len(tui_rows(state)) > TUI_LONG_LIST or screen in _PAGERS:
keys += [*([(names["page"], "Page")] if names["page"] else []), keys += [*([(names["page"], "Page")] if names["page"] else []),
("Home/End, g/G", "First, last")] *([("Home/End, g/G", "First, last")] if state.pad in ("both", "keys")
else [])]
primary = _primary(state) or _TUI_VIEWS[screen](state, 80, 24)[2][0] primary = _primary(state) or _TUI_VIEWS[screen](state, 80, 24)[2][0]
ok = names["ok"] ok = names["ok"]
if state.pad == "miyoo" and screen in _CHECKLISTS: if state.pad == "miyoo" and screen in _CHECKLISTS:
@@ -13755,6 +13827,9 @@ def _primary(state: TuiState) -> str | None:
verb = _ROW_VERBS.get(row.action) verb = _ROW_VERBS.get(row.action)
if state.screen == "systems" and row.action == "system" and not state.ticked: if state.screen == "systems" and row.action == "system" and not state.ticked:
verb = "Only this system" verb = "Only this system"
if state.screen == "systems" and row.action == "none" and not state.ticked:
# Nothing left to untick: A goes to the first system.
verb = "First system"
return f"A/Enter {verb}" if verb else None return f"A/Enter {verb}" if verb else None
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER="" TEMP_INSTALLER=""
TEMP_DIRECTORY="" TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="8833256124ffdfa38865e7b065f2720ec20997e1a91ba340eab4067e31b20727" DEFAULT_INSTALL_SHA256="d59defaa5802d2d7b73b4b02e693e9592618803b308eb625a6f436b8a5388464"
MAX_INSTALLER_BYTES=2097152 MAX_INSTALLER_BYTES=2097152
cleanup() { cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{ {
"dest": "roms/ports/retrobios/install.py", "dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py", "repo_path": "install.py",
"size": 646794, "size": 650749,
"sha256": "8833256124ffdfa38865e7b065f2720ec20997e1a91ba340eab4067e31b20727", "sha256": "d59defaa5802d2d7b73b4b02e693e9592618803b308eb625a6f436b8a5388464",
"executable": false "executable": false
}, },
{ {
@@ -71,8 +71,8 @@
"muos": { "muos": {
"archive": { "archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp", "dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 699343, "size": 703298,
"sha256": "42226d45d6a31c86b726b1af321e043f9894f9993d8c6de0f16cc84c04a2a1ed" "sha256": "bb54c96f4ae4850f997cc925b2958b21477be655a658b6bc3551d1aef68fdbb5"
}, },
"members": [ "members": [
{ {
@@ -92,8 +92,8 @@
{ {
"name": "RetroBIOS/install.py", "name": "RetroBIOS/install.py",
"repo_path": "install.py", "repo_path": "install.py",
"size": 646794, "size": 650749,
"sha256": "8833256124ffdfa38865e7b065f2720ec20997e1a91ba340eab4067e31b20727", "sha256": "d59defaa5802d2d7b73b4b02e693e9592618803b308eb625a6f436b8a5388464",
"executable": false "executable": false
}, },
{ {
@@ -166,8 +166,8 @@
{ {
"dest": ".config/modules/retrobios/install.py", "dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py", "repo_path": "install.py",
"size": 646794, "size": 650749,
"sha256": "8833256124ffdfa38865e7b065f2720ec20997e1a91ba340eab4067e31b20727", "sha256": "d59defaa5802d2d7b73b4b02e693e9592618803b308eb625a6f436b8a5388464",
"executable": false "executable": false
}, },
{ {
+6 -6
View File
@@ -550,7 +550,7 @@
2 of 3 systems: 4 files, 1.0 MB 2 of 3 systems: 4 files, 1.0 MB
Start Continue R2 Back A Tick Start Continue A Tick R2 Back
=== systems 40x15 === systems 40x15
RetroBIOS: Choose systems RetroBIOS: Choose systems
@@ -566,7 +566,7 @@
2 of 3 systems: 4 files, 1.0 MB 2 of 3 systems: 4 files, 1.0 MB
Start Continue R2 Back A Tick Start Continue A Tick R2 Back
=== systems 80x24 === systems 80x24
RetroBIOS: Choose systems RetroBIOS: Choose systems
@@ -591,7 +591,7 @@
2 of 3 systems: 4 files, 1.0 MB 2 of 3 systems: 4 files, 1.0 MB
Start Continue R2 Back A Tick Start Continue A Tick R2 Back
=== options 40x12 === options 40x12
RetroBIOS: Options RetroBIOS: Options
@@ -712,7 +712,7 @@
Start Filter R2 Back A Tick Start Filter A Tick R2 Back
=== cores 40x15 === cores 40x15
RetroBIOS: Cores RetroBIOS: Cores
@@ -728,7 +728,7 @@
Start Filter R2 Back A Tick Start Filter A Tick R2 Back
=== cores 80x24 === cores 80x24
RetroBIOS: Cores RetroBIOS: Cores
@@ -753,7 +753,7 @@
Start Filter R2 Back A Tick Start Filter A Tick R2 Back
=== preview 40x12 === preview 40x12
RetroBIOS: Preview RetroBIOS: Preview
+1 -1
View File
@@ -438,7 +438,7 @@ class MisplacedInTheBiosFolder(Scratch):
for name, data in PAYLOAD.items(): for name, data in PAYLOAD.items():
self.place(f"system/{name}", data) self.place(f"system/{name}", data)
out = self.check() out = self.check()
self.assertIn("4 of these files are in the BIOS folder at the wrong place: a run " self.assertIn("4 of these files are in the BIOS folder in the wrong place: a run "
"without --check copies them where the cores open them, and leaves " "without --check copies them where the cores open them, and leaves "
"your copies as they are.", out) "your copies as they are.", out)
self.assertFalse((self.dest / "scph5501.bin").exists()) self.assertFalse((self.dest / "scph5501.bin").exists())
+166 -9
View File
@@ -2044,7 +2044,7 @@ class CopyFromAnOldCard(unittest.TestCase):
state.finished = [("scph5500.bin", True, ""), ("gba_bios.bin", True, "")] state.finished = [("scph5500.bin", True, ""), ("gba_bios.bin", True, "")]
text = " ".join(" ".join(install.tui_outcome(state)[1]).split()) text = " ".join(" ".join(install.tui_outcome(state)[1]).split())
self.assertIn("1 file fixed from copies already in the BIOS folder, 1 downloaded. " self.assertIn("1 file fixed from copies already in the BIOS folder, 1 downloaded. "
"Your copies at the wrong place were left as they are.", text) "Your copies in the wrong place were left as they are.", text)
def test_a_local_copy_is_no_download_speed(self): def test_a_local_copy_is_no_download_speed(self):
state = checked_state() state = checked_state()
@@ -2432,7 +2432,7 @@ class AuditRoundThree(unittest.TestCase):
self.assertEqual(rows[three].right, "1 file, 100 B + 2 shared") self.assertEqual(rows[three].right, "1 file, 100 B + 2 shared")
state.focus["systems"] = three state.focus["systems"] = three
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split()) text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
self.assertIn("2 of these files are shared with other systems, counted once", text) self.assertIn("+2 shared: counted once", text)
# The line mode counts the same files apart. # The line mode counts the same files apart.
self.assertIn("and 2 files, 500 B shared with other systems", install._system_sized([ self.assertIn("and 2 files, 500 B shared with other systems", install._system_sized([
{"dest": "panafz1.bin", "size": 100, "systems": ["3do"]}, {"dest": "panafz1.bin", "size": 100, "systems": ["3do"]},
@@ -2767,11 +2767,12 @@ class AuditRoundFour(unittest.TestCase):
plan = " ".join(" ".join(install.tui_view(state, 120, 40)).split()) plan = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
# The plan names the row that sets it, the result the option a # The plan names the row that sets it, the result the option a
# command carries. # command carries.
self.assertIn(line + "Choose it in Options, Save folder.", plan) self.assertIn(line + "Choose it in Options, Save folder (--save-dir).", plan)
state.stack.append("result") state.stack.append("result")
state.code = 0 state.code = 0
state.summary = {**summary, "mode": "install", "installed": 5} state.summary = {**summary, "mode": "install", "installed": 5}
self.assertIn(line + "Name it with --save-dir.", install.tui_outcome(state)[1]) self.assertIn(line + "Choose it in Options, Save folder (--save-dir), next time.",
install.tui_outcome(state)[1])
class TheSaveFolderHasARow(unittest.TestCase): class TheSaveFolderHasARow(unittest.TestCase):
@@ -3441,6 +3442,162 @@ class OfflineInTheInterface(unittest.TestCase):
self.assertIn("--offline", install.tui_command(state)) self.assertIn("--offline", install.tui_command(state))
class AuditFive(unittest.TestCase):
"""tui_audit_5 P2-1, P2-2 and its P3 points."""
def long_list(self) -> install.TuiState:
state = OneSystemInFewPresses().long_list("keys")
state.cols, state.rows = 80, 24
return state
def test_back_from_none_keeps_the_ticks_it_opened_with(self):
state = self.long_list()
everything = set(state.ticked)
press(state, "down", "enter")
self.assertEqual(install.tui_rows(state)[state.focus["systems"]].action, "none")
press(state, "enter")
self.assertEqual(state.ticked, set())
press(state, "up", "up")
self.assertEqual(install._primary(state), "A/Enter First system")
press(state, "esc")
self.assertEqual(state.screen, "plan")
self.assertEqual(state.ticked, everything)
text = " ".join(install.tui_view(state, 80, 24))
self.assertNotIn("No system is ticked", text)
def test_back_after_a_choice_keeps_the_choice(self):
state = self.long_list()
press(state, "down", "enter", "enter")
press(state, " ")
chosen = set(state.ticked)
press(state, "esc")
self.assertEqual((state.screen, state.ticked), ("plan", chosen))
def test_confirm_offline_with_nothing_on_hand_has_no_install(self):
state = checked_state()
state.stack.append("confirm")
state.dry = dict(CHECK)
state.dry_files = [dict(event, state="missing") for event in FILES]
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
rows = [row.action for row in install.tui_rows(state)]
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
self.assertEqual(rows, ["back"])
self.assertIn("Copy them from your old card or a folder", text)
state.found = [{"event": "found", "platform": "retroarch",
"dest": FILES[0]["dest"], "folder": "/media/old",
"path": "/media/old/x", "member": "", "reason": ""}]
self.assertEqual(install.tui_rows(state)[0].action, "install")
def test_the_checking_clock_starts_at_zero(self):
state = install.TuiState()
self.assertLess(abs(state.now - time.monotonic()), 5)
def test_a_pack_installs_from_the_pack(self):
state = checked_state()
state.found = [{"event": "found", "platform": "retroarch", "dest": FILES[0]["dest"],
"folder": "/media/old", "path": "/media/old/x", "member": "",
"reason": "misplaced"}]
with mock.patch.object(install.LOCAL, "pack", mock.Mock()):
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
self.assertIn("1 copied from this machine,", text)
self.assertIn("from the pack", text)
self.assertNotIn("downloaded", text)
self.assertNotIn("of them", text)
state.finished = [(FILES[0]["dest"], True, ""), (FILES[1]["dest"], True, "")]
state.local_dests = {FILES[0]["dest"]: ("/media/old", "misplaced")}
self.assertIn("1 from the pack", install._tui_sources(state, 2, 0))
def test_the_wrong_place_screen_says_the_copies_stay(self):
state = checked_state()
state.stack.append("misplaced")
self.assertEqual(install._pager_items(state)[0],
"They are copied where the emulators read them. Your copies stay "
"where they are.")
def test_the_save_folder_is_a_line_of_its_own(self):
state = checked_state()
saves = {"files": 1, "folder": str(HOME / ".config" / "retroarch" / "saves" / "<core>")}
lines = install._saves_lines(state, saves)
self.assertEqual(lines, ["1 file read from RetroArch's save directory goes to:",
"~/.config/retroarch/saves/<core>"])
def test_the_shared_note_is_one_line(self):
state = self.long_list()
state.cols, state.rows = 40, 12
state.files = [{"event": "file", "dest": "mame.zip", "size": 10,
"systems": ["sys-00", "sys-01"]}]
state.stack.append("systems")
state.focus["systems"] = 3
lines = install.tui_view(state, 40, 12)
self.assertIn(" +1 shared: counted once", lines)
def test_the_save_folder_remedy_is_the_same_on_both_screens(self):
summary = {"skipped": [{"reason": "no_save_directory", "files": 1}]}
state = checked_state()
lines = install._unplaced_lines(summary, state)
self.assertIn("Options, Save folder (--save-dir)", lines[0])
def test_a_size_keeps_its_unit(self):
for width in range(20, 45):
for line in install._wrap("2 files to install from the pack, 5.1 KB", width):
self.assertNotEqual(line.strip(), "KB")
def test_the_miyoo_bar_names_the_tick(self):
state = self.long_list()
state.pad = "miyoo"
press(state, "down", "enter")
state.ticked = set()
state.focus["systems"] = 3
bar = install.tui_view(state, 40, 15)[-1]
self.assertIn("A Tick", bar)
self.assertTrue(bar.strip().startswith("Start Only this system"))
def test_muxterm_help_names_no_home_key(self):
state = self.long_list()
state.pad = "muxterm"
press(state, "down", "enter")
keys = [key for key, _what in install._help_lines(state)]
self.assertNotIn("Home/End, g/G", keys)
state.pad = "keys"
self.assertIn("Home/End, g/G", [key for key, _what in install._help_lines(state)])
class LineModeOffline(unittest.TestCase):
"""tui_audit_5 P2-3 and P3-8.4: the line mode under --offline asked to
download, and said nothing of the data files."""
def test_nothing_is_offered_that_cannot_come(self):
with FakeRepository() as repo:
env = {**repo.env(), "TERM": "xterm", install.OFFLINE_VARIABLE: "1"}
term = Pty(["--plain", "--manifest", str(repo.root / "install" / "retroarch.json"),
"--dest", str(repo.dest), "--offline"], env)
term.wait("q) quit")
term.send("\r")
term.close()
out = term.raw.decode("utf-8", "replace")
self.assertIn("--offline: only the files found on this machine are installed.", out)
self.assertIn("5 files need the internet, which --offline does not use.", out)
self.assertNotIn("Downloading", out)
self.assertNotIn("failed", out.lower().replace("not failed", ""))
def test_the_data_files_are_counted_before_the_question(self):
files = [{"dest": "a.bin", "size": 1024}]
trees = [{"ref": "t", "files": {"x": {"size": 2048}, "y": {"size": 2048}}}]
with mock.patch.object(install, "_ask", return_value=""), \
contextlib.redirect_stdout(io.StringIO()) as out:
install._prompt_custom_selection("retroarch", files, [], data_trees=trees)
self.assertIn("RetroArch: 1 file, 1.0 KB, and 2 data files (4.0 KB).", out.getvalue())
def test_one_save_file_goes(self):
files = [{"dest": "keys.txt", "root": "save", "size": 1}]
place = install.SavePlace(Path("/saves"), False)
with mock.patch.object(install, "save_directory_for", return_value=place), \
contextlib.redirect_stdout(io.StringIO()) as out:
install.place_save_entries(files, Path("/bios"), {}, None, "RetroArch")
self.assertIn("1 file read from RetroArch's save directory goes to /saves",
out.getvalue())
class NarrowSystemNames(unittest.TestCase): class NarrowSystemNames(unittest.TestCase):
"""At 40 columns the size column took half the line: Game Boy, Game Boy """At 40 columns the size column took half the line: Game Boy, Game Boy
Advance and Game Boy Color all read "Nintendo Gam".""" Advance and Game Boy Color all read "Nintendo Gam"."""
@@ -3554,7 +3711,7 @@ class TheSaveFolderLineFollowsTheChoice(unittest.TestCase):
SAVES = {"event": "saves", "platform": "retroarch", "files": 1, SAVES = {"event": "saves", "platform": "retroarch", "files": 1,
"folder": "/home/user/MySaves/<core>"} "folder": "/home/user/MySaves/<core>"}
LINE = "1 file read from RetroArch's save directory goes to ~/MySaves/<core>" LINE = "1 file read from RetroArch's save directory goes to: ~/MySaves/<core>"
def said(self, lines) -> str: def said(self, lines) -> str:
return " ".join(" ".join(lines).split()) return " ".join(" ".join(lines).split())
@@ -4356,11 +4513,11 @@ class EndToEnd(unittest.TestCase):
self.assertIn("6 files to download", screen) self.assertIn("6 files to download", screen)
# The second folder the run writes to, named before # The second folder the run writes to, named before
# anything is written, and again on the result. # anything is written, and again on the result.
self.assertIn("1 file read from RetroArch's save directory goes to " self.assertIn("1 file read from RetroArch's save directory goes to:\n"
"~/MySaves", screen) " ~/MySaves", screen)
term.send("\r") term.send("\r")
screen = term.wait("6 files installed.") screen = term.wait("6 files installed.")
self.assertIn("save directory goes to ~/MySaves", screen) self.assertIn("save directory goes to:\n ~/MySaves", screen)
term.send("\r") term.send("\r")
self.assertEqual(term.close(), 0) self.assertEqual(term.close(), 0)
results.append(written(repo, saves)) results.append(written(repo, saves))
@@ -4741,7 +4898,7 @@ class ThePlanNumbersAddUp(unittest.TestCase):
state.found = [found("scph5500.bin", str(SYSTEM_DIR), "one folder too deep")] state.found = [found("scph5500.bin", str(SYSTEM_DIR), "one folder too deep")]
screen = " ".join(" ".join(install.tui_view(state, 120, 40)).split()) screen = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
self.assertIn("3 files to download", screen) self.assertIn("3 files to download", screen)
self.assertIn("1 in place, 3 missing, 1 wrong copy: 1 of them is copied from this " self.assertIn("1 in place, 3 missing, 1 wrong copy 4 to install: 1 copied from this "
"machine, 3 downloaded", screen) "machine, 3 downloaded", screen)
+1 -1
View File
@@ -736,7 +736,7 @@ class TheInstallerWritesThem(unittest.TestCase):
b"keys below the save directory", b"keys below the save directory",
) )
self.assertFalse((app / "system" / "Azahar").exists()) self.assertFalse((app / "system" / "Azahar").exists())
self.assertIn("read from RetroArch's save directory go to", proc.stdout) self.assertIn("read from RetroArch's save directory goes to", proc.stdout)
def test_where_save_dir_points(self): def test_where_save_dir_points(self):
repo = _Repository(self, self.FILES) repo = _Repository(self, self.FILES)