Skip to content

Window Modes

Every app.show() call needs to decide where the content appears — a new OS window, the same window, a browser tab, or an inline notebook cell. PyWry uses five window modes to control this behavior.

The Five Modes

Mode Enum value What happens on each show()
New Window WindowMode.NEW_WINDOW Opens a fresh native OS window every time
Single Window WindowMode.SINGLE_WINDOW Replaces content in one reusable window
Multi Window WindowMode.MULTI_WINDOW Opens or updates windows by label
Browser WindowMode.BROWSER Starts a local web server, opens system browser
Notebook WindowMode.NOTEBOOK Renders inline in a Jupyter notebook cell

Setting the Mode

from pywry import PyWry, WindowMode

# At construction
app = PyWry(mode=WindowMode.SINGLE_WINDOW)

# Or via environment variable (before import)
# PYWRY_WINDOW__MODE=single_window

PyWry auto-detects Notebook mode when running inside Jupyter. For all other environments the default is NEW_WINDOW.


NEW_WINDOW

Each show() call creates an independent native window. This is the default for scripts and CLI usage.

app = PyWry(mode=WindowMode.NEW_WINDOW)

handle1 = app.show("<h1>Window 1</h1>", label="win1")
handle2 = app.show("<h1>Window 2</h1>", label="win2")
# Two separate OS windows appear

Use this when the user needs to see multiple pieces of content side-by-side, each with its own lifecycle.

SINGLE_WINDOW

All content renders in the same window. Calling show() again replaces whatever was there before.

app = PyWry(mode=WindowMode.SINGLE_WINDOW)

app.show("<h1>Loading...</h1>")
# ... fetch data ...
app.show("<h1>Dashboard</h1>")  # Replaces "Loading..."

Use this for a single-page-app experience where there's only ever one viewport.

MULTI_WINDOW

Like NEW_WINDOW, but labels let you reuse existing windows. If a window with the given label already exists, its content is replaced instead of opening a new one.

app = PyWry(mode=WindowMode.MULTI_WINDOW)

app.show("<h1>Main</h1>", label="main")
app.show("<h1>Settings</h1>", label="settings")

# Later — update the main window without opening a third one
app.show("<h1>Updated Main</h1>", label="main")

Use this for multi-panel applications where each panel has a stable identity.

BROWSER

No native window is created. PyWry starts a FastAPI server in a background thread and opens the system browser to the widget URL. Communication happens over WebSocket.

app = PyWry(mode=WindowMode.BROWSER)

handle = app.show("<h1>Hello Browser!</h1>")
# Browser opens http://127.0.0.1:8765/widget/{label}

Use this for SSH sessions, remote servers, Docker containers, or any environment without a display server. See Browser Mode for full details.

NOTEBOOK

Content renders inline in a Jupyter notebook cell. PyWry uses anywidget for bidirectional traitlet sync when available, falling back to an IFrame + WebSocket bridge.

# Usually auto-detected — no need to set explicitly
app = PyWry()  # detects Jupyter automatically

app.show_plotly(fig)  # Renders inline in the cell output

Labels

Every window or widget has a label — a unique string identifier. You can set it explicitly or let PyWry generate a UUID.

# Explicit label
handle = app.show(content, label="dashboard")

# Auto-generated (UUID)
handle = app.show(content)
print(handle.label)  # e.g., "a3f1c2d4-..."

Labels are used to:

  • Target a specific window for content updates (MULTI_WINDOW mode)
  • Identify the source window in event callbacks (label parameter)
  • Construct widget URLs in browser mode (/widget/{label})
  • Route handle.emit() calls to the correct window

Window Properties

Native windows accept layout properties through show():

handle = app.show(
    "<h1>My App</h1>",
    title="My Application",
    width=1280,
    height=720,
)

WindowConfig Defaults

Property Type Default Description
title str "PyWry" Window title bar text
width int 1280 Window width in pixels
height int 720 Window height in pixels
min_width int 400 Minimum resize width
min_height int 300 Minimum resize height
theme ThemeMode DARK LIGHT, DARK, or SYSTEM
center bool True Center window on screen
resizable bool True Allow window resizing
decorations bool True Show title bar and borders
always_on_top bool False Keep above other windows
devtools bool False Open browser DevTools on launch

The Window Handle

app.show() returns a NativeWindowHandle in native mode — a wrapper that provides the same emit() / on() interface as notebook widgets, plus access to the full OS window API via its .proxy attribute. In browser/notebook mode it's an InlineWidget with the same event interface.

Common Methods (all modes)

Method Description
handle.emit(event, data) Send an event from Python to the window's JavaScript
handle.on(event, callback) Register a Python callback for events from the window
handle.label The window/widget label
handle.alert(message, ...) Show a toast notification
handle.set_toolbar_value(id, value, ...) Update a toolbar component's value
handle.set_toolbar_values({id: value, ...}) Update multiple toolbar components
handle.request_toolbar_state() Request current toolbar state

Native-only Methods (WindowProxy)

The WindowProxy (accessible via handle.proxy) exposes the full set of OS window operations:

Window state:

Method Description
show() / hide() Toggle visibility
close() / destroy() Close the window
maximize() / unmaximize() Maximize or restore
minimize() / unminimize() Minimize or restore
center() Center on screen
set_focus() Bring to front
set_fullscreen(bool) Enter/exit fullscreen
set_always_on_top(bool) Pin above other windows

Window properties:

Method Description
set_title(str) Change title bar text
set_size(PhysicalSize) Resize window
set_position(PhysicalPosition) Move window
set_decorations(bool) Toggle title bar
set_resizable(bool) Toggle resizing
set_theme(ThemeMode) Change theme

Read-only properties (via handle.proxy):

handle.proxy.inner_size       # Content area size (PhysicalSize)
handle.proxy.outer_size       # Window frame size
handle.proxy.inner_position   # Content position on screen
handle.proxy.is_fullscreen    # bool
handle.proxy.is_maximized     # bool
handle.proxy.is_focused       # bool
handle.proxy.is_visible       # bool
handle.proxy.current_monitor  # Monitor info (name, size, position, scale)

JavaScript execution:

# Fire-and-forget
handle.eval_js("document.title = 'Hello'")

# With return value (blocks up to timeout, via proxy)
result = handle.proxy.eval_with_result("document.querySelectorAll('li').length", timeout=5.0)

Updating Content

All modes support updating widget content through events:

# Replace the entire page
handle.emit("pywry:update-html", {"html": "<h1>New Content</h1>"})

# Update a specific element by ID
handle.emit("pywry:set-content", {"id": "title", "text": "Updated!"})

# Update by CSS selector
handle.emit("pywry:set-content", {"selector": ".status", "html": "<b>Online</b>"})

Multi-Window Communication

Windows are isolated — they don't share DOM or JavaScript state. Communication routes through Python callbacks:

windows = {}

def on_action(data, event_type, label):
    # Forward data from "main" to "sidebar"
    sidebar = windows.get("sidebar")
    if sidebar:
        sidebar.emit("app:update", data)

windows["main"] = app.show(
    main_html,
    label="main",
    callbacks={"app:action": on_action},
)
windows["sidebar"] = app.show(sidebar_html, label="sidebar")

Blocking

Scripts exit when the main thread ends. app.block() keeps the process alive until all windows close (or the user presses Ctrl+C):

app.show("<h1>Hello</h1>")
app.block()  # Waits here until the window is closed

How blocking works depends on the mode:

  • Native modes — polls get_labels() every 100ms until no windows remain
  • Browser mode — monitors WebSocket disconnections; blocks until all widgets disconnect
  • Both catch KeyboardInterrupt and call app.destroy() to clean up
# Block until a specific window closes
app.block(label="main")