There is no JavaScript override for browser tab-close security:
``window.close()`` only succeeds on windows JS opened (Chrome --app
windows qualify; a regular browser tab does not). What we can do is
make the --app path easier to hit and the failure case more
actionable.
Three changes:
1. ``src/gui/__main__.py`` — extend browser detection. PATH lookup
now also looks for ``msedge`` / ``microsoft-edge``; Windows install
candidates include the Edge install path; macOS candidates include
Edge and Chromium. Edge is Chromium-based, supports ``--app``, and
ships on every Windows 10+ machine — so users without Chrome no
longer fall through to the regular browser tab. When the fallback
IS hit, print a warning to stderr explaining why Close-from-page
will require Ctrl+W. Renamed ``_find_chrome`` to
``_find_app_browser`` to reflect the broader scope.
2. ``_FAREWELL_SCRIPT_TEMPLATE`` in ``components/_legacy.py`` —
factor close attempts into a ``tryClose`` helper that runs three
escalating tries: standard ``win.close()``, the
``win.open('', '_self')`` history-rewrite trick (no-op in modern
Chrome but free), and ``win.top.close()``. Auto-close on paint AND
the manual button now both call this helper. Skip the manual hint
if the close eventually succeeded between the click and the 250 ms
timeout.
3. ``quit.close_hint`` in en/es i18n packs — rewrite the message to
tell the user honestly that this is a browser security restriction,
tell them the Ctrl+W keystroke that works, and point them at
``python -m src.gui`` for the auto-closing app-mode experience.
2008 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 lines
3.9 KiB
Python
109 lines
3.9 KiB
Python
"""Allow running as ``python -m src.gui``.
|
|
|
|
Launches Streamlit with headless=true and opens Chrome in --app mode
|
|
so the window has no address bar, tabs, or bookmarks — looks like a
|
|
native desktop app.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
_URL = "http://localhost:8501"
|
|
|
|
|
|
def _find_app_browser() -> str | None:
|
|
"""Return the path to a Chromium-based browser that supports --app mode.
|
|
|
|
Chrome is preferred. Edge (Chromium-based, ships on every Windows
|
|
10+ install) is the fallback that lets ``window.close()`` actually
|
|
close the tab on the Close page — without it Windows users who
|
|
don't have Chrome end up in a regular browser tab where the close
|
|
button is silently blocked by browser security.
|
|
"""
|
|
# PATH first — covers Linux package installs and devs who put their
|
|
# preferred browser on PATH explicitly.
|
|
for name in (
|
|
"chrome", "google-chrome", "google-chrome-stable",
|
|
"chromium", "chromium-browser",
|
|
"msedge", "microsoft-edge",
|
|
):
|
|
found = shutil.which(name)
|
|
if found:
|
|
return found
|
|
|
|
# Windows common install locations.
|
|
if sys.platform == "win32":
|
|
candidates = [
|
|
# Chrome — preferred when present.
|
|
Path(os.environ.get("PROGRAMFILES", r"C:\Program Files"))
|
|
/ "Google" / "Chrome" / "Application" / "chrome.exe",
|
|
Path(os.environ.get("PROGRAMFILES(X86)", r"C:\Program Files (x86)"))
|
|
/ "Google" / "Chrome" / "Application" / "chrome.exe",
|
|
Path(os.environ.get("LOCALAPPDATA", ""))
|
|
/ "Google" / "Chrome" / "Application" / "chrome.exe",
|
|
# Edge — ships with Windows 10+, Chromium-based, also
|
|
# supports --app and the window.close() it enables.
|
|
Path(os.environ.get("PROGRAMFILES(X86)", r"C:\Program Files (x86)"))
|
|
/ "Microsoft" / "Edge" / "Application" / "msedge.exe",
|
|
Path(os.environ.get("PROGRAMFILES", r"C:\Program Files"))
|
|
/ "Microsoft" / "Edge" / "Application" / "msedge.exe",
|
|
]
|
|
for c in candidates:
|
|
if c.exists():
|
|
return str(c)
|
|
|
|
# macOS
|
|
if sys.platform == "darwin":
|
|
candidates = [
|
|
Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
|
|
Path("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
|
|
Path("/Applications/Chromium.app/Contents/MacOS/Chromium"),
|
|
]
|
|
for c in candidates:
|
|
if c.exists():
|
|
return str(c)
|
|
|
|
return None
|
|
|
|
|
|
def _open_app_window() -> None:
|
|
"""Wait for the server, then open the URL in an --app-mode window.
|
|
|
|
--app mode makes the window look like a desktop app (no address bar,
|
|
no tabs) AND — crucially — makes ``window.close()`` work, because
|
|
the browser treats --app windows as programmatically closable. The
|
|
regular-browser fallback prints a warning so the user knows why the
|
|
Close button on the closing screen will require a manual Ctrl+W.
|
|
"""
|
|
time.sleep(2)
|
|
browser = _find_app_browser()
|
|
if browser:
|
|
subprocess.Popen([browser, f"--app={_URL}"])
|
|
return
|
|
print(
|
|
"DataTools: Chrome and Edge were not found in their standard "
|
|
"locations. Falling back to the default browser. The Close "
|
|
"button on the shutdown screen will likely require a manual "
|
|
"Ctrl+W to close the tab (a browser security restriction "
|
|
"prevents JavaScript from closing user-opened tabs).",
|
|
file=sys.stderr,
|
|
)
|
|
import webbrowser
|
|
webbrowser.open(_URL)
|
|
|
|
|
|
app_path = Path(__file__).parent / "app.py"
|
|
|
|
# Open the browser in a background thread so it doesn't block Streamlit
|
|
threading.Thread(target=_open_app_window, daemon=True).start()
|
|
|
|
subprocess.run([
|
|
sys.executable, "-m", "streamlit", "run", str(app_path),
|
|
"--server.headless", "true",
|
|
])
|