Source code for klea_utils.ui.web.nicegui.components.header
#!/usr/bin/env python3
"""
Header component: title bar, dark-mode toggle and exit button.
File: klea_utils/ui/web/nicegui/components/header.py
Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""
import asyncio
import logging
from nicegui import app, background_tasks, ui
from klea_utils.ui.exit_confirm import EXIT_CONFIRM_MESSAGE, EXIT_CONFIRM_TITLE
from klea_utils.ui.web.nicegui.components.context import PageContext
from klea_utils.ui.web.nicegui.components.stream import stop_all_streams
logger = logging.getLogger(__name__)
#: Delay before shutting the server down, so the fire-and-forget
#: ``window.close()`` has a chance to reach the browser first.
_TAB_CLOSE_GRACE_SECONDS = 0.2
[docs]
def attach_header(ctx: PageContext) -> None:
"""Build the page header: title, subtitle, dark-mode toggle, exit button.
Relies on ``ctx.dark`` being set by :func:`theme.install_theme`
before this is called. Registers ``ctx.exit_app``: it stops this page's
active runs, then shuts the NiceGUI server down (which, for a
client-spawned server, ends the subprocess via the CLI's process-group
teardown). ``ctx.exit_app`` is the immediate action used by the ``/exit``
client command; the header's exit button confirms first, since an
accidental click would stop the server for every open tab.
:param ctx: The shared page context.
"""
def _exit_app():
"""Stop the page's runs, close the tab, then shut the server down."""
logger.info("exit requested from the web header")
stop_all_streams(ctx)
_close_browser_tab()
# Give the fire-and-forget ``window.close()`` a moment to reach the
# browser before the socket drops, then stop the server.
background_tasks.create(_shutdown_soon())
def _close_browser_tab() -> None:
"""Best-effort ``window.close()``.
Browsers only let a script close a window it opened itself, so this is
a no-op for a normal, user-opened tab; the server is stopped either way.
"""
try:
ui.run_javascript("window.close()")
except Exception as e: # noqa: BLE001
logger.debug("window.close() not sent: %s", e)
async def _shutdown_soon() -> None:
await asyncio.sleep(_TAB_CLOSE_GRACE_SECONDS)
app.shutdown()
def _confirm_exit() -> None:
"""Confirm before exiting: the header button is easy to hit by accident.
The dialog always shows (an accidental click would stop the web server
for every open tab, not just this one); ``/exit`` skips it because
typing the command is deliberate.
"""
logger.debug("opening exit-confirmation dialog")
dialog = ui.dialog()
with dialog, ui.card():
ui.label(EXIT_CONFIRM_TITLE).classes("text-lg font-bold")
ui.label(EXIT_CONFIRM_MESSAGE).classes("text-sm")
with ui.row().classes("w-full justify-end"):
ui.button("Cancel", on_click=dialog.close)
ui.button("Exit", on_click=lambda: (dialog.close(), _exit_app())).props(
"unelevated color=negative"
)
dialog.open()
ctx.exit_app = _exit_app
with ui.header().classes("items-center"):
ui.label(ctx.title).classes("text-xl font-bold")
if ctx.subtitle:
ui.label(ctx.subtitle).classes("text-sm text-grey-4 ml-2 mr-2")
ui.space()
def _toggle_dark():
"""Flip the dark-mode flag in ``app.storage.user``."""
ctx.dark.value = not ctx.dark.value
logger.debug("toggled dark mode: value=%s", ctx.dark.value)
ui.button(icon="dark_mode", on_click=_toggle_dark).props(
"flat color=white round"
)
ui.button(icon="power_settings_new", on_click=_confirm_exit).props(
"flat color=white round"
).tooltip("Exit")