feat(gui): one-click Close in its own bottom sidebar section
Close is now a direct shutdown trigger: visiting the Close page (the sidebar entry) fires shutdown_app() immediately — no confirm step, no intermediate body. The farewell overlay paints and os._exit(0) lands ~1s later from a daemon thread. Layout: Close moved into its own bottom-of-sidebar section so the destructive action is visually separated from Account/Activate. - New shutdown_app() in components/_legacy.py replaces quit_button. os._exit thread is skipped when "pytest" is in sys.modules so the test suite doesn't suicide on rendering 99_Close. - pages/99_Close.py shrinks to set_page_config + chrome + shutdown_app. - app.py nav grows a new "Close" section header (new nav.section_close key in en/es packs) pinned at the bottom of the navigation dict. Tests updated: - TestQuitButtonRenders → TestClosePageShutsDownImmediately. Assert the shutdown caption renders + no confirm button exists. - test_smoke EXPECTED_SUBSTRINGS["99_Close"] now pins "Shutting down" / "Cerrando" (the visible page body) instead of the removed page title. 2008 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
@@ -186,37 +187,38 @@ def _farewell_script() -> str:
|
||||
)
|
||||
|
||||
|
||||
def quit_button(label: str | None = None, *, key: str = "quit_app_button") -> None:
|
||||
"""Render a Quit button that terminates the Streamlit server.
|
||||
def shutdown_app() -> None:
|
||||
"""Terminate the Streamlit server immediately, no confirm.
|
||||
|
||||
Designed to be called from a page whose mere act of rendering means
|
||||
the user wants to quit (e.g., the sidebar Close entry). Schedules
|
||||
``os._exit(0)`` on a daemon thread so the process terminates after
|
||||
the farewell overlay has had a chance to paint, then injects the
|
||||
overlay JS and short-circuits the rest of the page via ``st.stop``.
|
||||
|
||||
Streamlit has no first-class shutdown hook, and signalling the
|
||||
process (SIGTERM/SIGINT) does not reliably terminate it — Streamlit
|
||||
installs its own handlers and the tornado/asyncio loop swallows or
|
||||
defers the signal, so the browser sees the websocket drop while the
|
||||
python process stays alive. We schedule ``os._exit(0)`` on a daemon
|
||||
thread to hard-kill the process, and inject a small JS shim that
|
||||
either closes the tab (when allowed) or replaces the top frame with
|
||||
a self-contained "App closed" page so the user never sees
|
||||
Streamlit's red connection-error overlay.
|
||||
python process stays alive. ``os._exit`` is the only reliable kill.
|
||||
|
||||
The hard-exit thread is skipped under pytest so the test suite does
|
||||
not suicide when a test renders this page. The overlay + caption
|
||||
still render so test assertions about content work.
|
||||
"""
|
||||
if label is None:
|
||||
label = _t("quit.button")
|
||||
|
||||
if st.session_state.get("_app_shutting_down"):
|
||||
from streamlit.components.v1 import html as _components_html
|
||||
_components_html(_farewell_script(), height=0)
|
||||
st.success(_t("quit.shutting_down"))
|
||||
st.stop()
|
||||
|
||||
if st.button(label, key=key, type="secondary"):
|
||||
if not st.session_state.get("_app_shutting_down"):
|
||||
st.session_state["_app_shutting_down"] = True
|
||||
if "pytest" not in sys.modules:
|
||||
def _hard_exit() -> None:
|
||||
time.sleep(1.0)
|
||||
os._exit(0)
|
||||
|
||||
def _hard_exit() -> None:
|
||||
time.sleep(1.0)
|
||||
os._exit(0)
|
||||
threading.Thread(target=_hard_exit, daemon=True).start()
|
||||
|
||||
threading.Thread(target=_hard_exit, daemon=True).start()
|
||||
st.rerun()
|
||||
from streamlit.components.v1 import html as _components_html
|
||||
_components_html(_farewell_script(), height=0)
|
||||
st.success(_t("quit.shutting_down"))
|
||||
st.stop()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user