Skip to content

ui.window

WindowAPI and WindowChild for pywebview-based floating UI.

This document covers the legacy pywebview shell. The repo also has an optional Electron shell with the same multi-window popup behavior; see electron_shell.md and src/taskclf/ui/ELECTRON_MIGRATION.md.

Overview

Creates frameless, always-on-top, draggable windows backed by the platform webview (WebKit on macOS, Edge WebView2 on Windows). Exposes a WindowAPI to the SolidJS frontend via window.pywebview.api.

Three windows are managed:

Window Size (w x h) Purpose
Compact pill 150 x 30 Persistent header badge showing current label/app
Label grid 280 x 330 Quick-label popup (hidden by default)
State panel 280 x 520 System/history debug panel (hidden by default)

The compact pill is positioned at the top-right of the primary screen. Child windows (label grid, panel) are anchored below the pill on initial show. Once visible, children can be freely dragged to any monitor; they will not snap back until hidden and re-shown.

Electron uses the same three-window arrangement. Its main process creates separate popup BrowserWindow instances for the label grid (?view=label) and state panel (?view=panel), with an equivalent child-window state machine for hover, pin, delayed hide, and drag detection. Those popups are created on first use (not at process startup) so the pill dashboard can load with lower overhead; routes and markup match the pywebview shell once opened.

The compact route in the Solid app uses the same layout and host commands as pywebview and Electron. The child routes ?view=label and ?view=panel use the same markup as native popups (full viewport, top drag strip, hover handlers that call Host.invoke) — not a separate “browser preview” layout.

A plain browser tab (for example Vite alone, with no window.pywebview or window.electronHost) still loads that UI; the compact route uses a light solid page background plus right-aligned in-page label/panel popups with the same 300 ms delayed-hide feel as the native shell because Host.invoke window calls are no-ops there. The separate ?view=label and ?view=panel routes match native popup markup for focused testing; use pywebview or Electron for real multi-window behavior.

See host_window_drag_strip.md for the shared grab-bar component used on child routes.

WindowChild

Encapsulates the visibility / pin / timer state machine shared by the label-grid and state-panel child windows. Each WindowChild instance holds its own window reference, visibility and pin flags, a delayed-hide timer, and an expected-position tuple for drag detection.

Positioning logic is injected via a callback (position_fn) so each child can use different layout math while sharing the same state machine. WindowChild and WindowAPI are implemented as slotted dataclasses; constructor arguments remain unchanged.

Methods

Method Description
visibility_on(main) Show on hover (non-pinned); cancels any pending hide timer
visibility_off_deferred() Schedule a delayed hide (300 ms) unless pinned
visibility_off() Immediate hide — clears visible, pinned, and expected position
pin_toggle(main) Toggle pinned state (click to pin/unpin)
timer_cancel() Cancel any pending hide timer
position_sync() Reposition via the injected layout callback
drag_detected() True if the user has dragged the window away from expected position

WindowAPI

Python API exposed to JavaScript as window.pywebview.api.<method>(). The Host adapter in the frontend's host.ts calls these methods; components never reference window.pywebview directly.

Internally, WindowAPI delegates to two WindowChild instances (_label and _panel) for the label grid and state panel.

Public methods

Method Description
bind(window) Bind the main compact window and subscribe to its moved event
bind_label(label) Bind the label grid window
bind_panel(panel) Bind the state panel window
window_toggle() Toggle the compact pill's visibility
label_grid_show() Show the label grid below the pill (right-aligned)
label_grid_hide() Schedule a delayed hide of the label grid (300 ms)
show_transition_notification(prompt) Show a native desktop notification for a prompt_label event using privacy-safe copy plus the exact local time range
state_panel_toggle() Toggle the panel's visibility; positions below the label grid when both are visible
frontend_debug_log(message) Accept frontend debug lines from the webview and forward them to the Python logger at DEBUG level
frontend_error_log(message) Accept frontend error lines from the webview and forward them to the Python logger at ERROR level
visible Property returning whether the compact pill is currently visible

Window positioning

  • The label grid is placed at (pill.x + pill.width - grid.width, pill.y + pill.height + 4), right-aligned with the pill.
  • The panel is placed below the pill (or below the label grid if it is visible), also right-aligned.
  • When the pill is dragged, the label grid follows only if the user has not independently dragged it. Once the user drags a child window away (beyond a 10 px tolerance), that child stops following and stays where the user placed it.
  • Hiding a child window resets its expected position, so the next show re-anchors it to the pill.
  • Child windows hide with a 300 ms delay to avoid flicker on rapid toggle.

Dragging

All three windows use CSS pywebview-drag-region elements for drag handles. The compact pill uses left and right flex spacers (empty regions that grow with flex: 1) as drag targets; the label badge and status dot sit in a fixed center column outside those regions so they still receive hover/click events. Each child window keeps a small grab bar at the top. The main pill sets easy_drag=False to avoid conflicts between the native easy-drag handler and the CSS drag region, which previously caused glitches on multi-monitor setups.

window_run (ui.window_run)

window_run(
    port: int = 8741,
    on_ready: Callable[..., Any] | None = None,
    window_api: WindowAPI | None = None,
) -> None

Creates all three pywebview windows and starts the GUI loop. Blocks on the main thread until the user closes the window.

Defined in taskclf.ui.window_run.

Parameter Default Description
port 8741 Port of the FastAPI server to load in the webview
on_ready None Callback invoked after the GUI loop starts (receives the window object)
window_api None Shared WindowAPI instance; a new one is created when None

The main window loads http://127.0.0.1:{port}, the label grid loads ?view=label, and the panel loads ?view=panel. All windows are created with frameless=True, on_top=True, transparent=True.

On macOS, stderr is temporarily redirected to /dev/null during pywebview startup to suppress noisy WebKit warnings, then restored in the startup callback.

Integration

  • Used by the ui CLI command and ui.tray to launch the native window.
  • The WindowAPI instance is shared with the FastAPI app so that REST endpoints (e.g. POST /api/window/show-label-grid) can control window visibility.
  • Events flow from ActivityMonitor in ui.runtime through EventBus to the WebSocket layer inside the webview.

taskclf.ui.window

WindowAPI and WindowChild for pywebview-based floating UI.

WindowAPI is exposed to the SolidJS frontend via window.pywebview.api. WindowChild encapsulates the visibility / pin / timer state machine shared by the label-grid and state-panel child windows.

WindowChild dataclass

Visibility, pinning, and delayed-hide for an anchored child window.

Source code in src/taskclf/ui/window.py
@dataclass(eq=False)
class WindowChild:
    """Visibility, pinning, and delayed-hide for an anchored child window."""

    name: str
    position_fn: Callable[[WindowChild], None]
    window: Any = field(init=False, default=None)
    visible: bool = field(init=False, default=False)
    pinned: bool = field(init=False, default=False)
    hide_timer: threading.Timer | None = field(init=False, default=None)
    expected_pos: tuple[int, int] | None = field(init=False, default=None)

    def visibility_on(self, main: Any) -> None:
        """Show on hover (non-pinned)."""
        if self.window is None or main is None:
            return
        self.timer_cancel()
        if not self.visible:
            self.visible = True
            self.position_sync()
            try:
                self.window.show()
            except Exception:
                logger.debug("Could not show %s", self.name, exc_info=True)

    def visibility_off_deferred(self) -> None:
        """Schedule hide unless pinned."""
        if self.pinned:
            return
        self.timer_cancel()
        timer = threading.Timer(_CHILD_HIDE_DELAY_S, self.visibility_off)
        timer.daemon = True
        timer.start()
        self.hide_timer = timer

    def visibility_off(self) -> None:
        """Immediate hide — clears visible, pinned, and expected_pos."""
        self.hide_timer = None
        if self.window is not None:
            try:
                self.window.hide()
            except Exception:
                logger.debug("Could not hide %s", self.name, exc_info=True)
        self.visible = False
        self.pinned = False
        self.expected_pos = None

    def pin_toggle(self, main: Any) -> None:
        """Toggle pinned state."""
        if self.window is None or main is None:
            return
        if self.visible and self.pinned:
            self.pinned = False
            self.visibility_off()
        elif self.visible and not self.pinned:
            self.pinned = True
        else:
            self.timer_cancel()
            self.pinned = True
            self.visible = True
            self.position_sync()
            try:
                self.window.show()
            except Exception:
                logger.debug("Could not show %s", self.name, exc_info=True)

    def timer_cancel(self) -> None:
        """Cancel any pending hide timer."""
        if self.hide_timer is not None:
            self.hide_timer.cancel()
            self.hide_timer = None

    def position_sync(self) -> None:
        """Reposition via the injected layout callback."""
        self.position_fn(self)

    def drag_detected(self) -> bool:
        """True if the user has dragged this window away from expected position."""
        if not self.visible or self.window is None or self.expected_pos is None:
            return False
        try:
            cx, cy = self.window.x, self.window.y
            ex, ey = self.expected_pos
            return abs(cx - ex) > _DRAG_TOLERANCE or abs(cy - ey) > _DRAG_TOLERANCE
        except Exception:
            return False

visibility_on(main)

Show on hover (non-pinned).

Source code in src/taskclf/ui/window.py
def visibility_on(self, main: Any) -> None:
    """Show on hover (non-pinned)."""
    if self.window is None or main is None:
        return
    self.timer_cancel()
    if not self.visible:
        self.visible = True
        self.position_sync()
        try:
            self.window.show()
        except Exception:
            logger.debug("Could not show %s", self.name, exc_info=True)

visibility_off_deferred()

Schedule hide unless pinned.

Source code in src/taskclf/ui/window.py
def visibility_off_deferred(self) -> None:
    """Schedule hide unless pinned."""
    if self.pinned:
        return
    self.timer_cancel()
    timer = threading.Timer(_CHILD_HIDE_DELAY_S, self.visibility_off)
    timer.daemon = True
    timer.start()
    self.hide_timer = timer

visibility_off()

Immediate hide — clears visible, pinned, and expected_pos.

Source code in src/taskclf/ui/window.py
def visibility_off(self) -> None:
    """Immediate hide — clears visible, pinned, and expected_pos."""
    self.hide_timer = None
    if self.window is not None:
        try:
            self.window.hide()
        except Exception:
            logger.debug("Could not hide %s", self.name, exc_info=True)
    self.visible = False
    self.pinned = False
    self.expected_pos = None

pin_toggle(main)

Toggle pinned state.

Source code in src/taskclf/ui/window.py
def pin_toggle(self, main: Any) -> None:
    """Toggle pinned state."""
    if self.window is None or main is None:
        return
    if self.visible and self.pinned:
        self.pinned = False
        self.visibility_off()
    elif self.visible and not self.pinned:
        self.pinned = True
    else:
        self.timer_cancel()
        self.pinned = True
        self.visible = True
        self.position_sync()
        try:
            self.window.show()
        except Exception:
            logger.debug("Could not show %s", self.name, exc_info=True)

timer_cancel()

Cancel any pending hide timer.

Source code in src/taskclf/ui/window.py
def timer_cancel(self) -> None:
    """Cancel any pending hide timer."""
    if self.hide_timer is not None:
        self.hide_timer.cancel()
        self.hide_timer = None

position_sync()

Reposition via the injected layout callback.

Source code in src/taskclf/ui/window.py
def position_sync(self) -> None:
    """Reposition via the injected layout callback."""
    self.position_fn(self)

drag_detected()

True if the user has dragged this window away from expected position.

Source code in src/taskclf/ui/window.py
def drag_detected(self) -> bool:
    """True if the user has dragged this window away from expected position."""
    if not self.visible or self.window is None or self.expected_pos is None:
        return False
    try:
        cx, cy = self.window.x, self.window.y
        ex, ey = self.expected_pos
        return abs(cx - ex) > _DRAG_TOLERANCE or abs(cy - ey) > _DRAG_TOLERANCE
    except Exception:
        return False

WindowAPI dataclass

Python methods exposed to JS as window.pywebview.api.<method>().

Each method returns a value that pywebview serializes as a JSON Promise to the frontend. The Host adapter in host.ts calls these; components never reference window.pywebview directly.

Source code in src/taskclf/ui/window.py
@dataclass(eq=False)
class WindowAPI:
    """Python methods exposed to JS as ``window.pywebview.api.<method>()``.

    Each method returns a value that pywebview serializes as a JSON
    Promise to the frontend.  The ``Host`` adapter in ``host.ts``
    calls these; components never reference ``window.pywebview``
    directly.
    """

    _window: Any = field(init=False, default=None)
    _visible: bool = field(init=False, default=True)
    _default_x: int | None = field(init=False, default=None)
    _default_y: int | None = field(init=False, default=None)
    _label: WindowChild = field(init=False)
    _panel: WindowChild = field(init=False)

    def __post_init__(self) -> None:
        self._label = WindowChild("label", self._label_position)
        self._panel = WindowChild("panel", self._panel_position)

    def bind(self, window: Any) -> None:
        self._window = window
        try:
            window.events.moved += self._on_main_window_moved
        except Exception:
            logger.debug("Could not bind moved event", exc_info=True)

    def bind_label(self, label: Any) -> None:
        self._label.window = label

    def bind_panel(self, panel: Any) -> None:
        self._panel.window = panel

    def window_hide(self) -> None:
        if self._window is not None:
            self._window.hide()
        self._visible = False

    def window_show(self) -> None:
        if self._window is not None:
            self._window.show()
        self._visible = True

    def window_toggle(self) -> None:
        if self._visible:
            self.window_hide()
        else:
            self.window_show()

    def dashboard_toggle(self) -> None:
        """Toggle all windows. Re-show positions the pill at its default location."""
        logger.debug("dashboard_toggle called — visible=%s", self._visible)
        if self._visible:
            if self._label.visible:
                self._label.visibility_off()
            if self._panel.visible:
                self._panel.visibility_off()
            self.window_hide()
        else:
            if (
                self._window is not None
                and self._default_x is not None
                and self._default_y is not None
            ):
                try:
                    self._window.move(self._default_x, self._default_y)
                except Exception:
                    logger.debug(
                        "Could not reposition window to default", exc_info=True
                    )
            self.window_show()

    # -- Label grid window -----------------------------------------------------

    def label_grid_show(self) -> None:
        """Show label grid on hover (non-pinned)."""
        self._label.visibility_on(self._window)

    def label_grid_hide(self) -> None:
        """Schedule label grid hide unless pinned."""
        self._label.visibility_off_deferred()

    def label_grid_cancel_hide(self) -> None:
        """Cancel any pending label hide (e.g. mouse entered label window)."""
        self._label.timer_cancel()

    def label_grid_toggle(self) -> None:
        """Toggle pinned state of the label grid."""
        self._label.pin_toggle(self._window)

    def show_transition_notification(self, prompt: dict[str, Any]) -> None:
        """Show a native desktop notification for a transition prompt."""
        _send_desktop_notification(
            _TRANSITION_NOTIFICATION_TITLE,
            _transition_notification_body(prompt),
            timeout=10,
        )

    # -- State panel window ----------------------------------------------------

    def state_panel_show(self) -> None:
        """Show panel on hover (non-pinned)."""
        self._panel.visibility_on(self._window)

    def state_panel_hide(self) -> None:
        """Schedule panel hide unless pinned."""
        self._panel.visibility_off_deferred()

    def state_panel_cancel_hide(self) -> None:
        """Cancel any pending panel hide (e.g. mouse entered panel window)."""
        self._panel.timer_cancel()

    def state_panel_toggle(self) -> None:
        """Toggle pinned state of the panel."""
        self._panel.pin_toggle(self._window)

    def frontend_debug_log(self, message: str) -> None:
        """Accept debug log lines from the frontend webview."""
        if not logger.isEnabledFor(logging.DEBUG):
            return
        logger.debug("[frontend] %s", message)

    def frontend_error_log(self, message: str) -> None:
        """Accept error log lines from the frontend webview."""
        logger.error("[frontend] %s", message)

    # -- Positioning -----------------------------------------------------------

    def _label_position(self, child: WindowChild) -> None:
        """Place label grid below pill, right-aligned."""
        if self._window is None:
            return
        try:
            if child.visible and child.window is not None:
                new_x = self._window.x + _COMPACT_SIZE[0] - _LABEL_SIZE[0]
                new_y = self._window.y + _COMPACT_SIZE[1] + 4
                child.window.move(new_x, new_y)
                child.expected_pos = (new_x, new_y)
        except Exception:
            logger.debug("Could not reposition label window", exc_info=True)

    def _panel_position(self, child: WindowChild) -> None:
        """Place panel below pill (and below label grid if visible)."""
        if self._window is None or child.window is None:
            return
        try:
            right_x = self._window.x + _COMPACT_SIZE[0]
            y = self._window.y + _COMPACT_SIZE[1] + 4
            if self._label.visible:
                y += _LABEL_SIZE[1] + 4
            new_x = right_x - _PANEL_SIZE[0]
            child.window.move(new_x, y)
            child.expected_pos = (new_x, y)
        except Exception:
            logger.debug("Could not position panel window", exc_info=True)

    def _on_main_window_moved(self) -> None:
        """Reposition child windows, unless the user has dragged them away."""
        if not self._label.drag_detected():
            self._label_position(self._label)
        if not self._panel.drag_detected():
            self._panel_position(self._panel)

    @property
    def visible(self) -> bool:
        return self._visible

dashboard_toggle()

Toggle all windows. Re-show positions the pill at its default location.

Source code in src/taskclf/ui/window.py
def dashboard_toggle(self) -> None:
    """Toggle all windows. Re-show positions the pill at its default location."""
    logger.debug("dashboard_toggle called — visible=%s", self._visible)
    if self._visible:
        if self._label.visible:
            self._label.visibility_off()
        if self._panel.visible:
            self._panel.visibility_off()
        self.window_hide()
    else:
        if (
            self._window is not None
            and self._default_x is not None
            and self._default_y is not None
        ):
            try:
                self._window.move(self._default_x, self._default_y)
            except Exception:
                logger.debug(
                    "Could not reposition window to default", exc_info=True
                )
        self.window_show()

label_grid_show()

Show label grid on hover (non-pinned).

Source code in src/taskclf/ui/window.py
def label_grid_show(self) -> None:
    """Show label grid on hover (non-pinned)."""
    self._label.visibility_on(self._window)

label_grid_hide()

Schedule label grid hide unless pinned.

Source code in src/taskclf/ui/window.py
def label_grid_hide(self) -> None:
    """Schedule label grid hide unless pinned."""
    self._label.visibility_off_deferred()

label_grid_cancel_hide()

Cancel any pending label hide (e.g. mouse entered label window).

Source code in src/taskclf/ui/window.py
def label_grid_cancel_hide(self) -> None:
    """Cancel any pending label hide (e.g. mouse entered label window)."""
    self._label.timer_cancel()

label_grid_toggle()

Toggle pinned state of the label grid.

Source code in src/taskclf/ui/window.py
def label_grid_toggle(self) -> None:
    """Toggle pinned state of the label grid."""
    self._label.pin_toggle(self._window)

show_transition_notification(prompt)

Show a native desktop notification for a transition prompt.

Source code in src/taskclf/ui/window.py
def show_transition_notification(self, prompt: dict[str, Any]) -> None:
    """Show a native desktop notification for a transition prompt."""
    _send_desktop_notification(
        _TRANSITION_NOTIFICATION_TITLE,
        _transition_notification_body(prompt),
        timeout=10,
    )

state_panel_show()

Show panel on hover (non-pinned).

Source code in src/taskclf/ui/window.py
def state_panel_show(self) -> None:
    """Show panel on hover (non-pinned)."""
    self._panel.visibility_on(self._window)

state_panel_hide()

Schedule panel hide unless pinned.

Source code in src/taskclf/ui/window.py
def state_panel_hide(self) -> None:
    """Schedule panel hide unless pinned."""
    self._panel.visibility_off_deferred()

state_panel_cancel_hide()

Cancel any pending panel hide (e.g. mouse entered panel window).

Source code in src/taskclf/ui/window.py
def state_panel_cancel_hide(self) -> None:
    """Cancel any pending panel hide (e.g. mouse entered panel window)."""
    self._panel.timer_cancel()

state_panel_toggle()

Toggle pinned state of the panel.

Source code in src/taskclf/ui/window.py
def state_panel_toggle(self) -> None:
    """Toggle pinned state of the panel."""
    self._panel.pin_toggle(self._window)

frontend_debug_log(message)

Accept debug log lines from the frontend webview.

Source code in src/taskclf/ui/window.py
def frontend_debug_log(self, message: str) -> None:
    """Accept debug log lines from the frontend webview."""
    if not logger.isEnabledFor(logging.DEBUG):
        return
    logger.debug("[frontend] %s", message)

frontend_error_log(message)

Accept error log lines from the frontend webview.

Source code in src/taskclf/ui/window.py
def frontend_error_log(self, message: str) -> None:
    """Accept error log lines from the frontend webview."""
    logger.error("[frontend] %s", message)

taskclf.ui.window_run

Webview lifecycle bootstrap for the taskclf floating window.

Creates all three pywebview windows (compact pill, label grid, state panel) and starts the GUI event loop. Blocks on the main thread.

window_run(port=8741, on_ready=None, window_api=None)

Create and start the pywebview floating window (blocks on main thread).

Parameters:

Name Type Description Default
port int

Port of the FastAPI server to load in the webview.

8741
on_ready Callable[..., Any] | None

Optional callback invoked after the GUI loop starts. Receives the window object as its first argument.

None
window_api WindowAPI | None

Shared WindowAPI instance. When None a new one is created.

None
Source code in src/taskclf/ui/window_run.py
def window_run(
    port: int = 8741,
    on_ready: Callable[..., Any] | None = None,
    window_api: WindowAPI | None = None,
) -> None:
    """Create and start the pywebview floating window (blocks on main thread).

    Args:
        port: Port of the FastAPI server to load in the webview.
        on_ready: Optional callback invoked after the GUI loop starts.
            Receives the window object as its first argument.
        window_api: Shared ``WindowAPI`` instance.  When ``None`` a new
            one is created.
    """
    import os
    import sys

    import webview

    api = window_api or WindowAPI()

    screens = webview.screens
    primary = screens[0] if screens else None
    x = (primary.width - _COMPACT_SIZE[0] - 16) if primary else None
    y = 16 if primary else None

    api._default_x = x
    api._default_y = y

    window = webview.create_window(
        "taskclf",
        url=f"http://127.0.0.1:{port}",
        width=_COMPACT_SIZE[0],
        height=_COMPACT_SIZE[1],
        x=x,
        y=y,
        frameless=True,
        on_top=True,
        easy_drag=False,
        resizable=False,
        transparent=True,
        js_api=api,
    )
    api.bind(window)

    label_x = (x + _COMPACT_SIZE[0] - _LABEL_SIZE[0]) if x is not None else 0
    label_y = (y + _COMPACT_SIZE[1] + 4) if y is not None else 50
    label = webview.create_window(
        "taskclf-label",
        url=f"http://127.0.0.1:{port}?view=label",
        width=_LABEL_SIZE[0],
        height=_LABEL_SIZE[1],
        x=label_x,
        y=label_y,
        frameless=True,
        on_top=True,
        easy_drag=False,
        resizable=False,
        transparent=True,
        js_api=api,
        hidden=True,
    )
    api.bind_label(label)

    panel_x = (x + _COMPACT_SIZE[0] - _PANEL_SIZE[0]) if x is not None else 0
    panel_y = (y + _COMPACT_SIZE[1] + 4) if y is not None else 50
    panel = webview.create_window(
        "taskclf-panel",
        url=f"http://127.0.0.1:{port}?view=panel",
        width=_PANEL_SIZE[0],
        height=_PANEL_SIZE[1],
        x=panel_x,
        y=panel_y,
        frameless=True,
        on_top=True,
        easy_drag=False,
        resizable=False,
        transparent=True,
        js_api=api,
        hidden=True,
    )
    api.bind_panel(panel)

    def _stdin_reader() -> None:
        """Read commands from stdin (sent by the tray process)."""
        try:
            while True:
                line = sys.stdin.readline()
                if not line:
                    logger.debug("stdin EOF — reader exiting")
                    break
                cmd = line.strip()
                logger.debug("stdin command received: %r", cmd)
                if cmd == "toggle":
                    api.dashboard_toggle()
        except Exception:
            logger.debug("stdin reader error", exc_info=True)

    stdin_thread = threading.Thread(target=_stdin_reader, daemon=True)
    stdin_thread.start()

    saved_stderr_fd: int | None = None
    if sys.platform == "darwin":
        try:
            saved_stderr_fd = os.dup(2)
            devnull = os.open(os.devnull, os.O_WRONLY)
            os.dup2(devnull, 2)
            os.close(devnull)
        except OSError:
            logger.debug("Could not redirect stderr for pywebview", exc_info=True)
            saved_stderr_fd = None

    def _startup(win: Any) -> None:
        nonlocal saved_stderr_fd
        if saved_stderr_fd is not None:
            try:
                os.dup2(saved_stderr_fd, 2)
                os.close(saved_stderr_fd)
            except OSError:
                logger.debug(
                    "Could not restore stderr after pywebview init", exc_info=True
                )
            saved_stderr_fd = None
        if on_ready is not None:
            on_ready(win)

    webview.start(func=_startup, args=[window])