fix: close an idle error screen with its code

An idle close exited 0 on every screen, an error one too. It now exits with the error's or the result's code and prints that screen's words after the idle line.
This commit is contained in:
Abdessamad Derraz committed 2026-10-23 23:29:03 +02:00
1 parent 71162d124d
commit 2a6a122d56
3 files changed
+79 -4

No files matched your search

+21 -3
View File
@@ -12941,9 +12941,27 @@ def tui_farewell(state: TuiState) -> list[str]:
return [screen_text(line) for line in _farewell_lines(state)] return [screen_text(line) for line in _farewell_lines(state)]
def tui_idle_code(state: TuiState) -> int:
"""The exit code of an idle close: an error's or a result's on those
screens, so a launcher tells them from a close with nothing to say."""
if state.screen == "error":
return state.error_code
if state.screen == "result" and state.code is not None:
return state.code
return 0
def _farewell_lines(state: TuiState) -> list[str]: def _farewell_lines(state: TuiState) -> list[str]:
if state.idle_closed: if not state.idle_closed:
return [f"RetroBIOS: {TUI_IDLE_WORDS}"] return _closing_lines(state)
idle = f"RetroBIOS: {TUI_IDLE_WORDS}"
# What an error or a result screen said stays said.
if state.screen in ("error", "result"):
return [idle, *_closing_lines(state)]
return [idle]
def _closing_lines(state: TuiState) -> list[str]:
if state.screen != "result" and state.download is not None: if state.screen != "result" and state.download is not None:
# An install ran: what it wrote is said, whatever ended it. # An install ran: what it wrote is said, whatever ended it.
title, lines = tui_outcome(state) title, lines = tui_outcome(state)
@@ -13114,7 +13132,7 @@ def run_tui(args: argparse.Namespace) -> int:
quiet_since = time.monotonic() quiet_since = time.monotonic()
elif (idle is not None and not pressed and state.exit_code is None elif (idle is not None and not pressed and state.exit_code is None
and time.monotonic() - quiet_since >= idle): and time.monotonic() - quiet_since >= idle):
state.idle_closed, state.exit_code = True, 0 state.idle_closed, state.exit_code = True, tui_idle_code(state)
if flags["interrupt"]: if flags["interrupt"]:
flags["interrupt"] = False flags["interrupt"] = False
if time.monotonic() >= ignore_interrupt_until[0]: if time.monotonic() >= ignore_interrupt_until[0]:
+54
View File
@@ -2595,6 +2595,60 @@ class IdleExit(unittest.TestCase):
self.assertEqual(install.tui_farewell(state), self.assertEqual(install.tui_farewell(state),
["RetroBIOS: No button pressed, closing."]) ["RetroBIOS: No button pressed, closing."])
def test_an_error_screen_closes_with_its_code(self):
"""Closed idle on "No internet: offline mode", the launcher read 0
and showed nothing."""
state = fresh_state()
state.stack = ["setup", "checking"]
install._fail(state, ["Give the file list with --manifest FILE."], 1)
state.error_title = "No internet: offline mode"
self.assertEqual(self.idle_close(state), 1)
self.assertEqual(install.tui_farewell(state), [
"RetroBIOS: No button pressed, closing.",
"RetroBIOS: No internet: offline mode",
"Give the file list with --manifest FILE."])
def test_a_result_closes_with_the_install_code(self):
state = fresh_state()
state.stack = ["setup", "result"]
state.code = 2
state.summary = {"event": "summary", "mode": "install", "installed": 1,
"up_to_date": 0, "pending": 0,
"failed": [{"dest": "a.bin", "reason": "disk full"}],
"skipped": [], "left_out": [], "kept": [], "interrupted": False}
state.dest = SYSTEM_DIR
self.assertEqual(self.idle_close(state), 2)
farewell = install.tui_farewell(state)
self.assertEqual(farewell[:2], ["RetroBIOS: No button pressed, closing.",
"RetroBIOS: Finished with errors"])
def test_any_other_screen_closes_with_0(self):
state = fresh_state()
state.stack = ["setup"]
self.assertEqual(self.idle_close(state), 0)
def idle_close(self, state: install.TuiState) -> int:
"""run_tui on state, no key ever pressed, closed by the idle timer."""
class Quiet(Crash.FakeTerminal):
def keys(self, timeout):
time.sleep(0.01)
return []
out = io.StringIO()
work = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, work, True)
with mock.patch.object(install, "_TuiTerminal", Quiet), \
mock.patch.object(install.tempfile, "mkdtemp", return_value=str(work)), \
mock.patch.object(install, "tui_start", return_value=(state, [])), \
mock.patch.object(install, "_open_local_sources"), \
mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.dict(os.environ, {install.TUI_IDLE_ENV: "0.05"}), \
mock.patch.object(sys, "stdout", out), mock.patch.object(sys, "stderr", out):
code = install.run_tui(_args())
self.assertTrue(state.idle_closed)
self.assertIn("No button pressed, closing.", out.getvalue())
return code
class AReadErrorIsNotAClosedWindow(unittest.TestCase): class AReadErrorIsNotAClosedWindow(unittest.TestCase):
"""Any EIO read as the window closing: a failing SD card answers EIO """Any EIO read as the window closing: a failing SD card answers EIO
+4 -1
View File
@@ -1087,6 +1087,9 @@ A launcher with no mapper at all (a keyboard) leaves it unset.
A launcher can also set `RETROBIOS_IDLE_EXIT` to a number of seconds: when A launcher can also set `RETROBIOS_IDLE_EXIT` to a number of seconds: when
no button has reached the interface since it opened, and nothing has run for no button has reached the interface since it opened, and nothing has run for
that long, it closes with `No button pressed, closing.` and exit status 0. that long, it closes with `No button pressed, closing.` and exit status 0.
Closed on an error or a result screen, it prints that screen's words after
the line and exits with its code (1 for `No internet: offline mode`), so
the launcher can tell an error from a close with nothing to report.
The time counts from the end of the last check, so a launcher that names the The time counts from the end of the last check, so a launcher that names the
platform, whose interface moves on to the plan by itself, closes the same way. platform, whose interface moves on to the plan by itself, closes the same way.
A mapper that never opened the pad would otherwise hold the device on that A mapper that never opened the pad would otherwise hold the device on that
@@ -1341,7 +1344,7 @@ RetroArch folder that holds only `scph5501.bin`, in the order written:
| `SSL_CERT_FILE`, `SSL_CERT_DIR` | The trusted certificates to check servers with, for a Python that finds none of its own | | `SSL_CERT_FILE`, `SSL_CERT_DIR` | The trusted certificates to check servers with, for a Python that finds none of its own |
| `RETROBIOS_PLAIN`, `ACCESSIBLE` | Set to anything but `0`: the questions come line by line, as with `--plain`, instead of the full-screen interface | | `RETROBIOS_PLAIN`, `ACCESSIBLE` | Set to anything but `0`: the questions come line by line, as with `--plain`, instead of the full-screen interface |
| `RETROBIOS_PAD` | The buttons a handheld launcher's mapper sends to the full screen (`gptokeyb`, `gptokeyb_muxterm`, `muxterm`, `evmapy`, `miyoo`), so its hints name them. See [On a handheld](#on-a-handheld-the-retrobios-port) | | `RETROBIOS_PAD` | The buttons a handheld launcher's mapper sends to the full screen (`gptokeyb`, `gptokeyb_muxterm`, `muxterm`, `evmapy`, `miyoo`), so its hints name them. See [On a handheld](#on-a-handheld-the-retrobios-port) |
| `RETROBIOS_IDLE_EXIT` | Seconds the full screen waits for a first button, counted while nothing runs. With none pressed, it closes with exit status 0 | | `RETROBIOS_IDLE_EXIT` | Seconds the full screen waits for a first button, counted while nothing runs. With none pressed, it closes with exit status 0, or with the code of the error or result screen it closed on |
| `HTTPS_PROXY`, `NO_PROXY` | Honoured for every download through the standard library's default proxy handling: one names the proxy, the other the hosts to reach directly | | `HTTPS_PROXY`, `NO_PROXY` | Honoured for every download through the standard library's default proxy handling: one names the proxy, the other the hosts to reach directly |
## When something goes wrong ## When something goes wrong