#!/usr/bin/env python3
"""Open the NexusOS UI in a native window.
Uses pywebview, which renders via the WebView2 runtime on Windows (already
present on Win10/11) -- a real app window with no browser chrome and none of the
Edge --app profile cold-start. Blocks until the window is closed; the launcher
waits on this process and stops the services when it exits.
The window opens IMMEDIATELY on a loading page and navigates to the app once the
backend answers, rather than waiting up to 40s with nothing on screen and then
appearing. One window the whole time: nothing pops up and vanishes, and there is
no moment where the user is left wondering whether the command did anything.
The loading page's status line is updated live (backend -> ready) via
evaluate_js, instead of sitting on one static sentence for the whole wait - the
point is a user watching the window can tell what stage it's stuck on, without
needing to go read a terminal or a log file to find out.
Falls back to the default browser if pywebview/WebView2 is unavailable, staying
alive so the launcher doesn't tear the services down underneath it.
"""
import os
import sys
import time
import urllib.request
URL = "http://localhost:8000"
ICON_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "NexusOS.ico")
# Inline so it renders with no server and no asset files - the whole point is
# that it shows before anything is listening. Colours match the app's dark UI so
# the swap to the real page is not a flash of a different-looking window. The
#
is the hook _set_status() rewrites as boot moves through its stages.
LOADING_HTML = """
NexusOS
Starting NexusOS
Starting backend...
"""
FAILED_HTML = LOADING_HTML.replace(
"", ""
).replace(
"Starting NexusOS", "Backend did not start"
).replace(
'id="status">Starting backend...',
'id="status">Nothing answered on :8000 after 40s. Check: ncp logs -b',
)
def _set_status(win, text: str) -> None:
# Best-effort: the window can be mid-teardown (user closed it while this
# background thread was still polling), and evaluate_js on a dead window
# raises - that's not a reason to kill the polling loop.
try:
win.evaluate_js(f"document.getElementById('status').textContent = {text!r}")
except Exception:
pass
def _http_ok(url: str, timeout: float = 2.0) -> bool:
try:
with urllib.request.urlopen(url, timeout=timeout) as r:
return r.status == 200
except Exception:
return False
def _wait_for_backend(win, timeout: float = 40.0) -> bool:
"""Poll the backend, updating the on-screen status as it comes up - so the
window never loads before the server is up, which shows a localhost error
the webview won't retry."""
status_url = URL.rstrip("/") + "/status"
deadline = time.time() + timeout
_set_status(win, "Starting backend...")
while time.time() < deadline:
try:
# 5s, not 2: /status probes Ollama, and a wedged Ollama made it
# slower than a 2s ceiling - the backend was up and answering 200
# while this loop timed out on every attempt and declared it dead.
# The server side is fixed too; this is the margin.
with urllib.request.urlopen(status_url, timeout=5) as r:
if r.status == 200:
_set_status(win, "Opening Nexus...")
return True
except Exception:
pass
time.sleep(0.5)
return False
def main() -> int:
try:
import webview
except Exception as e: # pywebview not installed
return _browser_fallback(f"pywebview unavailable ({e})")
try:
window = webview.create_window(
"NexusOS",
html=LOADING_HTML,
width=1200,
height=800,
min_size=(900, 600),
)
def _swap_in_app(win):
"""Runs once the GUI loop is up, so the spinner is already on screen
while we wait. load_url replaces the loading page in place - there is
never a second window to close."""
if _wait_for_backend(win):
win.load_url(URL)
else:
print("[nexus] backend not reachable on :8000 after 40s.",
file=sys.stderr)
win.load_html(FAILED_HTML)
icon = ICON_PATH if os.path.isfile(ICON_PATH) else None
webview.start(_swap_in_app, window, icon=icon) # blocks until the window is closed
return 0
except Exception as e: # no WebView2 runtime / backend failure
return _browser_fallback(f"native window failed ({e})")
def _browser_fallback(reason: str) -> int:
import webbrowser
print(f"[nexus] {reason}; opening default browser instead.", file=sys.stderr)
webbrowser.open(URL)
# Stay alive so the launcher keeps the services up. The user stops NexusOS
# by closing the launcher (or the browser tab, then the services idle out).
try:
while True:
time.sleep(3600)
except KeyboardInterrupt:
return 0
if __name__ == "__main__":
sys.exit(main())