On this page
Native pywebview desktop window backed by a detached Granian ASGI server subprocess, with filesystem-based cross-process window refcounting and automatic server lifecycle.
#src.wesktop.desktop
#src.wesktop.desktop
Native desktop window via pywebview, backed by a detached Granian ASGI server subprocess, with cross-process window refcounting and automatic server lifecycle.
Process-group / coordination story ---------------------------------- wesktop group-manages exactly ONE child: the detached server subprocess spawned by serve_background (which becomes its own process-group leader so stop can signal the whole group and reap granian workers). wesktop does NOT own the renderer child processes -- pywebview spawns and owns those (WebKitGTK/WebView2/ Cocoa) inside webview.start().
Because a single wesktop process can neither see nor count another wesktop process's windows, window lifecycle is coordinated through the filesystem, not in-process state:
- Window markers (
kind="window"in the fastware instance registry): one
marker file per open native window, carrying {pid, window_id}. Written before webview.start() and removed after it returns. The live-marker count (dead PIDs pruned by kill -0) is the true number of open windows across ALL wesktop processes sharing this app's server. The server is stopped only when zero live window markers remain after this process's window closes -- so process A closing its last window never kills the server under process B's still-open window.
- Focus-request markers (
kind="focus-request"): a platform-neutral,
file-based focus signal. With second_open="focus-existing", a second launch drops a focus-request marker and exits; the window-owning process runs a ~1s daemon poll that consumes the request and raises its window. No DBus, no AppleEvents.
- Registry entries (
list_instances): the detached server registers its
own {pid, port, name} descriptor. Together the registry entry and the live window markers are the cross-process enumeration surface (see :func:list_app_instances).
#_wire_runtime_bridge
def _wire_runtime_bridge(window: object, url: str) -> NoneBest-effort host-side update wiring for a native window.
Captures the window and, where pywebview exposes a focus event, polls /__fastware/version on focus to reload on a changed build id. This is a no-op on pywebview builds without a focus event -- native windows load the same page + client.js, which is the primary (poll-free) update path.
#_app_url
def _app_url(host: str, port: int) -> strCompose the same-origin packaged app URL from host and port.
The single source of truth for the URL an app window loads. In the join path the port comes from the port file; in the new-server path serve_background returns the same http://host:port form.
#_version_url
def _version_url(url: str) -> strThe fastware version endpoint for a given app URL.
#_port_from_url
def _port_from_url(url: str) -> intExtract the TCP port from an http://host:port URL.
#_startup_handshake
def _startup_handshake(url: str, *, timeout: float=5.0) -> str | NoneFetch /__fastware/version after window creation; loud stderr on failure.
Returns the observed build id, or None if the server is unreachable or the payload is malformed within timeout. The window is NEVER torn down on failure -- the user's window stays -- but the failure is logged loudly to stderr so it is unmissable.
#_inject_runtime_config
def _inject_runtime_config(window: object, build_id: str | None, port: int, app_name: str) -> boolBest-effort: set window.__wesktop = {buildId, port, appName} via JS.
Runtime-config injection is best-effort by nature -- pywebview's evaluate_js timing depends on the page being loaded. Retries ONCE on failure. Returns True if a call succeeded, False otherwise.
#_wire_runtime_config_injection
def _wire_runtime_config_injection(window: object, build_id: str | None, port: int, app_name: str) -> NoneWire runtime-config injection to the window's loaded event if present.
Injecting after page load is the reliable moment; when pywebview exposes no loaded event the injection is attempted immediately (best-effort).
#_raise_window
def _raise_window(window: object) -> NoneBest-effort raise-to-front of window.
Calls restore() (un-minimize) then show(). Raising a window ABOVE other applications' windows is window-manager dependent and not guaranteed on every platform -- this is the documented limitation of the platform- neutral, file-based focus signal.
#_request_focus_existing
def _request_focus_existing(pid_path: Path, existing_pid: int) -> NoneDrop a focus-request marker for the window-owning process and return.
Platform-neutral: the joining process writes a marker into the instance- registry dir and exits; the owning process's focus poll consumes it and raises its window.
#_install_focus_request_poll
def _install_focus_request_poll(window: object, pid_path: Path, stop_event: threading.Event, *, interval: float=1.0) -> threading.ThreadPoll for focus-request markers while the window is open; raise on request.
Runs a lightweight daemon thread that, every interval seconds until stop_event is set, consumes any focus-request markers (deleting them, even those owned by other/dead PIDs) and raises this window.
#AppInstances
A snapshot of an app's live server + windows from the registry.
#list_app_instances
def list_app_instances(pid_path: Path) -> AppInstancesEnumerate the app's live server instance(s) and open window markers.
Reads the fastware instance registry for pid_path: servers are the registered server descriptors (RegistryEntry); windows are the live per-window marker payloads (dicts with pid and window_id). Stale entries are pruned by the underlying registry reads.
#_require_webview_gui
def _require_webview_gui() -> objectImport pywebview and verify a GUI backend, returning the webview module.
Raises RuntimeError with an actionable message if pywebview is not installed or no GUI backend is available. Only called on paths that actually open a native window (the focus-existing early exit needs neither).
#_run_window
def _run_window(webview: object, window: object, url: str, pid_path: Path, port: int, app_name: str, icon: str | None) -> NoneManage a single native window's full lifecycle around webview.start().
Writes a per-window marker (cross-process refcount), runs the startup handshake, wires the runtime bridge + runtime-config injection + focus- request poll, blocks in webview.start(), then removes the marker and stops the server only when zero live window markers remain.
#ensure_gui_backend
def ensure_gui_backend() -> boolReport whether a native pywebview GUI backend is available, truthfully per platform.
On Linux, this additionally makes the system PyGObject importable in isolated venvs: if gi is not importable, common system site-packages locations are searched and the first one found is added to sys.path.
#_has_gui_backend
def _has_gui_backend() -> boolProbe whether pywebview can load a GUI backend.
Non-Linux platforms delegate to ensure_gui_backend(), which reports availability truthfully per platform. On Linux, honours the PYWEBVIEW_GUI env var and probes GTK first (via ensure_gui_backend, which also makes system PyGObject importable in isolated venvs), then Qt.
#_default_pid_path
def _default_pid_path(name: str) -> PathStable per-app PID file path under the platform runtime/state dir.
A CWD-relative default would defeat single-instance detection when the app is launched from different directories.
#_launch_command_parts
def _launch_command_parts() -> list[str]Reconstruct a runnable command line (as argv parts) for this process.
Handles the python -m pkg case, where sys.argv[0] is the package's __main__.py (a module file, not an executable): rebuilds sys.executable -m pkg instead.
#_auto_register_entry
def _auto_register_entry(title: str, icon: str | None) -> NoneCreate a desktop entry for this app if one doesn't exist.
On Linux/macOS a launcher script is created in ~/.local/bin and the entry points at it; this also self-heals: if an existing entry points to a missing launcher (e.g. the package was reinstalled to a different venv), the broken entry is removed and recreated with the current launcher path. On Windows the Start Menu shortcut points directly at the target -- a POSIX shell script cannot execute there.
#run
def run(target: str | Callable, *, title: str='wesktop', width: int=1280, height: int=800, icon: str | None=None, host: str | None=None, port: int | None=None, pid_path: Path | None=None, name: str='WESKTOP', pre_serve: Callable[[], None] | None=None, reload: bool=False, js_api: object | None=None, single_instance: bool=True, second_open: str='new-window') -> NoneStart server + open native desktop window. Blocks until window closes.
The server runs as a detached subprocess (see serve_background), so pre_serve and reload cannot work here and are hard errors: pre_serve would run in this process while the server re-imports the target in another, and a file watcher cannot restart the detached server. Use :func:wesktop.serve for both.
second_open selects what happens on a second launch while an instance is already running (single-instance join). It must be chosen explicitly from:
"new-window"(default): open an additional native window joined to the
existing server. Windows are refcounted across processes via marker files; the server stops only when the last window (in any process) closes.
"focus-existing": do NOT open a new window. Drop a platform-neutral
focus-request marker and exit; the process that owns the window raises it via a ~1s file-based poll. Raising above other apps is WM-dependent.