Scripts

Talking to the user

Toasts, banners, dialogs and pickers, progress, the HTML output window and your own windows: what each one is for, every function's signature, and how they follow AutoCAD's theme.

A tool that runs and shows nothing reads as broken. A tool that opens a window to say one sentence reads as clumsy. Between those two failures sits a small set of surfaces, each with one job, and most of this page is about matching the message to the surface. Reach for the lightest one that can carry what you have to say.

Choosing a surface#

The question If this is true Use What do you need to tell the user, and how big is it? The script cannot continue until the user answers. The action is finished and the result fits on one line. One value is the whole point, or an instruction to follow. It is a table, a list, links, a log, or a report to keep. It is a real tool: many fields, a preview, a list to work through. forms modal, waits for an answer toast a corner note, goes by itself banner full width, large type output HTML window, scrollable forms.WPFWindow your XAML, or a dock pane
Read the conditions top to bottom and take the first that is true. Most tools need a dialog or two, a toast, and now and then the output window.
SurfaceBlocks the script?Use it forNot for
pyacad.toastNo The result of a finished action, in one line. Questions; anything the user must read to carry on.
pyacad.bannerNo The one value a tool exists to produce (a length, a count), or the instruction a tool is waiting on. Routine confirmations a toast would carry.
pyacad.formsYes A question that must be answered; picking from a list; files and folders; a cancellable progress window. Reporting a result nobody asked about.
pyacad.outputNo Tables, lists, links that select objects, charts, long logs, a report to save. One sentence. Opening a window to say one line is a defect.
forms.WPFWindow or a *.dockpaneYour choice A tool with many fields, a preview, or a list to work through. Anything the four above already do.

All of them are themed for you (see Theming), and all of them are safe under pyacad run, where AutoCAD runs with no screen: toasts, banners and alerts are written to the run's transcript instead, and anything that would wait for an answer raises (see Under pyacad run).

Toast: one line, no interruption#

python
from pyacad import toast

toast.success('Plotted 12 layouts to PDF', r'D:\Issue\2026-10')
toast.info('Nothing selected', 'Select the blocks to count, then run this again.')
toast.warning('Plotted 10 of 12 layouts', '2 failed')
toast.error('Could not reach the xref', r'\\server\cad\base.dwg')
toast.show('warning', 'Same thing, with the level as a string')
FunctionReturns
toast.success(message, detail=None)None
toast.info(message, detail=None)None
toast.warning(message, detail=None)None
toast.error(message, detail=None)None
toast.show(level, message, detail=None)None

A toast is a small card, 300 pixels wide, in the bottom-right corner of the AutoCAD window: the message in semibold, the optional detail line under it in muted text, a border and a left rail in the level's colour (success green, error red, info blue, warning amber).

  • It never takes focus. The user keeps typing at the command line while it shows.
  • It goes by itself: after 4 seconds for success and info, 5 for a warning and 7 for an error, which carries something to read. Hovering holds it; a click dismisses it at once.
  • Three at most. Toasts stack upwards from the corner, newest at the bottom. A fourth dismisses the oldest.
  • It never raises. A toast that cannot be shown is logged; a toast is never worth failing a script over. It may be called from any thread.
  • Reload clears them, along with any banner.
An unknown level is a green success

toast.show accepts 'success', 'error', 'info' and 'warning', trimmed and in any case. Anything else, including a typo such as 'warn', shows as a success. Use the four named functions and the question never comes up.

Never print a one-line status#

print() writes to the output window, and the first write opens it. A print('Done') at the end of a script opens a whole window to hold one word. One-line results go through pyacad.toast; print() is for output someone will actually read.

Do
toast.success('Renamed 14 layers')
Don't
print('Renamed 14 layers')

Writing a good toast#

  • Lead with the result, and put the number in the message: Plotted 12 layouts to PDF, not Done.
  • Use the detail for where or what next: the folder written to, or the one step that fixes an empty state.
  • Say nothing on Cancel. The user just told you they want nothing to happen.
  • Pick the level by outcome: success when everything worked, warning when part of it did, error when nothing did, info when there was nothing to do.

Where a toast is a note in the corner, a banner is a readout: a bar the full width of the AutoCAD window, sitting on its bottom edge, filled solid in the level colour, with the message in large type (22 px) and an optional detail line under it. It is for the one value a tool exists to produce, legible from across the room, and for the instruction a tool is waiting on.

python
from pyacad import banner, units

banner.success(units.format_length(total, units.current()), '14 polylines on M-DUCT')
banner.prompt('Pick the first point of the run', 'Esc to stop')   # stays until cleared
banner.clear()
FunctionStays for
banner.success(message, detail=None), banner.info(...) 8 seconds
banner.warning(message, detail=None)10 seconds
banner.error(message, detail=None)12 seconds
banner.show(level, message, detail=None, seconds=None) None: the level's own time. 0: until cleared. Any other number: that many seconds.
banner.prompt(message, detail=None) Until replaced or cleared (an info banner with seconds=0).
banner.clear()Removes the banner, if one is showing.

There is only ever one banner: a new one replaces the last. Like a toast it never takes focus, hovering holds its timer, a click dismisses it, Reload removes it, and a failure to show it is logged rather than raised. Levels and the unknown-level rule are the toast's.

Dialogs: when you need an answer#

pyacad.forms holds every prebuilt dialog. They are modal and owned by the AutoCAD window, so they stay in front of it and the script waits on the line that showed them. None of them touches the AutoCAD API, so they work with no drawing open.

FunctionReturnsOn cancel
forms.alert(message, title='pyACAD', copy=None) None(nothing to cancel)
forms.confirm(message, title='pyACAD') True on YesFalse
forms.ask_string(prompt, default='', title='pyACAD') The textNone
forms.ask_number(prompt, default=None, min_value=None, max_value=None, title='pyACAD')A float in rangeNone
forms.ask_options(prompt, options, title='pyACAD') The option clickedNone
forms.select_from_list(items, title='Select', multiselect=False, prompt=None, checked=None) The value, or a list of values with multiselectNone
forms.pick_folder(title='Select folder', initial=None) A folder pathNone
forms.open_file(filter='All files (*.*)|*.*', title='Open file') A file pathNone
forms.save_file(filter='CSV files (*.csv)|*.csv|All files (*.*)|*.*', default_name='', title='Save file') A file pathNone

The rule for every one of them: test for None (or False) and stop quietly. Never toast "cancelled".

python
from pyacad import forms

name = forms.ask_string('New layer name:', default='A-NOTES', title='New layer')
if name is None:
    raise SystemExit          # cancelled: change nothing, say nothing

An empty string is not a cancel: ask_string returns '' when the user clears the box and presses OK, so check for both if empty is not a valid answer.

alert#

A message with one OK button. Pass copy='...' and the dialog grows a Copy button on the left that puts that text on the clipboard without closing the dialog (it reads Copied for a moment). Batch Plot uses it for the list of layouts that failed, so the user can paste it into an email.

python
text = '\n'.join('%s: %s' % (layout, reason) for layout, reason in failures)
forms.alert('%d layouts did not plot:\n\n%s' % (len(failures), text), 'Batch plot', copy=text)

confirm#

A Yes/No question. Enter picks No and Escape picks No: confirm is the dialog you show before destroying something, and an absent-minded Enter must never delete 400 layouts. Phrase the question so Yes is the action: Delete 3 layouts?, not Keep the layouts?

ask_number#

A numeric field with live validation. While the text is not a number, or is outside min_value and max_value, the field turns red with a message such as Enter a number between 1 and 12. and OK is disabled, so a returned value is always in range. The result is always a float: wrap it in int() for a count. The field parses with a dot as the decimal separator whatever the Windows locale.

python
position = forms.ask_number('New position for A3-02, from 1 to 12', default=4,
                            min_value=1, max_value=12, title='Layout manager')
if position is not None:
    position = int(position)

ask_options#

A one-click choice between a few options, drawn as a row of buttons. Clicking a button is the answer; there is no OK. Escape or closing the window returns None. The options can be any objects: their str() is the label, and the object itself comes back.

python
EXPORT, COPY, CLOSE = 'Export to Excel', 'Copy totals', 'Close'
choice = forms.ask_options('Total run length: 412.6 m', [EXPORT, COPY, CLOSE], 'Length takeoff')

Pass None or '' as the prompt for the bare quick-switch look: no title bar and no question, just the buttons in a thin frame, and a click anywhere else dismisses it like Escape.

select_from_list#

A searchable list. Items are strings, or (label, value) tuples when what you show is not what you want back. The search box filters by substring, ignoring case. The list is virtualised, so ten thousand layer names are as quick as ten.

python
from pyacad import forms, layers

# Single select: double-click or OK returns one value.
name = forms.select_from_list(layers.names(), 'Make current', prompt='Layer to make current')

# Multiselect with (label, value) pairs, opening with the saved choice ticked.
picked = forms.select_from_list([('Model', 'model'), ('A3-01', 'A3-01'), ('A3-02', 'A3-02')],
                                'Batch plot', multiselect=True,
                                prompt='Layouts to plot', checked=['A3-01'])
  • Single select returns one value. OK stays disabled until a row is picked, and a double-click picks and closes.
  • Multiselect draws a checkbox per row, counts N selected in the footer, and returns a list in the items' order. OK stays disabled until at least one row is ticked, so the result is never an empty list: it is a list, or None.
  • checked is a list of values to open with ticked (or, single select, the first of them picked). Values that are not among the items are ignored, so passing a stale saved choice is safe.
  • title is the second positional argument, before multiselect; the prompt line above the search box only shows when you pass one.

Files and folders#

open_file and save_file are the standard Windows dialogs; the save dialog asks before overwriting. The filter string is the Windows format: pairs of description|pattern, all joined by |, with ; between patterns in one entry.

python
path = forms.save_file(filter='Excel workbooks (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv',
                       default_name='blocks.xlsx', title='Export blocks')
dwgs = forms.open_file(filter='Drawings (*.dwg;*.dxf)|*.dwg;*.dxf', title='Pick a drawing')
folder = forms.pick_folder('Folder for the PDFs', initial=r'D:\Issue')

pick_folder is the modern Windows folder picker. initial is used only when that folder exists. Any failure inside it is logged and returned as None, the same as a cancel.

A progress window with a Cancel button#

For long work that changes the drawing, forms.progress(title, label='') is a context manager that shows a small progress window with a Cancel button.

python
from pyacad import forms, plot, toast

done = 0
try:
    with forms.progress('Batch plot', 'Starting') as bar:
        for index, (layout, path) in enumerate(targets):
            bar.check()                                   # raises Cancelled after Cancel
            bar.update(float(index) / len(targets),
                       'Plotting %s (%d of %d)' % (layout, index + 1, len(targets)))
            plot.to_pdf(layout, path)
            done += 1
        bar.update(1.0, 'Done')
except forms.Cancelled:
    toast.info('Batch plot stopped', 'Plotted %d before you cancelled' % done)
CallDoes
bar.update(fraction, label=None) Moves the bar (0 to 1, clamped) and, when given, replaces the label. Returns False once the user has clicked Cancel, True otherwise.
bar.check() Raises forms.Cancelled if Cancel was clicked.

Four things about it are deliberate:

  • AutoCAD is disabled while it shows. A script runs on AutoCAD's UI thread, so showing progress means letting the message loop run, and that would let the user edit the very objects the script is half way through changing. The window disables input to AutoCAD for its lifetime instead. It has no title bar and no close button.
  • Cancel only raises a flag. Clicking it relabels the window Cancelling...; your loop notices at its next update() or check() and stops at a clean point. Nothing is aborted for you.
  • Call one of the two often. They are also what lets the Cancel click through (throttled to five times a second, so calling them on every item is cheap). A loop that calls neither cannot be cancelled.
  • It cannot be leaked. Leaving the with block closes it and gives AutoCAD its input back, and if a script dies with one open, the runtime closes it after the run anyway.

The output window#

The output window is where anything bigger than a line goes. It is created lazily: the first print() or pyacad.output call of a run opens it, titled pyACAD - <tool title>, and later writes append. A script that only toasts never opens one. Each run gets its own window, so earlier reports stay open until the user closes them, and if the user closes the window mid-run the next write opens a fresh one.

python
from pyacad import output

output.print_html(html)                      # an HTML fragment
output.print_md(markdown)                    # a small markdown subset
output.print_table(rows, headers)            # a styled table
output.print_code(text)                      # an escaped monospace block
output.print_image(path, caption=None)       # png, jpg, gif or svg, embedded
output.progress(fraction, label='')          # a bar at the bottom; 1.0 completes it
output.chart_bar(labels, values, title=None)
output.chart_line(labels, series, title=None)        # series is {name: [values]}
output.chart_pie(labels, values, title=None)
output.chart_doughnut(labels, values, title=None)
output.set_title(text)                       # retitles the window
output.save(path)                            # everything so far, as one HTML file

link = output.element_link(handle, label)    # HTML for a link that selects an object
html = output.table_html(rows, headers)      # the HTML print_table emits
text = output.format_table(rows, headers)    # aligned plain text with a dashed rule

The last three return strings and print nothing. table_html and format_table are pure Python, so they can be unit tested anywhere.

Tables#

print_table takes rows first, headers second. The headers set the column count: short rows are padded with empty cells and anything past the last header is dropped. Every cell is converted with str() and HTML-escaped.

python
rows = [['A-WALL', 1204, '38.5'], ['A-DOOR', 96, '0'], ['A-GLAZ', '', '']]
output.print_table(rows, ['Layer', 'Objects', 'Length (m)'])

A column is right-aligned in tabular figures when every non-empty cell in it reads as a number once commas are removed, and at least one cell is non-empty. Alignment follows the data, not the column's position.

One stray cell changes the whole column

An 'n/a' or '-' in a numeric column makes the whole column text, and it jumps back to the left. Use '' for a missing number: empty cells do not count against the column.

Markdown#

print_md understands a deliberately small subset: #, ## and ### headings, **bold**, *italic*, `code`, fenced ``` blocks, - bullet lists, [text](url) links and paragraphs. No tables (use print_table), no numbered or nested lists, no images. The input is HTML-escaped before it is converted, so a layer name or attribute value containing < or & prints as itself.

element_link(item, label) returns the HTML for a link; embed it in whatever you print. Clicking it makes that object the pickfirst selection in the active drawing, as if the user had picked it, so its grips show. The item can be a handle string (as every pyacad function returns them), an ObjectId, a Handle or an integer.

python
from pyacad import output, sel

handles = sel.get(type='TEXT', layer='0')
output.print_md('## %d texts on layer 0' % len(handles))
output.print_html(' '.join(output.element_link(h, h) for h in handles[:20]))
  • Handles belong to a drawing. The click resolves the handle in whichever drawing is active when it is clicked. Switch drawings and the same link selects whatever object has that handle there, or nothing.
  • Not inside print_table. Table cells are escaped, so a link in a cell prints as its own source. Put links in a print_html line.
  • A handle that no longer exists, or an erased object, does nothing.

Progress in the window#

output.progress(fraction, label='') shows a bar pinned to the bottom of the window. 1.0 completes it, and it fades away a moment later. Unlike forms.progress it has no Cancel button and does not disable AutoCAD: use it for read-only work that reports as it goes. Calling it also lets the window repaint mid-run (at most five times a second), which plain print() calls do not.

python
total = len(handles)
for index, handle in enumerate(handles):
    if index % 100 == 0:
        output.progress(float(index) / total, 'Checking %d of %d' % (index, total))
    check(handle)
output.progress(1.0, 'Done')

Put the count in the label. Checking 4,200 of 11,038 tells the user whether to wait; a bare percentage does not.

Charts#

Four shapes are built in, drawn as SVG that takes its colours from the window's theme. chart_bar, chart_pie and chart_doughnut take parallel labels and values lists; chart_line takes one labels axis and a {name: [values]} dict of series.

python
counts = blocks.counts()
names = sorted(counts, key=counts.get, reverse=True)[:8]
output.chart_bar(names, [counts[n] for n in names], title='Most used blocks')
  • A bar chart turns horizontal past eight categories, where column labels would overlap.
  • Mismatched lists are cut to the shorter one, and a value that is not a number (including None) is drawn as 0. A chart never raises half way through a report.
  • Line series are coloured in name order; a series longer than the labels is cut, and does not stretch the scale.
  • Pie and doughnut slices are labelled with their share; zero and negative values get no slice. All-zero data draws a "no data" card.

A chart earns its place when the shape of the data is the message. When the numbers are the message, a table reads better and copies out.

Images, code and keeping the report#

print_image(path, caption=None) embeds a PNG, JPEG, GIF or SVG file and raises ValueError for any other extension. print_code(text) is an escaped, horizontally scrolling monospace block, the right home for a traceback or a generated LISP expression.

set_title(text) renames the window; called before the first write, the title is kept until the window opens. save(path) writes everything this run has printed as one standalone HTML file, styled for the current theme, that opens in any browser. It works even when no window ever opened.

python
from pyacad import doc, forms, output, toast

output.set_title('Layer audit: ' + doc.path())
output.print_table(rows, ['Layer', 'Objects', 'Frozen'])
path = forms.save_file(filter='HTML files (*.html)|*.html', default_name='layer-audit.html',
                       title='Save the report')
if path:
    output.save(path)
    toast.success('Report saved', path)

Custom HTML#

print_html takes any fragment. To look like the rest of the window, use the classes and colour variables its stylesheet already defines. The variables are emitted once, for the current theme, so var(--fg) is right in both.

VariableIsVariableIs
--bgpage background --fgbody text
--mutedsecondary text --surfacetable header, code background
--linerow separators --frameframes around tables and code
--accentthe Windows accent: links, progress --row-hovertable row hover
--err, --err-wash, --err-line error text, background, rail --warn, --warn-wash, --warn-line warning text, background, rail
--chart-1 to --chart-6 chart series colours

Useful classes: table.pyacad (with td.num for numbers), pre.codeblock, .pyacad-code, .pyacad-warn and .pyacad-error (a tinted line with a coloured rail), and a.pyacad-el.

python
output.print_html(
    '<div style="border:1px solid var(--frame); border-radius:4px; padding:12px 14px">'
    '<div style="font-size:28px; font-weight:300">11,038</div>'
    '<div style="color:var(--muted)">entities scanned</div>'
    '</div>')
These are not the dialog token names

The desktop dialogs call the background Paper and the text Ink; the output page names them --bg and --fg. var(--paper) in a print_html fragment resolves to nothing.

Links in printed HTML: http, https and mailto links open in the user's default browser. Anything else (file:, a relative path) is not opened outside the window, so link to files with pyacad.script.show_in_explorer from a button instead.

Without WebView2#

The rich window is a WebView2 browser. On a machine without the WebView2 runtime, pyACAD opens a plain text window instead, with Copy, Clear, Save (as .txt) and Find (Ctrl+F, F3 for the next match). Your script does not change: every call still works, and degrades like this:

  • Text and tracebacks print as they are, tracebacks in the error colour.
  • HTML from print_html, print_md and print_table shows as its source; an image shows as [image] and a chart as [chart].
  • output.progress prints a line such as [ 50%] Checking 500 of 1000.
  • An element link is just its label.

If WebView2 is installed but cannot start (a locked-down profile, say), the window switches to a read-only text box in place and carries on the same way. Its browser profile lives in %APPDATA%\pyACAD\webview2.

Logger lines in the window#

pyacad.script.get_logger writes to the pyACAD log. Its warnings and errors also appear in the output window as tinted lines, but only when a window is already open: logging a warning never opens one by itself. See Logs.

Your own windows#

When chained prompts stop being enough, at about three fields, load a XAML file shipped in the bundle folder:

Rename.pushbutton\script.py
from pyacad import forms

win = forms.WPFWindow('rename.xaml')          # a path relative to this bundle's folder
win['NewName'].Text = 'A-NOTES'
win['Ok'].Click += lambda sender, args: win.close(True)
win['Cancel'].Click += lambda sender, args: win.close(False)
if win.show_dialog():
    new_name = win['NewName'].Text
MemberDoes
forms.WPFWindow(xaml_file) Loads the XAML. A relative name is looked up in the running bundle's folder; a missing file raises IOError. A root that is not a Window is wrapped in one sized to its content.
win.find(name)The element with that x:Name, or None.
win[name]The same, raising KeyError when it does not exist.
win.show_dialog() Shows it modally, owned by AutoCAD and with the themed title bar, and returns its DialogResult (True, False or None).
win.close(result=None) Closes it, setting DialogResult first when given.
win.windowThe WPF Window itself.

The window's chrome is themed for you; its content is your XAML, so give it colours that work in both themes (see Theming). For a tool the user keeps returning to, a *.dockpane puts XAML in a real AutoCAD palette that stays open between runs, switched from the ribbon; see Buttons, stacks and pulldowns.

Handlers run after the script has returned

A modal dialog's handlers run while show_dialog() waits, so they are still inside your run. A modeless window's or a dock pane's handlers run later, outside any command: the drawing is not locked (use doc.lock() to write), and pyacad.script's per-tool calls answer for whichever tool ran last, so pin your identity first with pyacad.script.bind. See Settings and per-tool state.

Theming#

Every built-in surface follows one answer to "is AutoCAD dark?": the theme key in config.json when set, otherwise AutoCAD's COLORTHEME system variable (0 is dark). The full rule is in Icons, light or dark. The answer is read once and kept until the next Reload, so after switching AutoCAD's theme, press Reload.

  • Dialogs and pickers draw on a solid paper background with the real Windows title bar, dark in the dark theme, and rounded corners on Windows 11.
  • The Windows accent colour (Settings, Personalisation, Colours) appears in a few places only: the primary button, the selection rail, focus rings, the progress bar and output links. It is read once and refreshed on Reload.
  • Toasts and banners use fixed success, error, info and warning colours per theme.
  • The output window uses its own light and dark palettes, both checked for WCAG AA contrast, tracebacks included.

For your own XAML, ask the same question:

python
from pyacad import script

if script.is_dark_theme():
    background, foreground = '#202020', '#E8E8E8'
else:
    background, foreground = '#FFFFFF', '#1A1A1A'

Ask at the moment you build the window, not at import time. The C# toolkit every pyACAD dialog is built from is public too, so an IronPython script can borrow its colours: DesignSystem.Tokens.Current (from PyAcad.Runtime.Forms) holds the current theme's Paper, Surface, Ink, Muted, Line and status colours. The style guide covers what a tool's window should look like.

Under pyacad run#

When a script runs through pyacad run there is no screen and no one to answer. __runner__ is True (and pyacad.script.is_runner returns it). The surfaces behave like this:

  • toast.*, banner.* and forms.alert write one line to the run's transcript, such as [success] Plotted 12 layouts: D:\Issue, and return.
  • forms.confirm, ask_string, ask_number, ask_options, select_from_list, pick_folder, open_file, save_file and WPFWindow.show_dialog raise Dialogs are not available under pyacad run. Check __runner__ and skip the prompt.
  • print() and pyacad.output go to the transcript.
python
from pyacad import forms, script

folder = r'D:\Issue' if script.is_runner() else forms.pick_folder('Folder for the PDFs')

One tool, every surface#

A realistic tool uses each surface in its place: a dialog for the one thing it cannot guess, progress while it works, a table and a chart for the detail, and a toast for the verdict.

Layer_Usage.pushbutton\script.py
"""Counts model space objects per layer and lists the layers nothing is on."""
from pyacad import ents, forms, layers, output, toast

handles = list(ents.iter())
if not handles:
    toast.info('Model space is empty', 'Draw something, then run this again.')
    raise SystemExit

per_layer = {}
try:
    with forms.progress('Layer usage', 'Reading objects') as bar:
        for index, handle in enumerate(handles):
            if index % 200 == 0:
                bar.update(float(index) / len(handles),
                           'Reading %d of %d' % (index, len(handles)))
                bar.check()
            per_layer.setdefault(ents.get(handle)['layer'], []).append(handle)
except forms.Cancelled:
    raise SystemExit                              # the user stopped it: say nothing

empty = [name for name in layers.names() if name not in per_layer]
busiest = sorted(per_layer, key=lambda name: len(per_layer[name]), reverse=True)

output.print_md('## %d objects on %d layers' % (len(handles), len(per_layer)))
output.print_table([[name, len(per_layer[name])] for name in busiest], ['Layer', 'Objects'])
output.chart_bar(busiest[:8], [len(per_layer[name]) for name in busiest[:8]],
                 title='Busiest layers')
output.print_html('First object on %s: %s' % (busiest[0],
                  output.element_link(per_layer[busiest[0]][0], 'select it')))
if empty:
    output.print_md('### Nothing on\n' + '\n'.join('- ' + name for name in empty))

if empty:
    toast.warning('%d layers have nothing on them' % len(empty), 'The list is in the report.')
else:
    toast.success('Every layer is in use', '%d objects counted' % len(handles))

Notice what is not there: no print(), no toast on cancel, no window opened before there was something to put in it. And the toast comes last, so the user has the verdict even if they never read the window.