mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 04:39:06 -05:00
chore: merge old card copy in the interface
This commit is contained in:
commit
5d8d7ce371
14 files changed
+326
-55
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "163a216c1b09e8d788d4de6148194705813d4c6b8a956c84a70a267205f2c25d"
|
||||
$defaultInstallSha256 = "bcbde4c289b32c02ada78001b75274a87b125c1efd3d8fb1064d7f5482ef3997"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+131
-25
@@ -8966,6 +8966,14 @@ class TuiState:
|
||||
self.keep_ticked: set | None = None
|
||||
# (files read, files to read) while the check hashes what is in place.
|
||||
self.reading: tuple | None = None
|
||||
# Folders to copy BIOS files from (--from), and what the check found.
|
||||
self.from_folders: list = []
|
||||
self.found: list = []
|
||||
# What the browser is choosing: the BIOS folder, or one to copy from.
|
||||
self.browse_for: str = "dest"
|
||||
# Files an install copies from this machine, whose bytes are no download.
|
||||
self.local_dests: set = set()
|
||||
self.local_done: int = 0
|
||||
# The child run in progress: check, confirm, preview or install.
|
||||
self.run: str = ""
|
||||
self.running: bool = False
|
||||
@@ -9057,8 +9065,10 @@ def tui_blocker(
|
||||
return (f"the terminal is {cols}x{rows}, smaller than "
|
||||
f"{TUI_MIN_COLS}x{TUI_MIN_ROWS}")
|
||||
if os.name == "nt":
|
||||
import importlib # noqa: PLC0415
|
||||
|
||||
try:
|
||||
import msvcrt # noqa: F401, PLC0415
|
||||
importlib.import_module("msvcrt")
|
||||
except ImportError:
|
||||
return "this Python has no msvcrt to read keys"
|
||||
if not _enable_ansi():
|
||||
@@ -9094,6 +9104,7 @@ def tui_flags(state: TuiState) -> list[tuple[str, str | None]]:
|
||||
flags.append(("--installed-cores", None))
|
||||
if state.jobs != 8:
|
||||
flags.append(("--jobs", str(state.jobs)))
|
||||
flags += [("--from", str(folder)) for folder in state.from_folders]
|
||||
return flags
|
||||
|
||||
|
||||
@@ -9135,7 +9146,7 @@ class TuiRow(NamedTuple):
|
||||
|
||||
|
||||
# Screens that scroll text instead of listing rows.
|
||||
_PAGERS = ("preview", "command", "result")
|
||||
_PAGERS = ("preview", "command", "result", "misplaced")
|
||||
_CHECKLISTS = ("systems", "cores")
|
||||
|
||||
|
||||
@@ -9270,9 +9281,13 @@ def _plan_rows(state: TuiState) -> list[TuiRow]:
|
||||
rows = [TuiRow("install", label)]
|
||||
if state.facts.get("writable") is False:
|
||||
# Every file would fail alike: the way out is another folder.
|
||||
rows = [TuiRow("folder", "Choose another folder"), TuiRow("install", "Install anyway")]
|
||||
rows = [TuiRow("folder", "Choose another folder"), TuiRow("install", "Install anyway"),
|
||||
*rows[1:]]
|
||||
if records_selection((state.manifest or {}).get("files", [])) and len(state.systems) > 1:
|
||||
rows.append(TuiRow("systems", "Choose systems"))
|
||||
rows.append(TuiRow("fromcard", "Copy the BIOS files from your old card or a folder"))
|
||||
if _tui_misplaced(state):
|
||||
rows.append(TuiRow("misplaced", "Show the files in the wrong place"))
|
||||
return rows + [TuiRow("options", "Options"), TuiRow("quit", "Quit")]
|
||||
|
||||
|
||||
@@ -9323,10 +9338,12 @@ def _option_rows(state: TuiState) -> list[TuiRow]:
|
||||
if choices["cores"]:
|
||||
chosen = f"{len(state.cores)} chosen" if state.cores else "all"
|
||||
rows.append(TuiRow("cores", f"Cores: {chosen}", "--core"))
|
||||
copied = ", ".join(str(folder) for folder in state.from_folders) or "none"
|
||||
rows += [
|
||||
TuiRow("jobs", f"Parallel downloads: {state.jobs}", "--jobs"),
|
||||
TuiRow("folder", f"BIOS folder: {_short_path(state.dest, state.home)}", "--dest",
|
||||
style="path"),
|
||||
TuiRow("from", f"Copy from: {copied}", "--from"),
|
||||
TuiRow("preview", "Preview the file list", "--dry-run"),
|
||||
TuiRow("command", "Show the command line"),
|
||||
]
|
||||
@@ -9397,12 +9414,13 @@ def _start_run(state: TuiState, run: str) -> list[tuple]:
|
||||
# The systems of an earlier check go first: ticks read against them
|
||||
# dropped the --system the command line gave.
|
||||
state.check, state.files, state.systems, state.reading = None, [], [], None
|
||||
state.found = []
|
||||
state.checked_flags = tui_flags(state)
|
||||
elif run in ("confirm", "preview"):
|
||||
state.dry, state.dry_files, state.dry_summary = None, [], None
|
||||
else:
|
||||
state.download = state.progress = state.reading = None
|
||||
state.samples = []
|
||||
state.samples, state.local_dests, state.local_done = [], set(), 0
|
||||
state.finished, state.summary, state.code, state.stopping = [], None, None, False
|
||||
return [("run", run)]
|
||||
|
||||
@@ -9479,8 +9497,9 @@ def _mounted_cards() -> list[Path]:
|
||||
return found
|
||||
|
||||
|
||||
def _browse_rows(folder: Path | None) -> tuple[list[TuiRow], str]:
|
||||
"""A directory's rows for the browser, cards first, and what went wrong."""
|
||||
def _browse_rows(folder: Path | None, zips: bool = False) -> tuple[list[TuiRow], str]:
|
||||
"""A directory's rows for the browser, cards first, and what went wrong.
|
||||
With zips, the ZIP files too: a backup is often one archive."""
|
||||
if folder is None:
|
||||
drives = [Path(f"{letter}:\\") for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
if os.path.exists(f"{letter}:\\")]
|
||||
@@ -9492,17 +9511,21 @@ def _browse_rows(folder: Path | None) -> tuple[list[TuiRow], str]:
|
||||
what = f"{display_name(plat)} card" if plat else "card or drive"
|
||||
rows.append(TuiRow("dir", f"{card} ({what})", value=card))
|
||||
try:
|
||||
entries = [entry for entry in os.scandir(folder) if entry.is_dir()]
|
||||
entries = list(os.scandir(folder))
|
||||
except OSError as exc:
|
||||
return rows, f"Cannot open this folder: {exc.strerror or exc}"
|
||||
entries.sort(key=lambda entry: (entry.name.startswith("."), entry.name.casefold()))
|
||||
rows += [TuiRow("dir", f"{entry.name}/", value=Path(entry.path)) for entry in entries]
|
||||
rows += [TuiRow("dir", f"{entry.name}/", value=Path(entry.path))
|
||||
for entry in entries if entry.is_dir()]
|
||||
if zips:
|
||||
rows += [TuiRow("zip", entry.name, value=Path(entry.path)) for entry in entries
|
||||
if entry.is_file() and entry.name.lower().endswith(".zip")]
|
||||
return rows, ""
|
||||
|
||||
|
||||
def _browse_to(state: TuiState, folder: Path | None, came_from: Path | None = None) -> None:
|
||||
state.browse_dir = folder
|
||||
state.browse_rows, state.status = _browse_rows(folder)
|
||||
state.browse_rows, state.status = _browse_rows(folder, state.browse_for == "from")
|
||||
focus = 0
|
||||
for index, row in enumerate(state.browse_rows):
|
||||
if came_from is not None and row.action == "dir" and row.value == came_from:
|
||||
@@ -9558,6 +9581,7 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
|
||||
if action == "default":
|
||||
return _choose(state, state.platform, state.folder_default)
|
||||
if action == "browse":
|
||||
state.browse_for = "dest"
|
||||
_push(state, "browse")
|
||||
_browse_to(state, _nearest_folder(state.folder_default or state.home))
|
||||
return []
|
||||
@@ -9565,6 +9589,13 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
|
||||
state.path_selected = bool(state.path_text)
|
||||
return _push(state, "path")
|
||||
if screen == "browse":
|
||||
if action in ("use", "zip") and state.browse_for == "from":
|
||||
chosen = state.browse_dir if action == "use" else row.value
|
||||
if chosen not in state.from_folders:
|
||||
state.from_folders.append(chosen)
|
||||
state.browse_for = "dest"
|
||||
# The check runs again with it: the plan then says what it holds.
|
||||
return _start_check(state)
|
||||
if action == "use":
|
||||
return _choose(state, state.platform, state.browse_dir)
|
||||
if action == "up" and state.browse_dir is not None:
|
||||
@@ -9595,6 +9626,10 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
|
||||
return tui_quit(state)
|
||||
if action == "folder":
|
||||
return _open_folder_screen(state, state.dest)
|
||||
if action == "fromcard":
|
||||
return _open_from_browser(state)
|
||||
if action == "misplaced":
|
||||
return _push(state, "misplaced")
|
||||
# On the first system, so A continues and the D-pad ticks.
|
||||
return _push(state, action, 3 if action == "systems" else 0)
|
||||
if screen == "systems":
|
||||
@@ -9662,12 +9697,27 @@ def _option(state: TuiState, action: str, step: int) -> list[tuple]:
|
||||
return _push(state, action)
|
||||
elif action == "folder":
|
||||
return _open_folder_screen(state, state.dest)
|
||||
elif action == "from" and step < 0:
|
||||
if state.from_folders:
|
||||
state.from_folders.pop()
|
||||
return _start_check(state)
|
||||
elif action == "from":
|
||||
return _open_from_browser(state)
|
||||
elif action == "preview":
|
||||
_push(state, "preview")
|
||||
return _start_run(state, "preview")
|
||||
return []
|
||||
|
||||
|
||||
def _open_from_browser(state: TuiState) -> list[tuple]:
|
||||
"""The browser, to name an old card, a backup or a ZIP to copy from."""
|
||||
state.browse_for = "from"
|
||||
_push(state, "browse")
|
||||
start = state.from_folders[-1].parent if state.from_folders else state.home
|
||||
_browse_to(state, _nearest_folder(start))
|
||||
return []
|
||||
|
||||
|
||||
def _toggle_region(state: TuiState, name: str) -> None:
|
||||
if name in state.regions:
|
||||
state.regions.remove(name)
|
||||
@@ -9913,7 +9963,8 @@ def tui_update(state: TuiState, key: TuiKey) -> list[tuple]:
|
||||
return _activate(state, rows[0 if char == "a" else 1])
|
||||
elif screen == "options" and name in ("left", "right"):
|
||||
return _option(state, row.action, 1 if name == "right" else -1) \
|
||||
if row.action in ("game_data", "installed", "jobs") else []
|
||||
if row.action in ("game_data", "installed", "jobs") \
|
||||
or (row.action == "from" and name == "left") else []
|
||||
elif name == "enter" or char == " ":
|
||||
return _activate(state, row)
|
||||
state.focus[screen] = focus
|
||||
@@ -9924,6 +9975,10 @@ def _take_event(state: TuiState, event: dict) -> None:
|
||||
kind = event.get("event")
|
||||
if kind == "checking" and state.run in ("check", "install"):
|
||||
state.reading = (event["files_done"], event["files_total"])
|
||||
elif kind == "found" and state.run == "check":
|
||||
state.found.append(event)
|
||||
elif kind == "found" and state.run == "install":
|
||||
state.local_dests.add(event["dest"])
|
||||
elif state.run == "check":
|
||||
if kind == "check":
|
||||
state.check = event
|
||||
@@ -9943,9 +9998,13 @@ def _take_event(state: TuiState, event: dict) -> None:
|
||||
state.download = event
|
||||
elif kind in ("progress", "downloaded"):
|
||||
state.progress = event
|
||||
if kind == "downloaded" and event["dest"] in state.local_dests:
|
||||
# A copy from an old card or a folder: its bytes come from
|
||||
# this machine, and a speed counting them was not the network's.
|
||||
state.local_done += event["size"]
|
||||
state.samples = [sample for sample in state.samples
|
||||
if sample[0] >= state.now - 10] + [
|
||||
(state.now, event["bytes_done"])]
|
||||
(state.now, event["bytes_done"] - state.local_done)]
|
||||
if kind == "downloaded":
|
||||
state.finished.append((event["dest"], event["ok"], event["reason"]))
|
||||
elif kind == "summary":
|
||||
@@ -10037,6 +10096,7 @@ _ROW_VERBS = {
|
||||
"region": "Tick", "regions": "Open", "cores": "Open", "command": "Open",
|
||||
"preview": "Open", "folder": "Choose", "game_data": "Change", "installed": "Change",
|
||||
"jobs": "Change", "retry": "Try again", "back": "Back", "stay": "Stay",
|
||||
"fromcard": "Choose", "from": "Add a folder", "misplaced": "Show", "zip": "Copy from it",
|
||||
}
|
||||
|
||||
|
||||
@@ -10120,18 +10180,46 @@ def _pager(state: TuiState, width: int, height: int, items: list) -> list[TuiLin
|
||||
return lines + [_styled(f"{first + 1}-{max(first + 1, whole)} of {len(items)}", "dim")]
|
||||
|
||||
|
||||
def _tui_misplaced(state: TuiState) -> list[dict]:
|
||||
"""The files the BIOS folder holds at another place, each with its reason."""
|
||||
return [event for event in state.found if event["reason"]]
|
||||
|
||||
|
||||
def _tui_found_elsewhere(state: TuiState) -> dict[str, list[dict]]:
|
||||
"""The files a --from folder holds, by the folder as it was named."""
|
||||
found: dict[str, list[dict]] = {}
|
||||
for event in state.found:
|
||||
if not event["reason"]:
|
||||
found.setdefault(event["folder"], []).append(event)
|
||||
return found
|
||||
|
||||
|
||||
def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
|
||||
need = _needed(check)
|
||||
data = check.get("data_missing", 0)
|
||||
# A file found on this machine is copied, not downloaded: the count and
|
||||
# the size to download leave it out, from the check's own events.
|
||||
local = {event["dest"] for event in state.found} if check is state.check else set()
|
||||
sizes = {event["dest"]: event["size"] for event in state.files}
|
||||
copied = [dest_ for dest_ in local if dest_ in sizes]
|
||||
fetch = max(0, need - len(copied))
|
||||
fetch_bytes = max(0, check["bytes_needed"] - sum(sizes[dest_] for dest_ in copied))
|
||||
elsewhere = _tui_found_elsewhere(state) if check is state.check else {}
|
||||
if elsewhere:
|
||||
where = ", ".join(f"{len(events)} found in {folder}"
|
||||
for folder, events in elsewhere.items())
|
||||
headline = (f"{where}, {fetch} to download ({format_size(fetch_bytes)})"
|
||||
if fetch else f"{where}, nothing to download")
|
||||
elif need and fetch:
|
||||
headline = f"{_plural(fetch, 'file')} to download, {format_size(fetch_bytes)}"
|
||||
elif need:
|
||||
headline = f"{_plural(len(copied), 'file')} to copy from this machine"
|
||||
if need:
|
||||
headline = (f"{_plural(need, 'file')} to download, "
|
||||
f"{format_size(check['bytes_needed'])}")
|
||||
if data:
|
||||
headline += f", and {_plural(data, 'data file')}"
|
||||
elif data:
|
||||
headline = f"{_plural(data, 'data file')} to fetch"
|
||||
else:
|
||||
headline = "Everything is in place"
|
||||
if not need:
|
||||
headline = (f"{_plural(data, 'data file')} to fetch" if data
|
||||
else "Everything is in place")
|
||||
wrong = check["wrong_hash"]
|
||||
lines: list = [
|
||||
_styled(headline, "title"),
|
||||
@@ -10147,6 +10235,13 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
|
||||
"choose another folder in Options.")
|
||||
if wrong:
|
||||
lines.append("They are replaced, and the old copies are kept in a backup folder.")
|
||||
misplaced = _tui_misplaced(state) if check is state.check else []
|
||||
if misplaced:
|
||||
lines.insert(1, f"{_plural(len(misplaced), 'file')} "
|
||||
f"{'is' if len(misplaced) == 1 else 'are'} in the wrong place, "
|
||||
f"{'it' if len(misplaced) == 1 else 'they'} will be copied where the "
|
||||
"emulators read "
|
||||
f"{'it' if len(misplaced) == 1 else 'them'}.")
|
||||
if state.facts.get("writable") is False:
|
||||
lines.insert(1, "You cannot write to this folder: choose another one, or run "
|
||||
+ ("the installer as an administrator." if state.os_type == "windows"
|
||||
@@ -10202,7 +10297,10 @@ def _view_folder(state: TuiState, width: int, height: int) -> tuple:
|
||||
def _view_browse(state: TuiState, width: int, height: int) -> tuple:
|
||||
where = "Drives" if state.browse_dir is None else middle(str(state.browse_dir), width)
|
||||
after = [state.status] if state.status else []
|
||||
return ("Browse for a folder",
|
||||
if state.browse_for == "from":
|
||||
after.insert(0, "Choose the old card, a backup folder or a ZIP: its BIOS files "
|
||||
"are copied, each one checked, nothing on it is changed.")
|
||||
return ("Copy from" if state.browse_for == "from" else "Browse for a folder",
|
||||
_compose(state, width, height, [where], tui_rows(state), after),
|
||||
("A/Enter Open", "B/Esc Back", "? Help"))
|
||||
|
||||
@@ -10273,6 +10371,9 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple:
|
||||
if state.status:
|
||||
lines.append(state.status)
|
||||
rows = tui_rows(state)
|
||||
if width < 54:
|
||||
rows = [row._replace(label="Copy from your old card or a folder")
|
||||
if row.action == "fromcard" else row for row in rows]
|
||||
return (display_name(state.platform, state.manifest),
|
||||
_compose(state, width, height, lines, rows),
|
||||
("A/Enter Install", "B/Esc Back", "Y/o Options", "? Help"))
|
||||
@@ -10357,6 +10458,8 @@ _STATE_WORDS = {"missing": "missing", "wrong_hash": "wrong copy"}
|
||||
|
||||
def _pager_items(state: TuiState) -> list:
|
||||
"""The text of a screen that scrolls, the same for its view and its keys."""
|
||||
if state.screen == "misplaced":
|
||||
return [f"{event['dest']}: {event['reason']}" for event in _tui_misplaced(state)]
|
||||
if state.screen == "result":
|
||||
return tui_outcome(state)[1]
|
||||
if state.screen == "command":
|
||||
@@ -10381,6 +10484,11 @@ def _view_preview(state: TuiState, width: int, height: int) -> tuple:
|
||||
("A/Enter Back", "Up/Down Scroll"))
|
||||
|
||||
|
||||
def _view_misplaced(state: TuiState, width: int, height: int) -> tuple:
|
||||
return ("Files in the wrong place", _pager(state, width, height, _pager_items(state)),
|
||||
("A/Enter Back", "Up/Down Scroll"))
|
||||
|
||||
|
||||
def _view_command(state: TuiState, width: int, height: int) -> tuple:
|
||||
return ("Command line", _pager(state, width, height, _pager_items(state)),
|
||||
("A/Enter Back",))
|
||||
@@ -10413,9 +10521,10 @@ def _view_installing(state: TuiState, width: int, height: int) -> tuple:
|
||||
lines.append(_bar(min(done / total, 1.0) if total else 1.0, width))
|
||||
lines.append(f"{format_size(done)} of {format_size(total)}")
|
||||
window = [sample for sample in state.samples if sample[0] >= state.now - 5]
|
||||
start, base = window[0] if window else (state.now, done)
|
||||
fetched = done - state.local_done
|
||||
start, base = window[0] if window else (state.now, fetched)
|
||||
seconds = state.now - start
|
||||
speed = (done - base) / seconds if seconds >= 1 else 0
|
||||
speed = (fetched - base) / seconds if seconds >= 1 else 0
|
||||
if speed > 0:
|
||||
lines.append(f"{format_size(int(speed))}/s, "
|
||||
f"{format_duration((total - done) / speed)} left")
|
||||
@@ -10544,7 +10653,7 @@ _TUI_VIEWS: dict[str, Callable[[TuiState, int, int], tuple]] = {
|
||||
"options": _view_options, "regions": _view_regions, "cores": _view_cores,
|
||||
"preview": _view_preview, "command": _view_command, "confirm": _view_confirm,
|
||||
"installing": _view_installing, "result": _view_result, "error": _view_error,
|
||||
"quit": _view_quit, "cardroot": _view_cardroot,
|
||||
"quit": _view_quit, "cardroot": _view_cardroot, "misplaced": _view_misplaced,
|
||||
}
|
||||
|
||||
|
||||
@@ -10665,10 +10774,6 @@ def _tui_passthrough(args: argparse.Namespace) -> list[tuple[str, str | None]]:
|
||||
flags.append((flag, str(Path(value).absolute())))
|
||||
flags += [("--emulator", name) for name in args.emulator or []]
|
||||
flags += [("--from-pack", str(Path(pack).absolute())) for pack in args.from_pack or []]
|
||||
# The handheld launchers start --tui --from the other card: without it here
|
||||
# the runs downloaded what that card holds.
|
||||
flags += [("--from", str(Path(folder).absolute()))
|
||||
for folder in getattr(args, "from", None) or []]
|
||||
for flag in ("--every-board", "--standalone-copies", "--verbose"):
|
||||
if getattr(args, flag[2:].replace("-", "_")):
|
||||
flags.append((flag, None))
|
||||
@@ -10683,6 +10788,7 @@ def tui_start(
|
||||
state.cli_systems = list(args.system or [])
|
||||
state.regions = resolve_regions(args.region) if args.region else []
|
||||
state.cli_regions = list(state.regions)
|
||||
state.from_folders = [Path(folder).absolute() for folder in getattr(args, "from") or []]
|
||||
state.cores = list(dict.fromkeys(
|
||||
token.strip() for value in args.core or [] for token in value.split(",")
|
||||
if token.strip()))
|
||||
|
||||
+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="163a216c1b09e8d788d4de6148194705813d4c6b8a956c84a70a267205f2c25d"
|
||||
DEFAULT_INSTALL_SHA256="bcbde4c289b32c02ada78001b75274a87b125c1efd3d8fb1064d7f5482ef3997"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
+1
-1
@@ -8,6 +8,7 @@
|
||||
Cores: all --core
|
||||
Parallel downloads: 8 --jobs
|
||||
BIOS folder: ~/.config/retroarch/system --dest
|
||||
Copy from: none --from
|
||||
Preview the file list --dry-run
|
||||
Show the command line
|
||||
|
||||
@@ -34,7 +35,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Open B/Esc Back Left/Right Set ? Help
|
||||
Vendored
+1
-1
@@ -6,7 +6,7 @@
|
||||
Cores: all
|
||||
Parallel downloads: 8
|
||||
BIOS folder: ~/.config...ch/system
|
||||
Copy from: none
|
||||
Preview the file list
|
||||
Show the command line
|
||||
|
||||
A Open B Back <> Set ? Help
|
||||
Vendored
+1
-1
@@ -6,10 +6,10 @@
|
||||
Cores: all
|
||||
Parallel downloads: 8
|
||||
BIOS folder: ~/.config...ch/system
|
||||
Copy from: none
|
||||
Preview the file list
|
||||
Show the command line
|
||||
|
||||
Sets --region
|
||||
|
||||
|
||||
A Open B Back <> Set ? Help
|
||||
Vendored
+1
-1
@@ -8,6 +8,7 @@
|
||||
Cores: all --core
|
||||
Parallel downloads: 8 --jobs
|
||||
BIOS folder: ~/.config/retroarch/system --dest
|
||||
Copy from: none --from
|
||||
Preview the file list --dry-run
|
||||
Show the command line
|
||||
|
||||
@@ -20,5 +21,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Open B/Esc Back Left/Right Set ? Help
|
||||
Vendored
+1
-1
@@ -9,6 +9,7 @@
|
||||
|
||||
> Install
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
@@ -34,7 +35,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Install B/Esc Back Y/o Options ? Help
|
||||
Vendored
+1
-1
@@ -4,9 +4,9 @@
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
Free space: 50.0 GB
|
||||
> Install
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
A Install B Back Y Options ? Help
|
||||
Vendored
+1
-1
@@ -7,9 +7,9 @@
|
||||
Free space: 50.0 GB
|
||||
They are replaced, and the old copies
|
||||
are kept in a backup folder.
|
||||
|
||||
> Install
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
A Install B Back Y Options ? Help
|
||||
Vendored
+1
-1
@@ -9,6 +9,7 @@
|
||||
|
||||
> Install
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
@@ -20,5 +21,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Install B/Esc Back Y/o Options ? Help
|
||||
@@ -331,20 +331,30 @@ class EventsForAFolder(FromFolder):
|
||||
class TheInterfaceReadsAFoundEvent(unittest.TestCase):
|
||||
"""The full-screen interface drives the engine through --events. A
|
||||
found event, which an install writes for a misplaced file even without
|
||||
--from, leaves its screens as they were."""
|
||||
--from, is what the plan counts after a check and what the download
|
||||
speed leaves out during an install. Elsewhere it changes nothing."""
|
||||
|
||||
FOUND = {"event": "found", "platform": "retroarch", "dest": "scph5501.bin",
|
||||
"folder": "/bios", "path": "/bios/bios/scph5501.bin", "member": "",
|
||||
"reason": "nested folder: it is at bios/bios/scph5501.bin"}
|
||||
|
||||
def test_every_run_ignores_it(self):
|
||||
for run in ("check", "confirm", "preview", "install"):
|
||||
def receive(self, run: str) -> tuple:
|
||||
state = install.TuiState()
|
||||
state.run, state.run_id, state.running = run, 3, True
|
||||
before = copy.deepcopy(vars(state))
|
||||
self.assertEqual(install.tui_receive(state, ("event", 3, dict(self.FOUND))), [])
|
||||
return state, before
|
||||
|
||||
def test_a_check_counts_it_and_an_install_copies_it(self):
|
||||
state, _before = self.receive("check")
|
||||
self.assertEqual(state.found, [self.FOUND])
|
||||
state, _before = self.receive("install")
|
||||
self.assertEqual(state.local_dests, {"scph5501.bin"})
|
||||
|
||||
def test_a_dry_run_ignores_it(self):
|
||||
for run in ("confirm", "preview"):
|
||||
with self.subTest(run=run):
|
||||
state = install.TuiState()
|
||||
state.run, state.run_id, state.running = run, 3, True
|
||||
before = copy.deepcopy(vars(state))
|
||||
self.assertEqual(install.tui_receive(state, ("event", 3, dict(self.FOUND))),
|
||||
[])
|
||||
state, before = self.receive(run)
|
||||
self.assertEqual(vars(state), before)
|
||||
|
||||
|
||||
|
||||
+161
-11
@@ -75,6 +75,7 @@ def _args(**given) -> argparse.Namespace:
|
||||
no_input=False, dry_run=False, yes=False, quiet=False, events=False,
|
||||
restore=None, from_pack=None, manifest=None, tui=False, plain=False,
|
||||
)
|
||||
values["from"] = None
|
||||
values.update(given)
|
||||
return argparse.Namespace(**values)
|
||||
|
||||
@@ -400,18 +401,22 @@ class Flow(unittest.TestCase):
|
||||
self.assertEqual(state.exit_code, 0)
|
||||
|
||||
def test_every_run_reads_the_folders_given_with_from(self):
|
||||
"""--tui --from OLD: the check and the install both copy from OLD."""
|
||||
"""--tui --from OLD: the check and the install both copy from OLD,
|
||||
each folder named once."""
|
||||
args = _args(**{"from": [Path("old-card"), Path("backup.zip")],
|
||||
"from_pack": [Path("pack.zip")]})
|
||||
flags = install._tui_passthrough(args)
|
||||
self.assertIn(("--from", str(Path("old-card").absolute())), flags)
|
||||
self.assertIn(("--from", str(Path("backup.zip").absolute())), flags)
|
||||
self.assertIn(("--from-pack", str(Path("pack.zip").absolute())), flags)
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]):
|
||||
started, _effects = install.tui_start(args, "linux")
|
||||
state = checked_state()
|
||||
state.passthrough = flags
|
||||
state.passthrough = started.passthrough
|
||||
state.from_folders = started.from_folders
|
||||
for run in ("check", "install"):
|
||||
self.assertIn(f"--from={Path('old-card').absolute()}",
|
||||
install.tui_argv(state, run, "py", "i.py"))
|
||||
argv = install.tui_argv(state, run, "py", "i.py")
|
||||
for folder in ("old-card", "backup.zip"):
|
||||
self.assertEqual(argv.count(f"--from={Path(folder).absolute()}"), 1, argv)
|
||||
self.assertIn(f"--from-pack={Path('pack.zip').absolute()}", argv)
|
||||
|
||||
def test_the_check_runs_with_the_command_line_choices(self):
|
||||
state = fresh_state()
|
||||
@@ -468,7 +473,7 @@ class Flow(unittest.TestCase):
|
||||
rows = install.tui_rows(state)
|
||||
self.assertEqual([row.right for row in rows], [
|
||||
"--region", "--no-game-data", "--installed-cores", "--core", "--jobs", "--dest",
|
||||
"--dry-run", ""])
|
||||
"--from", "--dry-run", ""])
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.screen, "regions")
|
||||
# Ticked in the order of preference, then moved up.
|
||||
@@ -512,7 +517,7 @@ class Flow(unittest.TestCase):
|
||||
def test_preview_is_a_dry_run(self):
|
||||
state = checked_state()
|
||||
press(state, "o")
|
||||
state.focus["options"] = 6
|
||||
state.focus["options"] = 7
|
||||
self.assertEqual(press(state, "enter"), [("run", "preview")])
|
||||
self.assertEqual(install.tui_argv(state, "preview", "py", "i.py")[-2:],
|
||||
["--dry-run", "--events"])
|
||||
@@ -683,7 +688,7 @@ class Flow(unittest.TestCase):
|
||||
def test_events_of_an_abandoned_run_are_ignored(self):
|
||||
state = checked_state()
|
||||
press(state, "o")
|
||||
state.focus["options"] = 6
|
||||
state.focus["options"] = 7
|
||||
press(state, "enter")
|
||||
old = state.run_id
|
||||
self.assertEqual(press(state, "esc"), [("kill",)])
|
||||
@@ -1590,6 +1595,121 @@ class WorkFolders(unittest.TestCase):
|
||||
self.assertFalse(work.exists())
|
||||
|
||||
|
||||
def found(dest: str, folder: str, reason: str = "") -> dict:
|
||||
return {"event": "found", "platform": "retroarch", "dest": dest, "folder": folder,
|
||||
"path": f"{folder}/{dest}", "member": "", "reason": reason}
|
||||
|
||||
|
||||
class CopyFromAnOldCard(unittest.TestCase):
|
||||
"""--from in the interface: a row on the plan, the browser, the counts."""
|
||||
|
||||
def recheck(self, state: install.TuiState, events: list) -> None:
|
||||
for event in [*FILES, *SYSTEMS, *events, CHECK]:
|
||||
install.tui_receive(state, ("event", state.run_id, event))
|
||||
with mock.patch.object(install, "_tui_folder_facts", return_value={
|
||||
"free": 50 * 1024 ** 3, "writable": True, "cores": []}):
|
||||
install.tui_receive(state, ("ended", state.run_id, 1, []))
|
||||
|
||||
def test_the_plan_offers_it_and_the_browser_adds_the_folder(self):
|
||||
old = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, old, True)
|
||||
(old / "bios").mkdir()
|
||||
(old / "backup.zip").write_bytes(b"PK")
|
||||
state = checked_state()
|
||||
row = next(i for i, r in enumerate(install.tui_rows(state)) if r.action == "fromcard")
|
||||
self.assertEqual(install.tui_rows(state)[row].label,
|
||||
"Copy the BIOS files from your old card or a folder")
|
||||
state.focus["plan"] = row
|
||||
state.home = old
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]):
|
||||
press(state, "enter")
|
||||
self.assertEqual((state.screen, state.browse_for), ("browse", "from"))
|
||||
labels = [r.label for r in install.tui_rows(state)]
|
||||
self.assertEqual(labels, ["Use this folder", "..", "bios/", "backup.zip"])
|
||||
self.assertIn("Choose the old card", "\n".join(install.tui_view(state, 80, 24)))
|
||||
effects = press(state, "enter")
|
||||
self.assertEqual(effects, [("run", "check")])
|
||||
self.assertEqual(state.from_folders, [old])
|
||||
self.assertIn(f"--from={old}", install.tui_argv(state, "check", "py", "i.py"))
|
||||
self.recheck(state, [found("scph5500.bin", str(old)), found("gba_bios.bin", str(old))])
|
||||
self.assertEqual(state.screen, "plan")
|
||||
text = "".join("".join(install.tui_view(state, 120, 40)).split())
|
||||
self.assertIn("".join(f"2 found in {old}, 2 to download (512.0 KB)".split()), text)
|
||||
# A second folder from the options: --from twice.
|
||||
press(state, "o")
|
||||
state.focus["options"] = next(i for i, r in enumerate(install.tui_rows(state))
|
||||
if r.action == "from")
|
||||
self.assertEqual(install.tui_rows(state)[state.focus["options"]].right, "--from")
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]):
|
||||
press(state, "enter")
|
||||
# It opens beside the first folder: the second card sits next to it.
|
||||
self.assertEqual(state.browse_dir, old.parent)
|
||||
install._browse_to(state, old)
|
||||
state.focus["browse"] = 3
|
||||
self.assertEqual(press(state, "enter"), [("run", "check")])
|
||||
self.assertEqual(state.from_folders, [old, old / "backup.zip"])
|
||||
self.assertEqual([flag for flag in install.tui_argv(state, "check", "py", "i.py")
|
||||
if flag.startswith("--from=")],
|
||||
[f"--from={old}", f"--from={old / 'backup.zip'}"])
|
||||
|
||||
def test_left_takes_the_last_folder_back(self):
|
||||
state = checked_state()
|
||||
state.from_folders = [Path("/media/old")]
|
||||
state.stack.append("options")
|
||||
state.focus["options"] = next(i for i, r in enumerate(install.tui_rows(state))
|
||||
if r.action == "from")
|
||||
self.assertEqual(press(state, "left"), [("run", "check")])
|
||||
self.assertEqual(state.from_folders, [])
|
||||
|
||||
def test_the_command_line_folders_are_kept(self):
|
||||
with mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]), \
|
||||
mock.patch.object(install, "_mounted_cards", return_value=[]):
|
||||
args = _args(platform="retroarch", dest=SYSTEM_DIR)
|
||||
setattr(args, "from", [Path("/media/old")])
|
||||
state, _effects = install.tui_start(args, "linux")
|
||||
self.assertEqual(state.from_folders, [Path("/media/old")])
|
||||
self.assertIn(("--from", "/media/old"), install.tui_flags(state))
|
||||
|
||||
def test_files_in_the_wrong_place_are_said_and_listed(self):
|
||||
state = checked_state()
|
||||
state.platform, state.dest = "retroarch", SYSTEM_DIR
|
||||
install._start_check(state)
|
||||
self.recheck(state, [found("scph5500.bin", str(SYSTEM_DIR),
|
||||
"nested folder: bios/scph5500.bin")])
|
||||
text = " ".join(line.strip() for line in install.tui_view(state, 120, 40))
|
||||
self.assertIn("1 file is in the wrong place, it will be copied where the emulators "
|
||||
"read it.", text)
|
||||
row = next(i for i, r in enumerate(install.tui_rows(state)) if r.action == "misplaced")
|
||||
state.focus["plan"] = row
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.screen, "misplaced")
|
||||
self.assertIn("scph5500.bin: nested folder: bios/scph5500.bin",
|
||||
"\n".join(install.tui_view(state, 80, 24)))
|
||||
press(state, "esc")
|
||||
self.assertEqual(state.screen, "plan")
|
||||
|
||||
def test_a_local_copy_is_no_download_speed(self):
|
||||
state = checked_state()
|
||||
press(state, "enter")
|
||||
mega = 1024 * 1024
|
||||
events = [(9.0, {"event": "found", **found("big.bin", "/media/old")}),
|
||||
(9.0, {"event": "download", "platform": "retroarch", "files": 2,
|
||||
"bytes": 300 * mega}),
|
||||
(10.0, {"event": "downloaded", "dest": "big.bin", "ok": True, "reason": "",
|
||||
"size": 200 * mega, "files_done": 1, "files_total": 2,
|
||||
"bytes_done": 200 * mega, "bytes_total": 300 * mega}),
|
||||
(11.0, {"event": "progress", "files_done": 1, "files_total": 2,
|
||||
"bytes_done": 201 * mega, "bytes_total": 300 * mega}),
|
||||
(12.0, {"event": "progress", "files_done": 1, "files_total": 2,
|
||||
"bytes_done": 202 * mega, "bytes_total": 300 * mega})]
|
||||
for when, event in events:
|
||||
state.now = when
|
||||
install.tui_receive(state, ("event", state.run_id, event))
|
||||
text = "\n".join(install.tui_view(state, 80, 24))
|
||||
self.assertIn("1.0 MB/s, 1:38 left", text)
|
||||
|
||||
|
||||
class Crash(unittest.TestCase):
|
||||
"""An unexpected error ends on one sentence and a log, never a traceback."""
|
||||
|
||||
@@ -1890,6 +2010,36 @@ class EndToEnd(unittest.TestCase):
|
||||
time.sleep(1)
|
||||
self.assertEqual(len(repo.installed()), on_disk)
|
||||
|
||||
def test_copy_from_an_old_card(self):
|
||||
repo = self.repo()
|
||||
old = Path(self.env["HOME"]) / "oldcard"
|
||||
(old / "bios").mkdir(parents=True)
|
||||
for name in ("scph5500.bin", "scph5501.bin", "gba_bios.bin"):
|
||||
payload = FakeRepository.PAYLOADS[name][0]
|
||||
(old / "bios" / name).write_bytes(payload)
|
||||
term = self.start("--platform", "retroarch", "--dest", str(repo.dest))
|
||||
term.wait("5 files to download")
|
||||
term.send(DOWN, DOWN, "\r")
|
||||
term.wait("Choose the old card")
|
||||
for _ in range(12):
|
||||
if "> oldcard/" in term.text():
|
||||
break
|
||||
term.send(DOWN)
|
||||
term.send("\r")
|
||||
term.wait("> Use this folder")
|
||||
term.send("\r")
|
||||
screen = term.wait("found in")
|
||||
# The folder wraps at a slash: compared without the spaces.
|
||||
self.assertIn("".join(f"3 found in {old}, 2 to download (8 B)".split()),
|
||||
"".join(screen.split()))
|
||||
# Checked again, the plan opens on Install.
|
||||
term.send("\r")
|
||||
term.wait("files installed.")
|
||||
term.send("\r")
|
||||
self.assertEqual(term.close(), 0)
|
||||
self.assertEqual(len(repo.installed()), 5)
|
||||
self.assertIn(f"--from {old}", term.after().replace("\n", " "))
|
||||
|
||||
def test_ctrl_c_on_the_first_screen(self):
|
||||
self.repo()
|
||||
term = self.start()
|
||||
|
||||
+6
-1
@@ -92,7 +92,12 @@ play, with the total to download under the list. **Options** sets the
|
||||
preferred regions, game data, the installed cores only, the cores, the parallel
|
||||
downloads and the folder. Each row names the option it sets, and **Show the
|
||||
command line** gives the same choice as one command for a script. **Preview the
|
||||
file list** shows what would be downloaded and writes nothing. Back on the plan
|
||||
file list** shows what would be downloaded and writes nothing. **Copy the BIOS
|
||||
files from your old card or a folder** opens the folder browser, mounted cards
|
||||
first, and adds [`--from`](#copy-from-an-old-card): the plan then says how many
|
||||
files that folder holds and how many are left to download. Files the BIOS
|
||||
folder holds at the wrong place are counted on the plan, and **Show the files
|
||||
in the wrong place** lists each one with its reason. Back on the plan
|
||||
after a change of options, the folder is checked again, so its numbers are the
|
||||
exact ones for the new choice. Ticked systems get their exact count before the
|
||||
install starts.
|
||||
|
||||
Reference in new issue
Block a user