feat: paint the installer with colour roles

One table of roles from the 16 basic colours (accent, success,
warning, failure, muted, strong, selected) read by the full screen and
by the end of a line-mode run. Status words take their role from one
pattern table, the text never changes, and colour is off with NO_COLOR,
TERM dumb or unset, a terminfo entry naming fewer than 8 colours, or an
output that is not a terminal.

Colour goldens keep the escape codes, and a test strips them back to
the text goldens.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 18:19:26 +01:00
1 parent 1f6c018006
commit 230885b77f
11 files changed
+2672 -120

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "749d14547ad7a5504ff6ee4c62aca2ec710839174e149df77759c70d3e4fe951"
$defaultInstallSha256 = "7998082004788f8d97177492ec371d8bdc507c5f132983ae05dc43be82d7e62a"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+337 -85
View File
@@ -338,12 +338,100 @@ def _enable_ansi() -> bool:
return False
# The roles text is painted with, from the 16 basic colours only: a
# handheld's Linux console has no more. The screens and the line questions
# name a role, never a code. Magenta is the hue that stays legible on black
# and on white in the xterm, VGA, Tango and Campbell palettes, so it is the
# accent, bold. Yellow reads poorly on white: it paints a status word, never
# a sentence. Bold red is bright red on the Linux console, where plain red on
# black is the weakest pair.
COLOUR_ROLES = {
"accent": "1;35",
"success": "32",
"warning": "33",
"failure": "1;31",
"muted": "2",
"strong": "1",
# The accent reversed: the row in focus, its > kept for no colour.
"selected": "7;35",
}
# The words a status takes its role from, wherever it is said. A count of
# zero is no news and stays plain: "0 missing" is not a warning.
_STATUS_WORDS = tuple((re.compile(pattern), role) for pattern, role in (
(r"(?<!of )\b[1-9][\d,]* (?:files? )?(?:installed|in place|already (?:up to date|in place)"
r"|up to date|ok)\b|\bDone\b|\bEverything is in place\b|\bAll files up to date\b"
r"|^ *ok\b", "success"),
(r"(?<!of )\b[1-9][\d,]* (?:files? )?(?:failed|refused)\b|\bFAILED\b|\bfailed:"
r"|\bFinished with errors\b|\bNothing was installed\b|\bSomething went wrong\b",
"failure"),
(r"(?<!of )\b[1-9][\d,]* (?:files? )?(?:missing|wrong cop(?:y|ies)|not fetched|left out"
r"|not downloaded)\b|\bFinished with files missing\b|\bNothing installed\b"
r"|\bStopped\b|\bLeft out\b|\bNot enough space\b", "warning"),
))
def _apart(spans: list[tuple[int, int, str]]) -> list[tuple[int, int, str]]:
"""The spans in order, none empty, the longer kept where two overlap."""
kept: list[tuple[int, int, str]] = []
for span in sorted(spans, key=lambda span: (span[0], -span[1])):
if span[1] > span[0] and (not kept or span[0] >= kept[-1][1]):
kept.append(span)
return kept
def status_spans(text: str) -> list[tuple[int, int, str]]:
"""(start, end, role) of the status words in text, in order, apart."""
return _apart([(match.start(), match.end(), role) for pattern, role in _STATUS_WORDS
for match in pattern.finditer(text)])
def _sgr(role: str) -> str:
"""The SGR sequence of a role, or of roles joined by + ("strong+success")."""
return "\033[0;" + ";".join(COLOUR_ROLES[name] for name in role.split("+")) + "m"
def painted(text: str, spans: list[tuple[int, int, str]], base: str = "") -> str:
"""text with each span in its role and the rest in the base role, a code
written where the role changes and the colours reset at the end. The
characters are those of text, in the same order. A span in "plain"
takes the terminal's own colours."""
pieces: list[tuple[str, str]] = []
at = 0
for start, end, role in spans:
pieces += [(text[at:start], base), (text[start:end], role)]
at = end
pieces.append((text[at:], base))
out: list[str] = []
current = ""
for piece, role in pieces:
role = "" if role == "plain" else role
if not piece:
continue
if role != current:
out.append(_sgr(role) if role else "\033[0m")
current = role
out.append(piece)
if current:
out.append("\033[0m")
return "".join(out)
_SGR = re.compile(r"\x1b\[[0-9;]*m")
def unpainted(text: str) -> str:
"""text without its SGR codes."""
return _SGR.sub("", text)
class Terminal:
"""What the output stream can show: a live line, colour, or neither.
A pipe or a log gets plain lines, and NO_COLOR and TERM=dumb or unset are
honoured, as is a terminal whose description moves no cursor the ANSI way
(vt52).
(vt52), or names fewer than 8 colours (vt100), which keeps the line live
and plain.
As no-color.org reads it, an empty NO_COLOR is not set: `NO_COLOR= cmd`
turned colour off. FORCE_COLOR or CLICOLOR_FORCE paints a pipe, for a
log viewer that shows colour, and never makes the line live.
@@ -362,25 +450,30 @@ class Terminal:
forced = any(os.environ.get(name, "0") not in ("", "0")
for name in ("FORCE_COLOR", "CLICOLOR_FORCE"))
self.colour = (
(self.live or forced) and not os.environ.get("NO_COLOR") and _enable_ansi()
((self.live and not terminal_few_colours(os.environ)) or forced)
and not os.environ.get("NO_COLOR") and _enable_ansi()
)
if not self.colour:
self.live = self.live and os.name != "nt"
def paint(self, text: str, code: str) -> str:
return f"\033[{code}m{text}\033[0m" if self.colour else text
def paint(self, text: str, role: str) -> str:
return f"\033[{COLOUR_ROLES[role]}m{text}\033[0m" if self.colour else text
def status(self, text: str) -> str:
"""text with its status words in their roles (status_spans)."""
return painted(text, status_spans(text)) if self.colour else text
def ok(self, text: str) -> str:
return self.paint(text, "32")
return self.paint(text, "success")
def bad(self, text: str) -> str:
return self.paint(text, "31")
return self.paint(text, "failure")
def dim(self, text: str) -> str:
return self.paint(text, "2")
return self.paint(text, "muted")
def bold(self, text: str) -> str:
return self.paint(text, "1")
return self.paint(text, "strong")
def format_duration(seconds: float) -> str:
@@ -5616,6 +5709,17 @@ NETWORK_FAILURE_ADVICE = (
f" More help: {TROUBLESHOOTING_PAGE}")
def _status_print(term: Terminal | None) -> Callable[..., None]:
"""print, with the status words of what it prints in their roles when
term paints (Terminal.status)."""
if term is None or not term.colour:
return print
def say(*parts: object, **kwargs: Any) -> None:
print(*(term.status(str(part)) for part in parts), **kwargs)
return say
def _report_outcome(
paths: list[Path],
downloaded: int,
@@ -5635,6 +5739,7 @@ def _report_outcome(
offline: int = 0,
kinds: list[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.
@@ -5645,8 +5750,9 @@ def _report_outcome(
the size question: counted with it, 14,696 files missing ended on Done
and exit 0 once 4 came from this machine. They end the run as files
that failed do, 1 when some were installed and 2 when none were, the
codes the full screen's run gives.
codes the full screen's run gives. term paints the status words.
"""
say = _status_print(term)
# A file --offline left out is not a failure, whether the run asked or
# not: without questions it was "14698 files failed", and "Run the same
# command again to retry only those", which cannot fetch them.
@@ -5658,22 +5764,22 @@ def _report_outcome(
offline += len(left)
if transferred and seconds > 0:
verb = "copied from the pack" if LOCAL.pack is not None else "downloaded"
print(f"\n{format_size(transferred)} {verb} in "
f"{format_duration(seconds)} ({format_size(int(transferred / seconds))}/s).")
say(f"\n{format_size(transferred)} {verb} in "
f"{format_duration(seconds)} ({format_size(int(transferred / seconds))}/s).")
reasons = {reason for _dest, reason in failures or []}
if failures and len(failures) > 1 and len(reasons) == 1 and reasons != {""}:
# One cause, said once: a read-only folder listed ten of its files
# and "... and 266 more" above the advice that fixes them all.
print(f" failed: {_plural(len(failures), 'file')} ({reasons.pop()}), "
f"first {failures[0][0]}")
say(f" failed: {_plural(len(failures), 'file')} ({reasons.pop()}), "
f"first {failures[0][0]}")
else:
for dest, reason in (failures or [])[:10]:
print(f" failed: {dest}" + (f" ({reason})" if reason else ""))
say(f" failed: {dest}" + (f" ({reason})" if reason else ""))
if failures and len(failures) > 10:
print(f" ... and {len(failures) - 10} more")
say(f" ... and {len(failures) - 10} more")
if declined and not downloaded and not errors and not interrupted and not offline:
print(f"\nNothing installed: {_plural(declined, 'file')} not downloaded, as answered.")
print(f"{_plural(up_to_date, 'file')} already up to date at {_locations(paths)}.")
say(f"\nNothing installed: {_plural(declined, 'file')} not downloaded, as answered.")
say(f"{_plural(up_to_date, 'file')} already up to date at {_locations(paths)}.")
return
# Left out for an unknown save directory, said with that reason in
# every ending: "the collection does not hold it" blamed the collection.
@@ -5684,10 +5790,10 @@ def _report_outcome(
f"{unplaced[0].get('frontend') or 'the frontend'}'s save directory left out: "
"its place is not known. Name it with --save-dir next time."] if unplaced else [])
if interrupted:
print(f"\nStopped. {_plural(downloaded, 'file')} installed.")
say(f"\nStopped. {_plural(downloaded, 'file')} installed.")
for line in kept:
print(line)
print("Run the same command again to carry on from here. Nothing is lost.")
say(line)
say("Run the same command again to carry on from here. Nothing is lost.")
sys.exit(130)
# A standalone copy fails by absolute path, in an emulator's folder
@@ -5720,86 +5826,86 @@ def _report_outcome(
if offline:
remedy = _offline_remedy()
if not downloaded and not errors:
print(f"\nNothing installed: {_plural(offline, 'file')} "
f"{'needs' if offline == 1 else 'need'} the internet, which "
f"{offline_named()} does not use.")
print(f" {remedy}")
say(f"\nNothing installed: {_plural(offline, 'file')} "
f"{'needs' if offline == 1 else 'need'} the internet, which "
f"{offline_named()} does not use.")
say(f" {remedy}")
for line in kept:
print(line)
say(line)
if up_to_date:
print(f"{_plural(up_to_date, 'file')} already up to date at "
f"{_locations(paths)}.")
say(f"{_plural(up_to_date, 'file')} already up to date at "
f"{_locations(paths)}.")
sys.exit(2)
failed = f", {errors} failed" if errors else ""
if downloaded:
print(f"\nFinished with files missing. {_plural(downloaded, 'file')} installed, "
f"{offline} not fetched ({offline_named()}){failed}.")
say(f"\nFinished with files missing. {_plural(downloaded, 'file')} installed, "
f"{offline} not fetched ({offline_named()}){failed}.")
else:
# Nothing written: "Finished with files missing. 0 files installed"
# read as a part done.
print(f"\nNothing installed: {_plural(offline, 'file')} not fetched "
f"({offline_named()}){failed}.")
say(f"\nNothing installed: {_plural(offline, 'file')} not fetched "
f"({offline_named()}){failed}.")
if sources:
print(f" {sources}")
say(f" {sources}")
# Under the files it is about: after the save-directory line, "They"
# read as that one file.
print(f" {remedy}")
print(f"Location: {_locations(paths)}")
say(f" {remedy}")
say(f"Location: {_locations(paths)}")
for line in kept + advice:
print(line)
say(line)
sys.exit(1 if downloaded else 2)
if errors and not downloaded:
print(f"\nNothing was installed. {_plural(errors, 'file')} failed.")
say(f"\nNothing was installed. {_plural(errors, 'file')} failed.")
if failures and all(isinstance(r, (NetworkReason, OfflineReason))
for _d, r in failures):
# The folder was never the problem: naming it as the place being
# written to sent people looking at their disk.
print(f"Destination: {_locations(paths)}")
say(f"Destination: {_locations(paths)}")
else:
print(f"Location that was being written to: {_locations(paths)}")
say(f"Location that was being written to: {_locations(paths)}")
for line in kept + advice:
print(line)
print("Run the same command again once that is resolved. "
"Files already installed are kept.")
say(line)
say("Run the same command again once that is resolved. "
"Files already installed are kept.")
sys.exit(2)
if errors:
print(f"\nFinished with errors. {_plural(downloaded, 'file')} installed, "
f"{_plural(errors, 'file')} failed.")
say(f"\nFinished with errors. {_plural(downloaded, 'file')} installed, "
f"{_plural(errors, 'file')} failed.")
if sources:
print(f" {sources}")
print(f"Location: {_locations(paths)}")
say(f" {sources}")
say(f"Location: {_locations(paths)}")
for line in kept + advice:
print(line)
say(line)
if not all(str(reason).startswith(NOT_KEPT) for _dest, reason in failures or []):
print("Run the same command again to retry only those.")
say("Run the same command again to retry only those.")
sys.exit(1)
print(f"\nDone. {_plural(downloaded, 'file')} installed, "
f"{_plural(up_to_date, 'file')} already up to date.")
say(f"\nDone. {_plural(downloaded, 'file')} installed, "
f"{_plural(up_to_date, 'file')} already up to date.")
if kinds and kinds[1]:
print(f" Installed: {kinds_words(*kinds)}.")
say(f" Installed: {kinds_words(*kinds)}.")
if sources:
print(f" {sources}")
print(f"Location: {_locations(paths)}")
say(f" {sources}")
say(f"Location: {_locations(paths)}")
for line in kept:
print(line)
say(line)
if omitted > len(unplaced):
for line in _omission_lines(omitted - len(unplaced),
[entry for entry in omitted_entries or []
if entry.get("reason") != "no_save_directory"],
Path(paths[0]) if len(paths) == 1 else None):
print(line)
say(line)
for line in hints or []:
print(line)
say(line)
if same_choice:
# After the questions, as the screen's result ends: "the same
# command" was one that asked them (tui_audit_11 P3-21).
print(f"Same choice without questions: {screen_text(same_choice)}")
print("To check this later, run that command with --check.")
say(f"Same choice without questions: {screen_text(same_choice)}")
say("To check this later, run that command with --check.")
return
print("To check this later, run the same command with --check.")
say("To check this later, run the same command with --check.")
def _offline_remedy(folders: list[str] | None = None) -> str:
@@ -6513,6 +6619,7 @@ def _install_standalone_folders(
_report_outcome(
[folder for folder, _entries in plan], downloaded, up_to_date_count, errors,
0, interrupted, failures=failures, transferred=transferred, seconds=seconds,
term=term,
)
return True
@@ -12440,6 +12547,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
sources=_run_sources(bios_indexes, total_downloaded,
total_errors + total_offline),
offline=total_offline,
term=term,
)
finish_app(args, platforms, app_cards, interactive and not total_declined)
@@ -12481,7 +12589,17 @@ _PASTE_END = "\x1b[201~"
# newline does not submit it half typed.
_SCREEN_ON = "\x1b[?1049h\x1b[?25l\x1b[?2004h\x1b[2J"
_SCREEN_OFF = "\x1b[?2004l\x1b[0m\x1b[?25h\x1b[?1049l"
_STYLES = {"title": "\x1b[1m", "focus": "\x1b[7m", "dim": "\x1b[2m"}
# How a line is drawn without colour: the screen's title and a headline
# bold, the row in focus reversed, the bar and secondary text dim.
_STYLES = {"title": "\x1b[1m", "head": "\x1b[1m", "focus": "\x1b[7m", "dim": "\x1b[2m",
"hint": "\x1b[2m"}
# The role each style is drawn in with colour (tui_paint).
_COLOUR_BASES = {"title": "strong", "head": "strong", "focus": "selected", "dim": "muted",
"hint": "muted", "refusal": "failure"}
# A count or a size on a headline: "4 files to download, 1.0 MB".
_KEY_NUMBER = re.compile(r"\b\d[\d,.]*(?: (?:B|KB|MB|GB|TB)\b)?")
_PROGRESS_BAR = re.compile(r"^ *\[(#*)(-*)\]")
_FOLDER_LABEL = "BIOS folder: "
class TuiKey(NamedTuple):
@@ -12753,7 +12871,8 @@ def _wrap(text: str, width: int, indent: str = " ", whole: str = "") -> list[st
class TuiLine(str):
"""A screen line and how it is drawn: "title", "focus", "dim" or plain."""
"""A screen line and how it is drawn: "head" (the screen's title),
"title", "focus", "dim", "hint" (the bar), "refusal" or plain."""
style = ""
@@ -12764,6 +12883,59 @@ def _styled(text: str, style: str = "") -> TuiLine:
return line
def _bar_keys() -> set[str]:
"""Every name a key or a button takes on the bar, on every route."""
keys = {"Enter", "Esc", "Space", "Start", "Select", "Ctrl-C", "?", "/"}
for route in TUI_ROUTES.values():
keys.update(route.bar)
keys.update(name for name in route.bar.values() if name)
keys.update(short.strip() for _long, short in _SHORT_KEYS)
return keys
def tui_paint(text: str, style: str, colour: bool) -> str:
"""A drawn line as the terminal gets it: its style, and with colour the
roles of its words (COLOUR_ROLES). The characters are those of text.
The title names RetroBIOS in the accent, a headline its counts and
sizes, the bar the key it names first, a line its status words, the
plan and the result the BIOS folder in bold.
"""
if not colour:
return _STYLES.get(style, "") + text
base = _COLOUR_BASES.get(style, "")
if style not in ("", "head", "title", "hint", "refusal"):
return painted(text, [], base)
spans: list[tuple[int, int, str]] = []
if style == "hint":
start = len(text) - len(text.lstrip(" "))
key = text[start:].split(" ", 1)[0]
if key in _bar_keys():
end = text.find(" ", start)
end = len(text) if end < 0 else end
spans = [(start, start + len(key), "accent"), (start + len(key), end, "plain")]
return painted(text, spans, base)
if style == "head":
at = text.find("RetroBIOS")
if at >= 0:
spans.append((at, at + len("RetroBIOS"), "accent"))
if style == "title":
spans += [(match.start(), match.end(), "accent")
for match in _KEY_NUMBER.finditer(text)]
lead = text.lstrip(" ")
if lead.startswith(_FOLDER_LABEL):
start = len(text) - len(lead) + len(_FOLDER_LABEL)
spans.append((start, len(text.rstrip(" ")), "strong"))
bar = _PROGRESS_BAR.match(text)
if bar:
spans += [(bar.start(1), bar.end(1), "accent"), (bar.start(2), bar.end(2), "muted")]
# On a bold line a status word stays bold.
spans += [(start, end, f"strong+{role}" if base == "strong"
and not COLOUR_ROLES[role].startswith("1;") else role)
for start, end, role in status_spans(text)]
return painted(text, _apart(spans), base)
def _short_path(path: Path | None, home: Path) -> str:
if path is None:
return ""
@@ -13103,6 +13275,8 @@ _TERMINFO_DIRS = ("/etc/terminfo", "/lib/terminfo", "/usr/share/terminfo",
"/usr/lib/terminfo", "/usr/share/lib/terminfo")
# cursor_address, string capability 10 of term(5).
_TERMINFO_CUP = 10
# max_colors, numeric capability 13.
_TERMINFO_COLORS = 13
def _terminfo_folders(env: Mapping[str, str]) -> list[Path]:
@@ -13114,19 +13288,47 @@ def _terminfo_folders(env: Mapping[str, str]) -> list[Path]:
return folders + [Path(folder) for folder in _TERMINFO_DIRS]
def _terminfo_header(data: bytes) -> tuple[int, int, int, int, int] | None:
"""Of a compiled term(5) entry: the width of a number, the counts of
names bytes, booleans, numbers and strings. None when data is none."""
if len(data) < 12:
return None
# The legacy format stores numbers in 2 bytes, the extended one in 4.
width = {0o432: 2, 0o1036: 4}.get(int.from_bytes(data[0:2], "little", signed=True))
if width is None:
return None
names, bools, numbers, strings = (int.from_bytes(data[at:at + 2], "little", signed=True)
for at in (2, 4, 6, 8))
return width, names, bools, numbers, strings
def terminfo_number(data: bytes, index: int) -> int | None:
"""Numeric capability index of a compiled term(5) entry, -1 when the
entry lacks it, None when data is no compiled entry."""
header = _terminfo_header(data)
if header is None:
return None
width, names, bools, numbers, _strings = header
if index >= numbers:
return -1
start = 12 + names + bools
start += start % 2 + index * width
if start + width > len(data):
return None
value = int.from_bytes(data[start:start + width], "little", signed=True)
return value if value >= 0 else -1
def terminfo_string(data: bytes, index: int) -> str | None:
"""String capability index of a compiled term(5) entry, "" when the
entry lacks it, None when data is no compiled entry."""
def short(at: int) -> int:
return int.from_bytes(data[at:at + 2], "little", signed=True)
if len(data) < 12:
header = _terminfo_header(data)
if header is None:
return None
# The legacy format stores numbers in 2 bytes, the extended one in 4.
width = {0o432: 2, 0o1036: 4}.get(short(0))
if width is None:
return None
names, bools, numbers, strings = short(2), short(4), short(6), short(8)
width, names, bools, numbers, strings = header
offsets = 12 + names + bools
offsets += offsets % 2 + numbers * width
if index >= strings:
@@ -13141,23 +13343,64 @@ def terminfo_string(data: bytes, index: int) -> str | None:
return data[start:end].decode("latin-1")
def terminal_cursor_address(term: str, env: Mapping[str, str]) -> str | None:
"""How TERM's description here moves the cursor, "" when it cannot,
None when no description is found: Windows, Android and many handheld
images carry none, and the screen is not held back for it."""
def _terminfo_data(term: str, env: Mapping[str, str]) -> bytes | None:
"""TERM's compiled description, the first found, None when none is:
Windows, Android and many handheld images carry none."""
if not term or "/" in term or term.startswith("."):
return None
for folder in _terminfo_folders(env):
# Linux files xterm under x/, macOS under 78/.
for sub in (term[0], f"{ord(term[0]):02x}"):
try:
data = (folder / sub / term).read_bytes()
return (folder / sub / term).read_bytes()
except (OSError, ValueError):
continue
return terminfo_string(data, _TERMINFO_CUP)
return None
def terminal_cursor_address(term: str, env: Mapping[str, str]) -> str | None:
"""How TERM's description here moves the cursor, "" when it cannot,
None when no description is found, and the screen is not held back
for it."""
data = _terminfo_data(term, env)
return None if data is None else terminfo_string(data, _TERMINFO_CUP)
def terminal_colours(term: str, env: Mapping[str, str]) -> int | None:
"""The colours TERM's description here names, 0 when it names none
(vt100), None when no description is found or it cannot be read."""
data = _terminfo_data(term, env)
if data is None:
return None
count = terminfo_number(data, _TERMINFO_COLORS)
return None if count is None else max(count, 0)
def terminal_few_colours(env: Mapping[str, str]) -> str:
"""Why TERM's description here leaves colour off, "" when it does not:
no description is no refusal, as for the cursor."""
if os.name == "nt":
return ""
term = env.get("TERM", "")
count = terminal_colours(term, env)
if count is not None and count < 8:
return f"TERM={term} names {count} colours, fewer than 8"
return ""
def colour_blocker(env: Mapping[str, str], stream: TextIO) -> str:
"""Why the full-screen interface draws without colour, "" when it paints
with the roles of COLOUR_ROLES. The text is the same either way."""
if env.get("NO_COLOR"):
return "NO_COLOR is set"
isatty = getattr(stream, "isatty", None)
if not (isatty and isatty()):
return "the output is not a terminal"
if os.name != "nt" and env.get("TERM", "") in ("", "dumb"):
return "TERM is dumb or not set"
return terminal_few_colours(env)
def terminal_without_ansi(env: Mapping[str, str]) -> str:
"""Why TERM's description here says the ANSI sequences the screen and
the progress line write would print as text, "" when it does not."""
@@ -13865,9 +14108,10 @@ def _start_check(state: TuiState) -> list[tuple]:
return [("fetch", state.platform)]
def _closed_status(state: TuiState, closed: tuple[Path, str]) -> str:
return (f"This user cannot open {_short_path(closed[0], state.home)} ({closed[1]}): "
"choose another folder, or give this user access to it.")
def _closed_status(state: TuiState, closed: tuple[Path, str]) -> TuiLine:
folder = _short_path(closed[0], state.home)
return _styled(f"This user cannot open {folder} ({closed[1]}): choose another folder, "
"or give this user access to it.", "refusal")
def _choose(state: TuiState, plat: str, dest: Path, proposed: bool = False) -> list[tuple]:
@@ -13968,7 +14212,7 @@ def _card_status(state: TuiState, choice: CardChoice, given: bool = False) -> No
else:
then = "Choose the card's top folder."
short = choice.short or choice.refused + "."
state.status = f"{short} {then}"
state.status = _styled(f"{short} {then}", "refusal")
state.status_more = f"{choice.refused}." + (f" {choice.detail}." if choice.detail else "")
@@ -14389,7 +14633,7 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]:
chosen = state.browse_dir if action == "use" else row.value
refused = copy_source_refusal(chosen, state.dest)
if refused:
state.status = refused
state.status = _styled(refused, "refusal")
return []
if chosen not in state.from_folders:
state.from_folders.append(chosen)
@@ -16084,7 +16328,8 @@ def _status_after(state: TuiState) -> list[str]:
# Said once: the full form repeated the short one under it. The help
# holds it, and a pad, with no button for the help, reads it here.
if state.pad in ("both", "keys"):
return [f"{state.status} {'F1' if state.screen == 'path' else '?'} says more."]
return [_styled(f"{state.status} {'F1' if state.screen == 'path' else '?'} says more.",
getattr(state.status, "style", ""))]
return [state.status, state.status_more]
@@ -16283,10 +16528,10 @@ def _view_systems(state: TuiState, width: int, height: int) -> tuple:
and cell_width(f"> {row.label} {row.value} {row.right}") + 2 <= width
else row for row in rows]
chosen = _ticked_files(state)
footer = [f"{len(state.ticked)} of {len(state.systems)} systems: "
f"{_plural(len(chosen), 'file')}, "
f"{format_size(sum(e['size'] for e in chosen))}"
+ (" before your options" if _counts_predate_options(state) else "")]
total = (f"{len(state.ticked)} of {len(state.systems)} systems: "
f"{_plural(len(chosen), 'file')}, {format_size(sum(e['size'] for e in chosen))}"
+ (" before your options" if _counts_predate_options(state) else ""))
footer = [_styled(total, "title")]
focused = rows[min(state.focus.get("systems", 0), len(rows) - 1)]
drawn = str(_row_line(focused, True, width))
if focused.action == "system" and not (
@@ -16749,6 +16994,9 @@ def tui_keeps_log(state: TuiState) -> bool:
def _view_result(state: TuiState, width: int, height: int) -> tuple:
title, lines = tui_outcome(state)
# The outcome in bold, its counts in their roles: it led a list of
# lines drawn alike.
lines[0] = _styled(lines[0], "title")
rows = tui_rows(state)
if rows:
return (title, _compose(state, width, height, lines, rows),
@@ -16944,14 +17192,14 @@ def tui_view(state: TuiState, cols: int, rows: int) -> list[str]:
# keys that end it, not Tick and Help.
hint = ("Enter Done", "Esc Clear")
hint = _route_hint(state, _back_hint(state, hint))
lines = [_styled(fit(f" RetroBIOS: {title}", cols), "title"), _styled("")]
lines = [_styled(fit(f" RetroBIOS: {title}", cols), "head"), _styled("")]
# A long line is drawn whole, the terminal folding it into the rows
# after it (_text_lines): cut to the width, a copied path lost its end.
lines += [_styled(" " + line if line.style == "long" else fit(" " + line, cols), line.style)
for line in body[:height]]
lines += [_styled("")] * (rows - 1 - len(lines))
# Never the last cell of the screen: a terminal may scroll when it is written.
lines.append(_styled(fit(" " + _hint(width, *hint), cols - 1), "dim"))
lines.append(_styled(fit(" " + _hint(width, *hint), cols - 1), "hint"))
return lines
@@ -17345,6 +17593,9 @@ class _TuiTerminal:
# Python turns a C locale into UTF-8 for itself, not for the
# terminal: a console font there has no CJK, often no accents.
self.ascii = not utf8 or locale_name in ("C", "POSIX")
# The same text without colour: NO_COLOR, a dumb or unset TERM, a
# description naming fewer than 8 colours, an output no terminal.
self.colour = not colour_blocker(os.environ, stdout) and _enable_ansi()
self.ended = False
self.heard = 0.0
self.frame: list[tuple[str, str]] = []
@@ -17410,7 +17661,8 @@ class _TuiTerminal:
text = ascii_text(text)
if style == "focus":
text = _pad(text, min(cols, TUI_MAX_WIDTH) - (index == len(frame) - 1))
out.append(f"\x1b[{index + 1};1H{_STYLES.get(style, '')}{text}\x1b[0m\x1b[K")
out.append(f"\x1b[{index + 1};1H"
f"{tui_paint(text, style, getattr(self, 'colour', False))}\x1b[0m\x1b[K")
self.frame = frame
if out:
self.write("".join(out))
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="749d14547ad7a5504ff6ee4c62aca2ec710839174e149df77759c70d3e4fe951"
DEFAULT_INSTALL_SHA256="7998082004788f8d97177492ec371d8bdc507c5f132983ae05dc43be82d7e62a"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 818600,
"sha256": "749d14547ad7a5504ff6ee4c62aca2ec710839174e149df77759c70d3e4fe951",
"size": 829229,
"sha256": "7998082004788f8d97177492ec371d8bdc507c5f132983ae05dc43be82d7e62a",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 871149,
"sha256": "cbca9dd0e3d7de656965dbff79512991856a03853c0e771da97e5e95d7d31f49"
"size": 881778,
"sha256": "7e5cc8505ce9c810888baf62260b7ac2ff1cfcbce2e13c44b99d15458969f38a"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 818600,
"sha256": "749d14547ad7a5504ff6ee4c62aca2ec710839174e149df77759c70d3e4fe951",
"size": 829229,
"sha256": "7998082004788f8d97177492ec371d8bdc507c5f132983ae05dc43be82d7e62a",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 818600,
"sha256": "749d14547ad7a5504ff6ee4c62aca2ec710839174e149df77759c70d3e4fe951",
"size": 829229,
"sha256": "7998082004788f8d97177492ec371d8bdc507c5f132983ae05dc43be82d7e62a",
"executable": false
},
{
+902
View File
@@ -0,0 +1,902 @@
=== setup
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where do you play?^[[0m
Found on this machine:
^[[0;7;35m > RetroArch^[[0m
KNULLI (Batocera files)
Another setup or folder
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
ES-DE found: its BIOS files go to each emulator, not to ES-DE itself.
^[[0;2m ^[[0;1;35mA/Enter^[[0m Continue^[[0;2m B/Esc Quit ? Help^[[0m
=== platforms
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which setup?^[[0m
Choose the setup that will use the BIOS files.
^[[0;2m Filter: setup or device (/)^[[0m
^[[0;7;35m > RetroArch^[[0m
Batocera
Recalbox
RetroBat
EmuDeck
Lakka
RetroDECK
RomM
BizHawk
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, Odroid Go Super, RG351V, +6
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, R35S, RGB10
CrossMix-OS, SD card TrimUI Smart Pro
dArkOS, SD card Odroid Go Advance, RGB10, RG351MP, G350
EmuELEC, SD card Odroid Go
JELOS, SD card RG351M, RG351V, RG353M, RG353P, RG353V, RG353VS, RG503, RG552, R33S, R36S, +24
KNULLI, SD card RG ARC-S, RG28XX, RG34XX, RG35XX, RG35XX Plus, RG35XX H, RG35XX 2024, +15
KOLOS, SD card RG353P, RG353M, RG353V, RG353VS, RG503, RK2023, RGB30, RGB10 Max 3
Koriki, SD card Miyoo Mini
MinUI, SD card RG28XX, RG34XX, RG34XX SP, RG35XX, RG35XX Plus, RG35XX H, RG35XX SP, +16
MiyooCFW, SD card BittBoy, Pocket Go, PowKiddy Q90, PowKiddy V90, PowKiddy Q20 Mini, Sup M3, +1
muOS, SD card RG35XX H, RG35XX Plus, RG35XX SP, RG35XX 2024, RG34XX, RG40XX H, RG40XX V, +4
MyMinUI, SD card RG35XX, Miyoo Mini, Miyoo Mini Flip, Miyoo Mini Plus, Miyoo A30, +16
NextUI, SD card TrimUI Brick, TrimUI Smart Pro
Onion, SD card Miyoo Mini, Miyoo Mini Plus
plumOS, SD card MagicX XU20 V32
RetroOZ, SD card Odroid Go Super, Odroid Go Advance, RGB10 Max, RGB10 Max 2
ROCKNIX, SD card RG351, RG353, RG503, RG552, RG 55G1, RG ARC, RG35XX 2024, RG35XX Plus, +37
spruceOS, SD card Miyoo A30, Miyoo Flip, Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip, +5
UnofficialOS, SD card R35S, R36S, RG351M, RG351P, RG351V, RG353M, RG353P, RG353V, RG503, +5
^[[0;2m ^[[0;1;35mA/Enter^[[0m Choose^[[0;2m B/Esc Back / Filter ? Help^[[0m
=== folder
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which folder?^[[0m
RetroArch usually reads its BIOS files from:
/home/user/.config/retroarch/system
^[[0;7;35m > Use this folder^[[0m
Browse for a folder
Type a path
^[[0;2m ^[[0;1;35mA/Enter^[[0m Use this folder^[[0;2m B/Esc Back ? Help^[[0m
=== folder-card
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where is the SD card?^[[0m
NextUI keeps its BIOS files at the root of its SD card. Choose the card's top folder, the one
holding .system.
No NextUI card is mounted. Insert it, then choose Look again.
^[[0;7;35m > Browse for a folder^[[0m
Type a path
Look again
^[[0;2m ^[[0;1;35mA/Enter^[[0m Browse^[[0;2m B/Esc Back ? Help^[[0m
=== browse
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Browse for a folder^[[0m
/home/user/.config/retroarch
^[[0;7;35m > Use this folder^[[0m
..
/media/user/SDCARD (NextUI card)
cores/
system/
^[[0;2m ^[[0;1;35mA/Enter^[[0m Use this folder^[[0;2m B/Esc Back ? Help^[[0m
=== path
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Type a path^[[0m
The folder for RetroArch:
^[[0;7;35m > ~/bios_^[[0m
Ctrl-U clears. Quotes are removed, ~ and variables are expanded.
^[[0;2m ^[[0;1;35mEnter^[[0m Use this folder^[[0;2m Esc Back Ctrl-U Clear^[[0m
=== create
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Create the folder?^[[0m
/home/user/bios/new does not exist.
The nearest folder that does: /home/user
^[[0;7;35m > Create it and continue^[[0m
Choose another folder
^[[0;2m ^[[0;1;35mA/Enter^[[0m Create^[[0;2m B/Esc Quit^[[0m
=== checking
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Checking^[[0m
RetroArch: ~/.config/retroarch/system
File list received.
Reading the files already in the folder... 0:04
^[[0;2m ^[[0;1;35mCtrl-C^[[0m Quit^[[0;2m B/Esc Quit^[[0m
=== plan
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
^[[0;7;35m > Install^[[0m
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
^[[0;2m ^[[0;1;35mA/Enter^[[0m Install^[[0;2m B/Esc Back Y/o Options ? Help^[[0m
=== plan-in-place
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
Free space: 50.0 GB
^[[0;7;35m > Close^[[0m
Options
^[[0;2m ^[[0;1;35mA/Enter^[[0m Close^[[0;2m B/Esc Back Y/o Options ? Help^[[0m
=== systems
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
All
None
^[[0;2m Filter (/ to type)^[[0m
^[[0;7;35m > [x] nintendo-gba (gba) 1 file, 16.0 KB^[[0m
[x] sony-playstation 3 files, 1.0 MB
^[[0;2m [ ] nintendo-famicom-disk (ファミコン) in place^[[0m
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
^[[0;2m ^[[0;1;35mA/Enter^[[0m Continue^[[0;2m B/Esc Back X/Space Tick ? Help^[[0m
=== options
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
Each row is an option of the command line, named on the right.
^[[0;7;35m > Preferred regions: North America, Japan --region^[[0m
Game data: kept --no-game-data
Installed cores only: no (1 found) --installed-cores
Cores: all --core
Parallel downloads: 8 --jobs
BIOS folder: ^[[0;1m~/.config/retroarch/system --dest^[[0m
Copy from: none --from
Preview the file list --dry-run
Show the command line
^[[0;2m ^[[0;1;35mA/Enter^[[0m Open^[[0;2m B/Esc Back Left/Right Set ? Help^[[0m
=== regions
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preferred regions^[[0m
Tick the regions to keep, best first. Where a system has one BIOS per region, the best one found
is installed.
^[[0;7;35m > [1] North America^[[0m
[ ] Japan
^[[0;2m ^[[0;1;35mA/Enter^[[0m Tick^[[0;2m B/Esc Done Left/Right Order ? Help^[[0m
=== cores
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
Tick the cores you use. With none ticked, every core's files are kept.
^[[0;7;35m > Filter (/ to type)^[[0m
[ ] mesen
[x] pcsx_rearmed
^[[0;2m ^[[0;1;35mA/Enter^[[0m Filter^[[0;2m B/Esc Back X/Space Tick ? Help^[[0m
=== preview
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
4 files to download, 1.0 MB. Nothing is written.
missing 512.0 KB scph5500.bin
wrong copy 512.0 KB scph5501.bin
missing 4 B pcsx/extra.bin
missing 16.0 KB gba_bios.bin
^[[0;2m ^[[0;1;35mA/Enter^[[0m Back
=== command
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Command line^[[0m
The same choice without questions:
python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system --system
nintendo-gba
^[[0;2m ^[[0;1;35mA/Enter^[[0m Back
=== confirm
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m
Counted again for your choices. Nothing is written before Install.
^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
0 in place, ^[[0;33m1 missing^[[0m, 0 wrong copies
Free space: 50.0 GB
^[[0;7;35m > Install^[[0m
Back
^[[0;2m ^[[0;1;35mA/Enter^[[0m Install^[[0;2m B/Esc Back ? Help^[[0m
=== installing
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m
[^[[0;1;35m##############################################^[[0;2m---------------------------------------------^[[0m] 50%
528.0 KB of 1.0 MB
128.9 KB/s, 0:04 left
2 of 4 files
^[[0;2m B, Ctrl-C or Select+Start stops. What is done stays done.^[[0m
^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404)
^[[0;32m ok^[[0m gba_bios.bin
^[[0;2m ^[[0;1;35mB/Esc^[[0m Stop^[[0;2m ? Help^[[0m
=== result
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mFinished with errors^[[0m
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
The files it replaced are kept in /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest /home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
^[[0;2m ^[[0;1;35mA/Enter^[[0m Exit
=== error
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
Could not fetch the file list for retroarch after 3 attempts: [Errno -2] Name or service not
known
Address: https://raw.githubusercontent.com/Abdess/retrobios/main/install/retroarch.json
^[[0;7;35m > Choose another setup or folder^[[0m
Quit
^[[0;2m ^[[0;1;35mA/Enter^[[0m Choose^[[0;2m B/Esc Back^[[0m
=== quit
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Quit?^[[0m
Quit without installing? Nothing was written.
^[[0;7;35m > Quit^[[0m
Stay
^[[0;2m ^[[0;1;35mA/Enter^[[0m Quit^[[0;2m B/Esc Stay^[[0m
=== help
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Keys^[[0m
Up/Down, k/j Move
A/Enter, Start Install
Y, o, Tab Options
B/Esc, Backspace Back
q, Ctrl-C Quit
?, F1 This help
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;2m Any key closes this help^[[0m
+286
View File
@@ -0,0 +1,286 @@
=== setup
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where do you play?^[[0m
Found on this machine:
^[[0;7;35m > RetroArch^[[0m
KNULLI (Batocera files)
Another setup or folder
BIOS folder:
/home/user/.config/retroarch/system
ES-DE found: its BIOS files go to each
^[[0;2m ^[[0;1;35mA^[[0m Continue^[[0;2m B Quit ? Help^[[0m
=== platforms
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which setup?^[[0m
^[[0;2m Filter: setup or device (/)^[[0m
^[[0;7;35m > RetroArch^[[0m
Batocera
Recalbox
RetroBat
EmuDeck
Lakka
RetroDECK
^[[0;2m 1-7 of 34^[[0m
^[[0;2m ^[[0;1;35mA^[[0m Choose^[[0;2m B Back / Filter ? Help^[[0m
=== folder
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which folder?^[[0m
RetroArch usually reads its BIOS files
from:
/home/user/.config/retroarch/system
^[[0;7;35m > Use this folder^[[0m
Browse for a folder
Type a path
^[[0;2m ^[[0;1;35mA^[[0m Use this folder^[[0;2m B Back ? Help^[[0m
=== folder-card
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where is the SD card?^[[0m
NextUI keeps its BIOS files at the
root of its SD card. Choose the
card's top folder, the one holding
.system.
No NextUI card is mounted. Insert it,
then choose Look again.
^[[0;7;35m > Browse for a folder^[[0m
Type a path
Look again
^[[0;2m ^[[0;1;35mA^[[0m Browse^[[0;2m B Back ? Help^[[0m
=== browse
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Browse for a folder^[[0m
/home/user/.config/retroarch
^[[0;7;35m > Use this folder^[[0m
..
/media/user/SDCARD (NextUI card)
cores/
system/
^[[0;2m ^[[0;1;35mA^[[0m Use this folder^[[0;2m B Back ? Help^[[0m
=== path
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Type a path^[[0m
The folder for RetroArch:
^[[0;7;35m > ~/bios_^[[0m
Ctrl-U clears. Quotes are removed, ~
and variables are expanded.
^[[0;2m ^[[0;1;35mEnter^[[0m Use this folder^[[0;2m Esc Back^[[0m
=== create
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Create the folder?^[[0m
/home/user/bios/new does not exist.
The nearest folder that does:
/home/user
^[[0;7;35m > Create it and continue^[[0m
Choose another folder
^[[0;2m ^[[0;1;35mA^[[0m Create^[[0;2m B Quit^[[0m
=== checking
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Checking^[[0m
RetroArch: ~/.config/retroarch/system
File list received.
Reading the files already in the
folder... 0:04
^[[0;2m ^[[0;1;35mCtrl-C^[[0m Quit^[[0;2m B Quit^[[0m
=== plan
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
^[[0;33m1 wrong copy^[[0m: backed up, replaced.
^[[0;7;35m > Install^[[0m
Choose systems
Copy from your old card or a folder
Options
Quit
^[[0;2m ^[[0;1;35mA^[[0m Install^[[0;2m B Back Y Options ? Help^[[0m
=== plan-in-place
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
Free space: 50.0 GB
^[[0;7;35m > Close^[[0m
Options
^[[0;2m ^[[0;1;35mA^[[0m Close^[[0;2m B Back Y Options ? Help^[[0m
=== systems
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
All
None
^[[0;2m Filter (/ to type)^[[0m
^[[0;7;35m > [x] nintendo-gba (gba) 16.0 KB^[[0m
[x] sony-playstation 1.0 MB
^[[0;2m [ ] nintendo-famicom-disk in place^[[0m
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
^[[0;2m ^[[0;1;35mA^[[0m Continue^[[0;2m B Back X Tick ? Help^[[0m
=== options
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
^[[0;7;35m > Preferred regions: North America, Ja^[[0m
Game data: kept
Installed cores only: no (1 found)
Cores: all
Parallel downloads: 8
BIOS folder: ^[[0;1m~/.config...ch/system^[[0m
Copy from: none
Preview the file list
Show the command line
^[[0;2m ^[[0;1;35mA^[[0m Open^[[0;2m B Back <> Set ? Help^[[0m
=== regions
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preferred regions^[[0m
Tick the regions to keep, best first.
Where a system has one BIOS per
region, the best one found is
installed.
^[[0;7;35m > [1] North America^[[0m
[ ] Japan
^[[0;2m ^[[0;1;35mA^[[0m Tick^[[0;2m B Done <> Order ? Help^[[0m
=== cores
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
Tick the cores you use. With none
ticked, every core's files are kept.
^[[0;7;35m > Filter (/ to type)^[[0m
[ ] mesen
[x] pcsx_rearmed
^[[0;2m ^[[0;1;35mA^[[0m Filter^[[0;2m B Back X Tick ? Help^[[0m
=== preview
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
4 files to download, 1.0 MB. Nothing
is written.
missing 512.0 KB scph5500.bin
wrong copy 512.0 KB scph5501.bin
missing 4 B pcsx/extra.bin
missing 16.0 KB gba_bios.bin
^[[0;2m ^[[0;1;35mA^[[0m Back
=== command
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Command line^[[0m
The same choice without questions:
python3 install.py --platform
retroarch --dest
/home/user/.config/retroarch/system
--system nintendo-gba
^[[0;2m ^[[0;1;35mA^[[0m Back
=== confirm
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m
Counted again for your choices.
Nothing is written before Install.
^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
0 in place, ^[[0;33m1 missing^[[0m, 0 wrong copies
Free space: 50.0 GB
^[[0;7;35m > Install^[[0m
Back
^[[0;2m ^[[0;1;35mA^[[0m Install^[[0;2m B Back ? Help^[[0m
=== installing
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m
[^[[0;1;35m###############^[[0;2m----------------^[[0m] 50%
528.0 KB of 1.0 MB
128.9 KB/s, 0:04 left
2 of 4 files
^[[0;2m B, Ctrl-C or Select+Start stops. What^[[0m
^[[0;2m is done stays done.^[[0m
^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404)
^[[0;32m ok^[[0m gba_bios.bin
^[[0;2m ^[[0;1;35mB^[[0m Stop^[[0;2m ? Help^[[0m
=== result
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mFinished with errors^[[0m
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
BIOS folder:
/home/user/.config/retroarch/system
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
^[[0;2m 1-8 of 17^[[0m
^[[0;2m ^[[0;1;35mA^[[0m Exit^[[0;2m ^v Scroll^[[0m
=== error
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
Could not fetch the file list for
retroarch after 3 attempts: [Errno
-2] Name or service not known
Address:
https://raw.githubusercontent.com/
^[[0;2m 1-5 of 7^[[0m
^[[0;7;35m > Choose another setup or folder^[[0m
Quit
^[[0;2m ^[[0;1;35mA^[[0m Choose^[[0;2m B Back PgUp/PgDn Scroll^[[0m
=== quit
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Quit?^[[0m
Quit without installing? Nothing was
written.
^[[0;7;35m > Quit^[[0m
Stay
^[[0;2m ^[[0;1;35mA^[[0m Quit^[[0;2m B Stay^[[0m
=== help
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Keys^[[0m
Up/Down, k/j Move
A/Enter, Start Install
Y, o, Tab Options
B/Esc, Backspace Back
q, Ctrl-C Quit
?, F1 This help
BIOS folder:
/home/user/.config/retroarch/system
^[[0;2m Any key closes this help^[[0m
+352
View File
@@ -0,0 +1,352 @@
=== setup
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where do you play?^[[0m
Found on this machine:
^[[0;7;35m > RetroArch^[[0m
KNULLI (Batocera files)
Another setup or folder
BIOS folder:
/home/user/.config/retroarch/system
ES-DE found: its BIOS files go to each
emulator, not to ES-DE itself.
^[[0;2m ^[[0;1;35mA^[[0m Continue^[[0;2m B Quit ? Help^[[0m
=== platforms
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which setup?^[[0m
^[[0;2m Filter: setup or device (/)^[[0m
^[[0;7;35m > RetroArch^[[0m
Batocera
Recalbox
RetroBat
EmuDeck
Lakka
RetroDECK
RomM
BizHawk
MiSTer FPGA
^[[0;2m 1-10 of 34^[[0m
^[[0;2m ^[[0;1;35mA^[[0m Choose^[[0;2m B Back / Filter ? Help^[[0m
=== folder
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which folder?^[[0m
RetroArch usually reads its BIOS files
from:
/home/user/.config/retroarch/system
^[[0;7;35m > Use this folder^[[0m
Browse for a folder
Type a path
^[[0;2m ^[[0;1;35mA^[[0m Use this folder^[[0;2m B Back ? Help^[[0m
=== folder-card
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where is the SD card?^[[0m
NextUI keeps its BIOS files at the
root of its SD card. Choose the
card's top folder, the one holding
.system.
No NextUI card is mounted. Insert it,
then choose Look again.
^[[0;7;35m > Browse for a folder^[[0m
Type a path
Look again
^[[0;2m ^[[0;1;35mA^[[0m Browse^[[0;2m B Back ? Help^[[0m
=== browse
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Browse for a folder^[[0m
/home/user/.config/retroarch
^[[0;7;35m > Use this folder^[[0m
..
/media/user/SDCARD (NextUI card)
cores/
system/
^[[0;2m ^[[0;1;35mA^[[0m Use this folder^[[0;2m B Back ? Help^[[0m
=== path
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Type a path^[[0m
The folder for RetroArch:
^[[0;7;35m > ~/bios_^[[0m
Ctrl-U clears. Quotes are removed, ~
and variables are expanded.
^[[0;2m ^[[0;1;35mEnter^[[0m Use this folder^[[0;2m Esc Back^[[0m
=== create
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Create the folder?^[[0m
/home/user/bios/new does not exist.
The nearest folder that does:
/home/user
^[[0;7;35m > Create it and continue^[[0m
Choose another folder
^[[0;2m ^[[0;1;35mA^[[0m Create^[[0;2m B Quit^[[0m
=== checking
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Checking^[[0m
RetroArch: ~/.config/retroarch/system
File list received.
Reading the files already in the
folder... 0:04
^[[0;2m ^[[0;1;35mCtrl-C^[[0m Quit^[[0;2m B Quit^[[0m
=== plan
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
^[[0;33m1 wrong copy^[[0m: backed up, replaced.
Free space: 50.0 GB
^[[0;7;35m > Install^[[0m
Choose systems
Copy from your old card or a folder
Options
Quit
^[[0;2m ^[[0;1;35mA^[[0m Install^[[0;2m B Back Y Options ? Help^[[0m
=== plan-in-place
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
Free space: 50.0 GB
^[[0;7;35m > Close^[[0m
Options
^[[0;2m ^[[0;1;35mA^[[0m Close^[[0;2m B Back Y Options ? Help^[[0m
=== systems
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
All
None
^[[0;2m Filter (/ to type)^[[0m
^[[0;7;35m > [x] nintendo-gba (gba) 16.0 KB^[[0m
[x] sony-playstation 1.0 MB
^[[0;2m [ ] nintendo-famicom-disk in place^[[0m
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
^[[0;2m ^[[0;1;35mA^[[0m Continue^[[0;2m B Back X Tick ? Help^[[0m
=== options
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
^[[0;7;35m > Preferred regions: North America, Ja^[[0m
Game data: kept
Installed cores only: no (1 found)
Cores: all
Parallel downloads: 8
BIOS folder: ^[[0;1m~/.config...ch/system^[[0m
Copy from: none
Preview the file list
Show the command line
Sets --region
^[[0;2m ^[[0;1;35mA^[[0m Open^[[0;2m B Back <> Set ? Help^[[0m
=== regions
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preferred regions^[[0m
Tick the regions to keep, best first.
Where a system has one BIOS per
region, the best one found is
installed.
^[[0;7;35m > [1] North America^[[0m
[ ] Japan
^[[0;2m ^[[0;1;35mA^[[0m Tick^[[0;2m B Done <> Order ? Help^[[0m
=== cores
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
Tick the cores you use. With none
ticked, every core's files are kept.
^[[0;7;35m > Filter (/ to type)^[[0m
[ ] mesen
[x] pcsx_rearmed
^[[0;2m ^[[0;1;35mA^[[0m Filter^[[0;2m B Back X Tick ? Help^[[0m
=== preview
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
4 files to download, 1.0 MB. Nothing
is written.
missing 512.0 KB scph5500.bin
wrong copy 512.0 KB scph5501.bin
missing 4 B pcsx/extra.bin
missing 16.0 KB gba_bios.bin
^[[0;2m ^[[0;1;35mA^[[0m Back
=== command
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Command line^[[0m
The same choice without questions:
python3 install.py --platform
retroarch --dest
/home/user/.config/retroarch/system
--system nintendo-gba
^[[0;2m ^[[0;1;35mA^[[0m Back
=== confirm
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m
Counted again for your choices.
Nothing is written before Install.
^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m
BIOS folder: ^[[0;1m~/.co.../retroarch/system^[[0m
0 in place, ^[[0;33m1 missing^[[0m, 0 wrong copies
Free space: 50.0 GB
^[[0;7;35m > Install^[[0m
Back
^[[0;2m ^[[0;1;35mA^[[0m Install^[[0;2m B Back ? Help^[[0m
=== installing
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m
[^[[0;1;35m###############^[[0;2m----------------^[[0m] 50%
528.0 KB of 1.0 MB
128.9 KB/s, 0:04 left
2 of 4 files
^[[0;2m B, Ctrl-C or Select+Start stops. What^[[0m
^[[0;2m is done stays done.^[[0m
^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404)
^[[0;32m ok^[[0m gba_bios.bin
^[[0;2m ^[[0;1;35mB^[[0m Stop^[[0;2m ? Help^[[0m
=== result
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mFinished with errors^[[0m
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
BIOS folder:
/home/user/.config/retroarch/system
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
^[[0;2m 1-11 of 17^[[0m
^[[0;2m ^[[0;1;35mA^[[0m Exit^[[0;2m ^v Scroll^[[0m
=== error
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
Could not fetch the file list for
retroarch after 3 attempts: [Errno
-2] Name or service not known
Address:
https://raw.githubusercontent.com/
Abdess/retrobios/main/install/
retroarch.json
^[[0;7;35m > Choose another setup or folder^[[0m
Quit
^[[0;2m ^[[0;1;35mA^[[0m Choose^[[0;2m B Back^[[0m
=== quit
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Quit?^[[0m
Quit without installing? Nothing was
written.
^[[0;7;35m > Quit^[[0m
Stay
^[[0;2m ^[[0;1;35mA^[[0m Quit^[[0;2m B Stay^[[0m
=== help
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Keys^[[0m
Up/Down, k/j Move
A/Enter, Start Install
Y, o, Tab Options
B/Esc, Backspace Back
q, Ctrl-C Quit
?, F1 This help
BIOS folder:
/home/user/.config/retroarch/system
^[[0;2m Any key closes this help^[[0m
+550
View File
@@ -0,0 +1,550 @@
=== setup
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where do you play?^[[0m
Found on this machine:
^[[0;7;35m > RetroArch^[[0m
KNULLI (Batocera files)
Another setup or folder
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
ES-DE found: its BIOS files go to each emulator, not to ES-DE itself.
^[[0;2m ^[[0;1;35mA/Enter^[[0m Continue^[[0;2m B/Esc Quit ? Help^[[0m
=== platforms
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which setup?^[[0m
^[[0;2m Filter: setup or device (/)^[[0m
^[[0;7;35m > RetroArch^[[0m
Batocera
Recalbox
RetroBat
EmuDeck
Lakka
RetroDECK
RomM
BizHawk
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
dArkOS, SD card Odroid Go Advance, RGB10, RG351MP, G350
EmuELEC, SD card Odroid Go
^[[0;2m 1-19 of 34^[[0m
^[[0;2m ^[[0;1;35mA/Enter^[[0m Choose^[[0;2m B/Esc Back / Filter ? Help^[[0m
=== folder
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Which folder?^[[0m
RetroArch usually reads its BIOS files from:
/home/user/.config/retroarch/system
^[[0;7;35m > Use this folder^[[0m
Browse for a folder
Type a path
^[[0;2m ^[[0;1;35mA/Enter^[[0m Use this folder^[[0;2m B/Esc Back ? Help^[[0m
=== folder-card
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Where is the SD card?^[[0m
NextUI keeps its BIOS files at the root of its SD card. Choose the card's top
folder, the one holding .system.
No NextUI card is mounted. Insert it, then choose Look again.
^[[0;7;35m > Browse for a folder^[[0m
Type a path
Look again
^[[0;2m ^[[0;1;35mA/Enter^[[0m Browse^[[0;2m B/Esc Back ? Help^[[0m
=== browse
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Browse for a folder^[[0m
/home/user/.config/retroarch
^[[0;7;35m > Use this folder^[[0m
..
/media/user/SDCARD (NextUI card)
cores/
system/
^[[0;2m ^[[0;1;35mA/Enter^[[0m Use this folder^[[0;2m B/Esc Back ? Help^[[0m
=== path
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Type a path^[[0m
The folder for RetroArch:
^[[0;7;35m > ~/bios_^[[0m
Ctrl-U clears. Quotes are removed, ~ and variables are expanded.
^[[0;2m ^[[0;1;35mEnter^[[0m Use this folder^[[0;2m Esc Back Ctrl-U Clear^[[0m
=== create
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Create the folder?^[[0m
/home/user/bios/new does not exist.
The nearest folder that does: /home/user
^[[0;7;35m > Create it and continue^[[0m
Choose another folder
^[[0;2m ^[[0;1;35mA/Enter^[[0m Create^[[0;2m B/Esc Quit^[[0m
=== checking
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Checking^[[0m
RetroArch: ~/.config/retroarch/system
File list received.
Reading the files already in the folder... 0:04
^[[0;2m ^[[0;1;35mCtrl-C^[[0m Quit^[[0;2m B/Esc Quit^[[0m
=== plan
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
^[[0;7;35m > Install^[[0m
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
^[[0;2m ^[[0;1;35mA/Enter^[[0m Install^[[0;2m B/Esc Back Y/o Options ? Help^[[0m
=== plan-in-place
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
Free space: 50.0 GB
^[[0;7;35m > Close^[[0m
Options
^[[0;2m ^[[0;1;35mA/Enter^[[0m Close^[[0;2m B/Esc Back Y/o Options ? Help^[[0m
=== systems
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
All
None
^[[0;2m Filter (/ to type)^[[0m
^[[0;7;35m > [x] nintendo-gba (gba) 1 file, 16.0 KB^[[0m
[x] sony-playstation 3 files, 1.0 MB
^[[0;2m [ ] nintendo-famicom-disk (ファミコン) in place^[[0m
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
^[[0;2m ^[[0;1;35mA/Enter^[[0m Continue^[[0;2m B/Esc Back X/Space Tick ? Help^[[0m
=== options
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
Each row is an option of the command line, named on the right.
^[[0;7;35m > Preferred regions: North America, Japan --region^[[0m
Game data: kept --no-game-data
Installed cores only: no (1 found) --installed-cores
Cores: all --core
Parallel downloads: 8 --jobs
BIOS folder: ^[[0;1m~/.config/retroarch/system --dest^[[0m
Copy from: none --from
Preview the file list --dry-run
Show the command line
^[[0;2m ^[[0;1;35mA/Enter^[[0m Open^[[0;2m B/Esc Back Left/Right Set ? Help^[[0m
=== regions
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preferred regions^[[0m
Tick the regions to keep, best first. Where a system has one BIOS per region,
the best one found is installed.
^[[0;7;35m > [1] North America^[[0m
[ ] Japan
^[[0;2m ^[[0;1;35mA/Enter^[[0m Tick^[[0;2m B/Esc Done Left/Right Order ? Help^[[0m
=== cores
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
Tick the cores you use. With none ticked, every core's files are kept.
^[[0;7;35m > Filter (/ to type)^[[0m
[ ] mesen
[x] pcsx_rearmed
^[[0;2m ^[[0;1;35mA/Enter^[[0m Filter^[[0;2m B/Esc Back X/Space Tick ? Help^[[0m
=== preview
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
4 files to download, 1.0 MB. Nothing is written.
missing 512.0 KB scph5500.bin
wrong copy 512.0 KB scph5501.bin
missing 4 B pcsx/extra.bin
missing 16.0 KB gba_bios.bin
^[[0;2m ^[[0;1;35mA/Enter^[[0m Back
=== command
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Command line^[[0m
The same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system --system nintendo-gba
^[[0;2m ^[[0;1;35mA/Enter^[[0m Back
=== confirm
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Confirm^[[0m
Counted again for your choices. Nothing is written before Install.
^[[0;1m ^[[0;1;35m1^[[0;1m file to download, ^[[0;1;35m16.0 KB^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
0 in place, ^[[0;33m1 missing^[[0m, 0 wrong copies
Free space: 50.0 GB
^[[0;7;35m > Install^[[0m
Back
^[[0;2m ^[[0;1;35mA/Enter^[[0m Install^[[0;2m B/Esc Back ? Help^[[0m
=== installing
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Installing^[[0m
[^[[0;1;35m####################################^[[0;2m-----------------------------------^[[0m] 50%
528.0 KB of 1.0 MB
128.9 KB/s, 0:04 left
2 of 4 files
^[[0;2m B, Ctrl-C or Select+Start stops. What is done stays done.^[[0m
^[[0;1;31mFAILED^[[0m scph5500.bin (HTTP 404)
^[[0;32m ok^[[0m gba_bios.bin
^[[0;2m ^[[0;1;35mB/Esc^[[0m Stop^[[0;2m ? Help^[[0m
=== result
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mFinished with errors^[[0m
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
^[[0;2m ^[[0;1;35mA/Enter^[[0m Exit
=== error
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
Could not fetch the file list for retroarch after 3 attempts: [Errno -2]
Name or service not known
Address: https://raw.githubusercontent.com/Abdess/retrobios/main/install/
retroarch.json
^[[0;7;35m > Choose another setup or folder^[[0m
Quit
^[[0;2m ^[[0;1;35mA/Enter^[[0m Choose^[[0;2m B/Esc Back^[[0m
=== quit
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Quit?^[[0m
Quit without installing? Nothing was written.
^[[0;7;35m > Quit^[[0m
Stay
^[[0;2m ^[[0;1;35mA/Enter^[[0m Quit^[[0;2m B/Esc Stay^[[0m
=== help
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Keys^[[0m
Up/Down, k/j Move
A/Enter, Start Install
Y, o, Tab Options
B/Esc, Backspace Back
q, Ctrl-C Quit
?, F1 This help
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;2m Any key closes this help^[[0m
+31
View File
@@ -918,6 +918,37 @@ class ColourFollowsNoColorOrg(unittest.TestCase):
self.assertFalse(term.live)
class TheEndOfARunPaintsItsStatusWords(unittest.TestCase):
"""Line mode ends on the words the full screen paints, in the same
roles, and on the same text without colour."""
def _end(self, colour: bool, *counts: int, **given) -> str:
term = install.Terminal(io.StringIO())
term.colour = colour
out = io.StringIO()
with contextlib.redirect_stdout(out), contextlib.suppress(SystemExit):
install._report_outcome([Path("bios")], *counts, term=term, **given)
return out.getvalue()
def test_the_same_words_with_and_without_colour(self):
for counts, given, painted in (
((3, 1, 1, 0, False), {"failures": [("a.bin", "HTTP 404")]},
(("failure", "Finished with errors"), ("success", "3 files installed"),
("failure", "1 file failed"), ("failure", "failed:"))),
((2, 1, 0, 0, False), {},
(("success", "Done"), ("success", "2 files installed"),
("success", "1 file already up to date"))),
((1, 0, 0, 0, False), {"offline": 2},
(("warning", "Finished with files missing"), ("warning", "2 not fetched")))):
with self.subTest(counts=counts):
plain, coloured = self._end(False, *counts, **given), \
self._end(True, *counts, **given)
self.assertNotIn("\x1b", plain)
self.assertEqual(install.unpainted(coloured), plain)
for role, word in painted:
self.assertIn(install._sgr(role) + word, coloured)
class SmallWordings(unittest.TestCase):
def test_check_asks_which_to_check(self):
"""--check with several platforms asked "Install for:", and
+194 -24
View File
@@ -951,6 +951,29 @@ def golden_states() -> dict:
return states
def result_states() -> dict:
"""The result of a run that installed everything, of one with a file
failed, of one --offline left files out of, and of one stopped."""
def ended(code: int, **summary) -> install.TuiState:
state = checked_state()
state.stack.append("result")
state.code = code
state.download = {"event": "download", "files": 4, "bytes": 1064964}
state.summary = {"event": "summary", "mode": "install", "installed": 4,
"up_to_date": 1, "pending": 0, "failed": [], "skipped": [],
"left_out": [], "kept": [], "interrupted": False, **summary}
return state
return {
"done": ended(0),
"partial": golden_states()["result"],
"offline": ended(1, installed=1, pending=3, failed=[
{"dest": name, "reason": install.OFFLINE_WORDS}
for name in ("scph5500.bin", "scph5501.bin", "pcsx/extra.bin")]),
"stopped": ended(130, installed=2, interrupted=True),
}
class Views(unittest.TestCase):
"""Every screen as text, at the sizes a handheld and a desktop show."""
@@ -1008,6 +1031,78 @@ class Views(unittest.TestCase):
with self.subTest(route=route):
self.assertEqual("".join(sections), path.read_text(encoding="utf-8"))
def test_colour_goldens(self):
"""Every screen as the terminal gets it with colour, the escape
codes kept (ESC written ^[), one file per size."""
update = os.environ.get("RETROBIOS_UPDATE_GOLDENS") == "1"
for cols, rows in SIZES:
sections = []
for name, state in golden_states().items():
state.cols, state.rows = cols, rows
painted = [install.tui_paint(str(line), line.style, True)
for line in install.tui_view(state, cols, rows)]
sections.append(f"=== {name}\n" + "\n".join(painted).replace("\x1b", "^[")
+ "\n")
path = GOLDEN / f"colour-{cols}x{rows}.txt"
if update:
path.write_text("".join(sections), encoding="utf-8")
with self.subTest(size=f"{cols}x{rows}"):
self.assertEqual("".join(sections), path.read_text(encoding="utf-8"))
def test_colour_adds_codes_and_never_text(self):
"""With colour off a line is drawn as before, its style alone, and
with colour on, stripped of its codes, it is the same text: the
golden screen, character for character."""
for cols, rows in SIZES:
colour = (GOLDEN / f"colour-{cols}x{rows}.txt").read_text(encoding="utf-8")
screens = dict(re.findall(r"=== (\S+)\n(.*?)(?=\n=== |\n\Z)", colour, re.S))
for name, state in golden_states().items():
with self.subTest(screen=name, size=f"{cols}x{rows}"):
state.cols, state.rows = cols, rows
lines = install.tui_view(state, cols, rows)
for line in lines:
self.assertEqual(install.tui_paint(str(line), line.style, False),
install._STYLES.get(line.style, "") + line)
stripped = [install.unpainted(painted.replace("^[", "\x1b")).rstrip()
for painted in screens[name].split("\n")]
golden = (GOLDEN / f"{name}-{cols}x{rows}.txt").read_text(encoding="utf-8")
self.assertEqual("\n".join(stripped) + "\n", golden)
def test_each_role_paints_a_status_word_on_the_result_screens(self):
"""Done, a part done, files left offline, a stop: between them the
result screens use every role, each on a word that says it."""
drawn = "\n".join(install.tui_paint(str(line), line.style, True)
for state in result_states().values()
for line in install.tui_view(state, 80, 24))
for role, word in (("accent", "RetroBIOS"), ("strong+success", "4 files installed"),
("strong+success", "Done"), ("failure", "1 failed"),
("failure", "failed:"), ("strong+warning", "3 not fetched"),
("strong+warning", "Stopped"), ("muted", " "),
("strong", "/home/user/.config/retroarch/system")):
with self.subTest(role=role, word=word):
self.assertIn(install._sgr(role) + word, drawn)
# "2 of 4 installed" is no count of files installed.
self.assertNotIn(install._sgr("strong+success") + "4 installed", drawn)
def test_the_status_words_take_their_role_and_a_zero_none(self):
spans = install.status_spans("1 in place, 0 missing, 3 missing, 1 wrong copy, 2 failed")
self.assertEqual([("1 in place", "success"), ("3 missing", "warning"),
("1 wrong copy", "warning"), ("2 failed", "failure")],
[("1 in place, 0 missing, 3 missing, 1 wrong copy, 2 failed"[a:b], r)
for a, b, r in spans])
self.assertEqual(install.status_spans("0 wrong copies, 0 missing"), [])
self.assertEqual(install.status_spans("python3 install.py --platform retroarch"), [])
def test_the_bar_paints_a_key_only(self):
"""The first entry's key in the accent, never a word: "Writing to the
card..." and "Any key closes this help" name no key."""
for text in (" Writing to the card...", " Any key closes this help"):
self.assertEqual(install.tui_paint(text, "hint", True),
install._sgr("muted") + text + "\x1b[0m")
painted = install.tui_paint(" A/Enter Exit B/Esc Back", "hint", True)
self.assertTrue(painted.startswith(install._sgr("muted") + " "
+ install._sgr("accent") + "A/Enter"), painted)
def test_the_hint_bar_names_the_primary_action_first(self):
for name, state in golden_states().items():
with self.subTest(screen=name):
@@ -3668,7 +3763,8 @@ class LineModeOffline(unittest.TestCase):
term.wait("q) quit")
term.send("\r")
code = term.close()
out = term.raw.decode("utf-8", "replace")
# Read as words: a terminal gets the status words painted.
out = install.unpainted(term.raw.decode("utf-8", "replace"))
self.assertIn("--offline: only the files found on this machine are installed, and no "
"--from names a folder to copy them from.", out)
# tui_audit_7 P3-4: nothing to copy from, nothing to install.
@@ -4988,52 +5084,77 @@ class EndToEnd(unittest.TestCase):
def test_no_color_draws_no_colour(self):
"""The screen marks the row in focus with > as well as reverse video,
and draws no colour at all: NO_COLOR has nothing left to remove."""
so NO_COLOR, or a terminal of fewer than 8 colours, takes the colour
away and leaves the same screen."""
self.repo()
self.env["NO_COLOR"] = "1"
term = self.start()
screen = term.wait("Where do you play?")
self.assertIn("> RetroArch", screen)
term.send("q")
self.assertEqual(term.close(), 130)
for params in re.findall(rb"\x1b\[([0-9;]*)m", term.raw):
codes = {int(code) for code in params.split(b";") if code}
self.assertFalse(codes & {*range(30, 50), *range(90, 108)}, params)
screens = {}
for name, env in (("colour", {}), ("no_color", {"NO_COLOR": "1"}),
("mono", {"TERM": "rbtest-mono",
"TERMINFO": str(_terminfo_folder(self))})):
with self.subTest(env=name):
self.env.pop("NO_COLOR", None)
self.env.update({"TERM": "xterm", **env})
term = self.start()
screen = term.wait("Where do you play?")
self.assertIn("> RetroArch", screen)
screens[name] = screen
# pyte reads the colours drawn: RetroBIOS in the accent.
title = term.screen.buffer[0]
at = screen.splitlines()[0].index("RetroBIOS")
self.assertEqual(title[at].fg, "magenta" if name == "colour" else "default")
term.send("q")
self.assertEqual(term.close(), 130)
colours = set()
for params in re.findall(rb"\x1b\[([0-9;]*)m", term.raw):
colours |= {int(code) for code in params.split(b";") if code} & {
*range(30, 50), *range(90, 108)}
self.assertEqual(bool(colours), name == "colour", colours)
self.assertEqual(screens["colour"], screens["no_color"])
self.assertEqual(screens["colour"], screens["mono"])
def _terminfo_entry(name: str, cup: str | None, extended: bool = False) -> bytes:
def _terminfo_entry(name: str, cup: str | None, extended: bool = False,
colours: int | None = 8) -> bytes:
"""A compiled term(5) entry holding the strings up to cursor_address,
only that one set."""
only that one set, and the numbers up to max_colors when colours is
given, columns alone when it is not."""
def short(value: int) -> bytes:
return value.to_bytes(2, "little", signed=True)
width = 4 if extended else 2
names = name.encode() + b"|test terminal\0"
table = (cup.encode("latin-1") + b"\0") if cup is not None else b""
offsets = [-1] * (install._TERMINFO_CUP + 1)
if cup is not None:
offsets[install._TERMINFO_CUP] = 0
numbers = [80] + ([-1] * (install._TERMINFO_COLORS - 1) + [colours]
if colours is not None else [])
body = names + (b"\0" if len(names) % 2 else b"")
# One number, columns, in the width of the format.
body += (80).to_bytes(4 if extended else 2, "little")
body += b"".join(number.to_bytes(width, "little", signed=True) for number in numbers)
body += b"".join(short(offset) for offset in offsets) + table
header = b"".join(short(value) for value in (
0o1036 if extended else 0o432, len(names), 0, 1, len(offsets), len(table)))
0o1036 if extended else 0o432, len(names), 0, len(numbers), len(offsets),
len(table)))
return header + body
def _terminfo_folder(test: unittest.TestCase) -> Path:
"""A $TERMINFO with a glass terminal, a VT52 and two ANSI terminals,
one in the extended format."""
"""A $TERMINFO with a glass terminal, a VT52, two ANSI terminals of 8
colours, one in the extended format, and two ANSI terminals without
colour: one naming 2, one naming none (vt100)."""
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
folder = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
test.addCleanup(shutil.rmtree, folder, True)
for name, cup, extended in (
("rbtest-glass", None, False),
("rbtest-vt52", "\x1bY%p1%' '%+%c%p2%' '%+%c", False),
("rbtest-ansi", "\x1b[%i%p1%d;%p2%dH", False),
("rbtest-ansi2", "\x1b[%i%p1%d;%p2%dH", True)):
ansi = "\x1b[%i%p1%d;%p2%dH"
for name, cup, extended, colours in (
("rbtest-glass", None, False, None),
("rbtest-vt52", "\x1bY%p1%' '%+%c%p2%' '%+%c", False, None),
("rbtest-ansi", ansi, False, 8),
("rbtest-ansi2", ansi, True, 256),
("rbtest-two", ansi, True, 2),
("rbtest-mono", ansi, False, None)):
(folder / "r").mkdir(exist_ok=True)
(folder / "r" / name).write_bytes(_terminfo_entry(name, cup, extended))
(folder / "r" / name).write_bytes(_terminfo_entry(name, cup, extended, colours))
return folder
@@ -5102,6 +5223,55 @@ class TerminalsTheScreenCannotDraw(unittest.TestCase):
self.assertEqual((shown.live, shown.colour), (live, live))
self.assertEqual(shown.ok("ok") == "ok", not live)
def test_the_colours_are_read(self):
self.assertEqual(install.terminal_colours("rbtest-ansi", self.env), 8)
self.assertEqual(install.terminal_colours("rbtest-ansi2", self.env), 256)
self.assertEqual(install.terminal_colours("rbtest-two", self.env), 2)
self.assertEqual(install.terminal_colours("rbtest-mono", self.env), 0)
self.assertIsNone(install.terminal_colours("rbtest-none", self.env))
for data in (b"", b"\x00" * 12, _terminfo_entry("x", None, colours=8)[:40]):
with self.subTest(data=data):
self.assertIsNone(install.terminfo_number(data, install._TERMINFO_COLORS))
def test_fewer_than_8_colours_draw_the_line_live_and_plain(self):
"""A vt100 moves the cursor the ANSI way and has no colour: the
progress line still redraws, its words unpainted."""
for term, colour in (("rbtest-ansi", True), ("rbtest-ansi2", True),
("rbtest-two", False), ("rbtest-mono", False),
("rbtest-none", True)):
with self.subTest(term=term), mock.patch.dict(
os.environ, {**self.env, "TERM": term}), \
mock.patch.object(install, "_enable_ansi", return_value=True):
os.environ.pop("NO_COLOR", None)
shown = install.Terminal(self.tty)
self.assertEqual((shown.live, shown.colour), (True, colour))
self.assertEqual(shown.status("1 failed") != "1 failed", colour)
def test_the_screen_draws_without_colour_for_each_reason(self):
"""NO_COLOR, TERM dumb or unset, fewer than 8 colours, an output no
terminal: each turns the colour off, and says which."""
for env, said in (({"TERM": "rbtest-ansi"}, ""),
({"TERM": "rbtest-none"}, ""),
({"TERM": "rbtest-ansi", "NO_COLOR": "1"}, "NO_COLOR is set"),
({"TERM": "rbtest-ansi", "NO_COLOR": ""}, ""),
({"TERM": "dumb"}, "TERM is dumb or not set"),
({}, "TERM is dumb or not set"),
({"TERM": "rbtest-two"},
"TERM=rbtest-two names 2 colours, fewer than 8"),
({"TERM": "rbtest-mono"},
"TERM=rbtest-mono names 0 colours, fewer than 8")):
with self.subTest(env=env):
self.assertEqual(install.colour_blocker({**self.env, **env}, self.tty), said)
self.assertEqual(install.colour_blocker({**self.env, "TERM": "rbtest-ansi"},
io.StringIO()),
"the output is not a terminal")
with mock.patch.dict(os.environ, {**self.env, "TERM": "rbtest-mono"}), \
mock.patch.object(install, "_enable_ansi", return_value=True):
self.assertFalse(install._TuiTerminal(self.tty, self.tty).colour)
os.environ["TERM"] = "rbtest-ansi"
os.environ.pop("NO_COLOR", None)
self.assertTrue(install._TuiTerminal(self.tty, self.tty).colour)
def test_an_unset_term_is_a_dumb_one(self):
with mock.patch.dict(os.environ, {"FORCE_COLOR": ""}), \
mock.patch.object(install, "_enable_ansi", return_value=True):
+10 -1
View File
@@ -220,6 +220,15 @@ given on the command line fill the screens in advance and are kept as they are.
menu shows it the same way, Enter keeping what `--system` named and `c`
choosing anew.
Colour is added to words, never put in their place. RetroBIOS in the title, the row in focus
and the key the bottom line names first are magenta, files installed or in place
green, files missing or left out yellow, failures and a refused folder red, and
secondary text dim. Only the 16 basic colours are used, the ones a handheld's
console has. `NO_COLOR` set to any value but an empty one turns colour off, as do
`TERM=dumb` or no `TERM`, a terminal whose description names fewer than 8 colours
and an output that is not a terminal. The text stays the same. The line-by-line
questions paint the end of a run the same way.
However the screen ends (Enter on the result, `q`, Ctrl+C, Ctrl+D, SIGTERM, a
hangup), the terminal is given back as the screen found it: the cursor shown,
the main screen back, echo and line editing on. Ctrl+Z, or SIGTSTP from
@@ -1584,7 +1593,7 @@ RetroArch folder that holds only `scph5501.bin`, in the order written:
| `RETROBIOS_INSTALL_URL` | Where the bootstrap fetches `install.py`. HTTPS only |
| `RETROBIOS_INSTALL_SHA256` | The SHA-256 the bootstrap requires of that installer, 64 hex characters. When either of these two is set and the fingerprint does not match, the error names the variable instead of suggesting a retry |
| `RETROBIOS_BOOTSTRAP` | Set by `install.sh` (`sh`) and `install.ps1` (`ps1`) for the temporary installer they run, so that its hints give back the one-line command rather than `python3 install.py` |
| `NO_COLOR` | Plain text, no colour, even at a terminal, when set to any value but an empty one. `TERM=dumb`, or no `TERM` outside Windows, also turns the live progress line and the colour off, as does a terminal whose description moves no cursor the ANSI way (`TERM=vt52`). The full screen draws no colour at all: the row in focus is marked with `>` as well as reverse video, and every state is written in words |
| `NO_COLOR` | Plain text, no colour, even at a terminal, when set to any value but an empty one. `TERM=dumb`, or no `TERM` outside Windows, also turns the live progress line and the colour off, as does a terminal whose description moves no cursor the ANSI way (`TERM=vt52`). A description naming fewer than 8 colours keeps the live line and drops the colour. The full screen then draws the same text without colour: the row in focus is marked with `>` as well as reverse video, and every state is written in words |
| `TERM`, `TERMINFO`, `TERMINFO_DIRS` | Decide whether the full screen opens. `TERM` unset or `dumb` (outside Windows) gives the questions line by line. Otherwise the terminal's description is read where ncurses reads it: `TERMINFO`, then `~/.terminfo`, then each folder of `TERMINFO_DIRS`, then the system's own. A description whose cursor moves are not the ANSI ones the screen writes (`TERM=vt52`) gives the questions too, and `--tui` there stops with the reason |
| `FORCE_COLOR`, `CLICOLOR_FORCE` | Colour even when the output is a pipe or a file, set to any value but `0` or an empty one. `NO_COLOR` wins over both, and the progress line stays one line per file |
| `SSL_CERT_FILE`, `SSL_CERT_DIR` | The trusted certificates to check servers with, for a Python that finds none of its own |