Metadata-Version: 2.2
Name: window-grabber
Version: 0.2.2
Summary: Deadline-bounded window capture with explicit evidence and isolated native workers
Author: window-grabber contributors
Classifier: Development Status :: 5 - Production/Stable
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: pydantic<2,>=1.10.17; python_version < "3.9"
Requires-Dist: pydantic<3,>=1.10.17; python_version >= "3.9"
Provides-Extra: test
Requires-Dist: pytest<9,>=7.4; extra == "test"
Requires-Dist: pytest-cov<6,>=4; extra == "test"

# window-grabber

## Monitor processes

```python
from window_capture import Match, WindowMonitor, Target, recommended_capture_mode

with WindowMonitor(
    [Target(process_name="example.exe"),
     Target(process_name=Match.glob("agent*.exe", ignore_case=True))],
    mode=recommended_capture_mode(),
) as monitor:
    events = monitor.poll()
```

`recommended_capture_mode()` selects an initial policy without starting workers:

| Executing host | Recommended mode |
| --- | --- |
| Windows 7/8/8.1 and Windows 10 before build 19041 | `controlled_desktop` |
| Windows 10 build 19041 (2004) or later, including Windows 11 | `isolated` |
| Linux | `isolated` |

Other platforms raise `RuntimeError`. This is an OS recommendation, not a
capability probe or a guarantee that a particular window can be captured.
The Windows boundary includes the cursor-exclusion API required by default capture options.
Capture failures never change the selected mode. Omitting `mode` keeps the existing
default policy; using this helper explicitly permits desktop changes on older Windows.

For RPyC, evaluate the helper and construct objects through the remote module:

```python
wc = conn.modules["window_capture"]
monitor = wc.WindowMonitor(
    wc.Target(process_name="example.exe"),
    mode=wc.recommended_capture_mode(),
)
try:
    events = monitor.poll()
finally:
    monitor.close()
```

Install the package on the remote machine and run the capture process in the target
user's interactive Windows session. The helper evaluates that remote host; complete
monitor-over-RPyC integration has not yet been qualified.

Use `mode="isolated"` for source-isolated capture without desktop changes.
Controlled desktop mode manages temporary visibility, topmost placement and Windows
popup separation, then restores the windows. It disables explicit foreground
activation to avoid cross-process input-state changes. Add `allow_resize=True` only if
layout-changing resizing is acceptable. Each popup has its own screenshot.

Target fields combine with AND, target lists with OR; matching windows are
captured once. Names, paths, classes and `window_title` accept literal strings,
`Match.regex(...)`, or `Match.glob(...)`. Patterns match the whole value; glob
supports `*` and `?`. The original flags remain available for advanced callers.
See [revision nine](docs/window_capture_design_revision_nine.md) for matching,
mode conflicts, shared discovery deadlines and platform limits.


A typed Python API backed by precompiled, isolated Rust helpers. The coordinator
owns desktop admission and recovery records. Native acquisition and potentially
blocking window operations stay outside the caller's Python process.

```python
from window_capture import CaptureSession, Target

with CaptureSession(mode="controlled_desktop") as session:
    result = session.capture(Target(process_name="viewer.exe"))
    if result.accepted:
        result.require_image().save("viewer.png")
    else:
        print(result.reason, result.message, result.recovery)
    for event in session.events.poll():
        print(event.kind, event.reason)
```

All supplied selectors use AND semantics; ambiguity fails. The default total
budget is 2,000 ms, including startup, queueing, fallback, encoding and restoration.
Desktop changes require controlled mode or explicit advanced desktop permission. PNG encoding is timed;
explicitly saving the returned image is separate file I/O. RGBA output contains
immutable straight-alpha bytes and must be encoded explicitly before saving.

`image.save(destination)` accepts a path, `BytesIO`, or an open binary file:

```python
from io import BytesIO

buffer = BytesIO()
event.require_image().save(buffer)
png_bytes = buffer.getvalue()

with open("capture.png", "wb") as output:
    event.require_image().save(output)
```

Saving writes the existing PNG at the stream's current position. Caller-owned
streams are not closed, flushed or rewound; call `buffer.seek(0)` before reading
with `read()`. RGBA images still require explicit encoding. Write errors propagate
and can leave partial output.

With RPyC, a path is opened on the host executing `save()`. A stream proxy writes
to the host owning that stream. For example, if `remote_event` is a remote event:

```python
buffer = BytesIO()  # Lives on this client.
remote_event.require_image().save(buffer)
png_bytes = buffer.getvalue()  # Screenshot transferred to this client.
```

The connection must permit the remote `save` call and callback access to the
stream's `write` method. A restricted service can expose a dedicated writer object
instead. Saving uses bounded byte chunks and supports short writes; it does not
change connection permissions or initiate another capture. RPyC remains optional.

## Process monitoring

Each visible top-level window has its own capture and change history. Owned popups
are discovered and captured separately at their full size; their pixels are never
composited onto the owner's screenshot. Child controls remain part of their window.

```python
from pathlib import Path
import time
from window_capture import ChangeOptions, WindowMonitor, Target

output = Path("captures")
output.mkdir(exist_ok=True)
with WindowMonitor(
    Target(process_name="viewer.exe"),
    changes=ChangeOptions(channel_tolerance=3, min_changed_pixels=1),
) as monitor:
    while True:
        started = time.monotonic()
        for event in monitor.poll():
            if event.image is not None:
                handle = event.window.hwnd or event.window.x11_window_id
                event.image.save(output / f"{handle}-{event.result.request_id}.png")
            elif event.result is not None:
                print(event.kind, event.result.reason)
        time.sleep(max(0, 5 - (time.monotonic() - started)))
```

`poll()` returns `opened`, `changed`, `geometry_changed`, `disappeared`,
`capture_failed`, and `capture_recovered` events. Repeated failures with the same
reason are suppressed, while capture retries continue every poll. A changed
reason emits another failure. Recovery emits metadata even when pixels are
unchanged; `event.image` is None for recovery events. Changed images are delivered
separately. See [revision ten](docs/window_capture_design_revision_ten.md).
An absent process produces no images; a newly visible popup gets its own `opened`
event. Disappearance includes hiding and no longer matching the selector. Failed
captures expose no image and preserve the last good baseline. Failed discovery
raises `CaptureError` and preserves the previous window set.

Comparison uses decoded RGBA pixels, independent of PNG compression. A pixel
counts as changed when any channel differs by **more than 3** on the 0–255 scale.
The default reports even one such pixel to preserve small text and indicators.
Increase `min_changed_pixels` to suppress isolated speckles, accepting that equally
small real changes will also be ignored. Comparisons use the last **reported**
image, so cumulative drift is detected. Use `poll(sink=...)` to commit comparison history only after successful batch saving. It does not use a whole-image percentage rule.

Default monitoring enforces `source_isolated` acquisition and returns per-window
capture failures instead of raising them. Controlled desktop mode explicitly permits
observed-clear desktop captures. Each
window receives its own capture deadline; a full poll also includes discovery and
comparison. The caller schedules polls and file writes. Defaults limit discovery
to 64 windows and retained RGBA baselines to 128 MiB; transient captures, decoding
buffers and caller-held images require additional memory. Wayland portals do not
provide this process-enumeration API.

WGC can refuse a popup that Windows does not expose as a capturable item, including
the non-Alt+Tab popup in our Server 2022 fixture. The default monitor then tries
PrintWindow for that popup. A GPU-rendered popup that neither route can capture
remains an explicit failure; the engine cannot promise coverage of every renderer.

## Capture routes

| Platform | Implemented routes | Capability boundaries |
| --- | --- | --- |
| Modern Windows | Windows Graphics Capture, PrintWindow, DXGI Desktop Duplication, GDI desktop | WGC isolates a window surface and supports cursor/alpha options. DXGI and GDI require permitted desktop control and report observed visibility. |
| Windows 7 SP1 build target | PrintWindow and GDI desktop, separate x86/x64 helpers | Modern WinRT/DXGI capture imports are excluded. Physical Windows 7 runtime and GPU qualification remain release gates. |
| Linux X11 | Composite named pixmap and controlled root-desktop capture | Local X11, opaque SDR TrueColor; client and child content. Reparented window-manager decorations are refused for whole-window capture. |
| Linux Wayland | Explicit portal window selection and a persistent PipeWire stream | Requires the packaged portal helper, system PipeWire and a ScreenCast portal. Complete selected window, hidden cursor, opaque BGRx; permission is prepared separately. |

Windows supports HWND/PID, executable name/path, class and title selectors. X11 supports
XID/PID, executable name/path, class and title selectors where local process identity can
be established. Wayland uses a source selected by the user:

```python
from window_capture import CaptureSession, Target

with CaptureSession() as session:
    source = session.select_window()  # interactive; separate from capture's deadline
    result = session.capture(Target(portal_source=source))
```

Prepared sources belong to that session and native process. Closure, worker death,
idle expiry or stream loss invalidates a source; capture never silently reconnects
or opens another selection dialog. `select_window(timeout_ms=...)` can bound the
interaction. `prepare()` probes capabilities and warms the coordinator process;
individual Windows acquisition processes still create fresh devices/frame pools.

## Evidence and safety

`CAPTURED` means the candidate met the requested policy and required restoration
completed. `source_isolated` describes acquisition scope; it is not independent
pixel verification. Desktop routes report at most `no_overlap_observed`, because
window events and geometry checks cannot atomically lock out unrelated programs.
Suspicious uniform regions produce `INDETERMINATE`, including legitimate content
that a generic capture engine cannot explain. Rejected pixels are never exposed
as an accepted image.

The implementation separates configuration, IPC, process ownership, selection,
mutation, pixel acquisition, validation and encoding. Resources have scoped owners;
OS-specific unsafe code stays in native boundary modules. Pull-based diagnostic
events have bounded capacity and overflow counts, with no user callbacks in the
request path and no screenshots in diagnostic events or object representations.
Process-monitor events deliberately expose accepted new/changed images to callers.

Capture admission currently serializes native acquisitions. Windows scopes it to
the session/window station/desktop. Linux conservatively shares one local-host
scope across X11, XWayland and Wayland, so environment aliases cannot create
competing leases. Cross-account access fails unless a shared service is supplied;
remote X11 capture is not coordinated by this package.

Unconfirmed mutation or restoration leaves a durable journal and quarantines new
captures. An abandoned OS lock or elapsed timeout does not clear it. See
[recovery and limitations](docs/window_capture_design_revision_four.md) before
operating a quarantined desktop.

## Wheels and development

End-user installation needs no Rust, Cargo, C/C++ compiler or first-use downloads:

```console
python -m pip install --no-index --find-links=dist --only-binary=:all: window-grabber
window-grabber --probe
```

Each Windows wheel includes architecture-matched modern and legacy executables;
the Python facade selects the legacy binary on pre-Windows-10 systems. Linux
wheels include separate X11 and optional portal executables, so absent PipeWire
libraries do not prevent the X11 helper from starting. The current Linux CI build
baseline is Ubuntu 24.04 x86_64; these are Linux platform wheels, not a claim of
compatibility with every glibc baseline. PipeWire's shared library is an explicit
system runtime dependency for the portal helper.

Maintainer builds use the pinned toolchains and Cargo.lock. The complete combined
wheel commands live in [.github/workflows/tests.yml](.github/workflows/tests.yml).
For local logic checks:

```console
cargo test --locked --workspace
cargo fmt --all --check
cargo clippy --locked --workspace --all-targets -- -D warnings
python -m ruff check src tests scripts setup.py
python -m ruff format --check src tests scripts setup.py
PYTHONPATH=src python -m pytest tests -m "not windows and not linux"
```

CI tests installed wheels outside the source tree, with offline installation and
Windows build tools removed from PATH. The desktop tests compare every pixel of
independent GDI/child/OpenGL fixtures, test fallback/timeouts and concurrency, and
verify restoration. Xvfb exercises installed X11 routes. Portal unit tests and
installed-helper startup checks do not establish compositor interoperability.

## Production qualification

Version **0.1.0** is the first production release of the documented capture routes.
Unsupported operations continue to return explicit outcomes. Hosted CI does not establish physical Windows 7 GPU support,
HDR/alpha accuracy, real compositor behavior, antivirus compatibility, or the
proposed 99.5% success-within-two-seconds gate. Group movement of visible popups,
Vulkan hooks and generic application adapters remain unavailable.
No automatic privilege escalation or endpoint-policy changes are performed.
Releases publish precompiled Windows wheels to PyPI; source distributions are disabled.

## Opt-in renderer hooks

The experimental `renderer_hook` backend attaches to already-running, matching
x86/x64 processes. It supports OpenGL 1.x/2.x compatibility contexts with a static
executable `SwapBuffers` import, Direct3D 9 primary `Present`, and Direct3D 11 DXGI
`Present`/`Present1`, plus experimental Direct3D 12 readback using a queue observed
inside DXGI presentation. Each captures one client surface; select popups separately by
HWND. Direct3D formats and state are deliberately restricted, including no MSAA.
Direct3D 11 requires existing multithread protection and rejects partial updates.
Direct3D 12 requires D3D11On12, IDXGISwapChain3 and an unambiguous observed queue;
it does not guess from unrelated submissions. Vulkan readback remains unsupported.

```python
result = capture(
    Target(hwnd=hwnd),
    options=CaptureOptions(
        allow_injection=True,
        backend_order=("renderer_hook", "print_window_full", "print_window"),
        content=ContentOptions(area="client", include_children=False),
    ),
)
```

The precompiled DLL is packaged with the Windows wheel. The hook is never enabled
by a preset. It remains loaded and its presentation interception remains installed until
the target exits, but readback requires an active request. Readback executes on the
target's render thread and can stall it; the caller deadline cannot preempt that
driver call. Hook errors are recorded without assuming antivirus caused them.
An exclusion for Python does not establish that the DLL is permitted.

See [revision sixteen](docs/window_capture_design_revision_sixteen.md) for the
compatibility limits, lifetime rules and tests. Hardware GPU and Windows 7 runtime
qualification remain outstanding.

Use [the qualification guide](docs/qualification.md) to measure required deployments.
The runner separates failures from accepted latency and requires an independent
reference to pass a scenario. Exact commit results are recorded in
[GitHub Actions](https://github.com/Heknon/window-grabber/actions).

[Revision five](docs/window_capture_design_revision_five.md) defines separate-window
capture and process monitoring, superseding earlier popup-composition behavior.
[Revision four](docs/window_capture_design_revision_four.md) records the underlying
implementation; [revision two](docs/window_capture_design_revision_two.md) retains
the remaining intended contract and production gates. Earlier proposals remain history.

### Scheduled process recording

```console
python -m window_capture --monitor --process-name example.exe --interval-seconds 5 --output captures
```

Each popup is saved independently. Complete `batch-*` directories contain PNGs
and a manifest; incomplete `pending-*` directories are not recordings. A failed
save does not advance comparison baselines. The interval is a wait after each
finished poll. Restarting records fresh baselines, so duplicate images are
possible. The default 1 GiB limit covers new output per recorder instance; arrange
archive retention separately. See [revision six](docs/window_capture_design_revision_six.md)
for API usage, code-review scope, GPU evidence and remaining production gates.


### Crowded desktops and popup foreground capture

Enable `DesktopOptions(enabled=True, separate_owned_popups=True, resize_to_fit=True)`
to permit temporary parent resizing when popups cannot be moved aside, and to
raise an owned popup independently for its own screenshot. The complete owner
group is journaled and restored. Resizing can change layout and hide content.
The monitor reports different image dimensions as `geometry_changed`, with a
fresh baseline, rather than treating them as pixel content changes.

CLI: `--desktop --separate-owned-popups --resize-to-fit`.
See [design revision eight](docs/window_capture_design_revision_eight.md).

## External VM screenshots after a capture failure

A Windows monitor event offers an explicit desktop transaction:

```python
if event.result is not None and not event.result.accepted:
    with event.bring_to_top(max_hold_ms=10000) as presented:
        # Your own vCenter client call; obtain a fresh VM screenshot here.
        screenshot = take_vm_screenshot()
    # Only publish after exit confirms restoration and observation checks.
    save_vm_screenshot(screenshot, guest_bounds=presented.bounds)
```

`take_vm_screenshot` and `save_vm_screenshot` are caller-provided functions.
The context manager uses the event's complete window identity, separates owned
popups, temporarily makes the target topmost, and restores journaled state. It
holds the same desktop lock as normal captures. `allow_resize=True` explicitly
permits layout changes when needed. A custom `coordinator` must match the monitor's
configuration. This operation explicitly permits desktop changes even when the
monitor uses isolated mode.

The hold limit is 100–60000 ms; setup and restoration have separate bounded
budgets. Context exit, caller-pipe closure, and native hold expiry initiate
restoration. A crashed/hung native transaction can leave a recovery journal and
quarantine; restoration errors are raised, never silently treated as success.
Expiry or observed window changes also raise, even if restoration completed.

`presented.bounds` uses guest desktop pixels. Map these to the vCenter image using
its resolution/display arrangement; do not assume a multi-monitor VM screenshot
shares that origin. Success reports observation and restoration, not external
image freshness, completeness, or atomic freedom from overlap. The external image
never changes the monitor baseline or automatically marks capture as recovered.

With RPyC, call `bring_to_top()` on the remote event and keep the context open while
the local caller requests the vCenter screenshot. The native hold timer remains
active if that connection is lost. Full vCenter/RPyC integration is not qualified.

## Capture fallback and confidence

The default Windows order is WGC, `print_window_full`, `print_window`, desktop
DXGI, then desktop GDI. The default attempt limit is five; caller limits and the
single shared deadline still apply. Desktop backends are eligible only when the
selected policy permits them. Full-content PrintWindow uses PW_RENDERFULLCONTENT
on Windows 8.1 or newer; ordinary PrintWindow remains a separate fallback.
Neither method is a universal GPU-content guarantee.

WGC uses cursor control when the interface exists. Older WGC can retain its default
cursor inclusion when requested, but cannot satisfy cursor exclusion; that produces
`CURSOR_CONTROL_UNAVAILABLE`. The recommended mode therefore uses Windows 10 2004
as its boundary for default cursor-free capture.

Suspicious uniform regions trigger fallback. If later attempts also fail, the
suspicious result is preserved as `CONTENT_UNCERTAIN`, with the attempt history and
an optional diagnostic candidate image. The cause remains unknown; matching black
outputs do not prove that the application intended black content. Restoration
failures remain authoritative. `content_evidence` describes checks and suspicious
regions; `completeness="not_established"` explicitly distinguishes these checks
from proof that all application content was rendered. `pixel_assurance="checked"`
means the configured checks ran, not reference-image verification.

Monitor baselines retain their backend and occlusion evidence. A change in capture
source emits `capture_source_changed` with an image and starts a new baseline,
even if pixels are identical. Geometry changes retain their existing event type.
An uncertain result never replaces either baseline pixels or baseline source.

## Release automation

Merges to `main` run final tests, create a version tag and GitHub release, then
publish the tested Windows wheels to PyPI. See [the release guide](docs/releasing.md)
for version bumps, Trusted Publisher setup, Linux distribution, and failed-job retries.

### Inspecting windows

Use the existing `find_windows()` API with or without a `Target`:

```python
from window_capture import DiscoveryOptions, find_windows, Target

windows = find_windows(best_effort=True)  # visible top-level windows
app_windows = find_windows(
    Target(pid=1234),
    discovery=DiscoveryOptions(include_children=True, include_hidden=True),
)
for window in app_windows:
    print(window.hwnd, window.kind, window.process_path)
```

On Windows, each fully inspected record contains the existing stable identity
(HWND, PID, TID, process creation time, class, root and owner), plus `title`,
`process_name`, `process_path`, `parent_hwnd`, `bounds`, `visible`, `minimized`,
`maximized`, `foreground`, `cloaked`, `is_child`, `is_popup`, and `kind`.
`kind` is a structural classification: child, dialog (class `#32770`), owned
popup, or top-level. It does not identify an application's semantic UI role.
`foreground` means the foreground HWND; visibility does not establish that a
window is unobscured or capturable. Enumeration covers the caller's desktop,
not other sessions, secure desktops, or message-only windows.

`DiscoveryOptions.include_children` also searches descendants of enumerated top-level windows.
For an explicit HWND, inspection remains limited to that HWND. An explicit HWND
can be inspected even when hidden. `include_hidden`, disabling `include_minimized`, and `best_effort` currently
require Windows; Linux X11 retains its existing identity records and strict
selection. Wayland does not provide general desktop window enumeration.

Optional fields may be null with a reason in `inspection_errors`; for example,
cloaking information is unavailable on Windows 7. Strict discovery is the default.
With `best_effort=True`, inaccessible or disappearing candidates can instead
produce a partial record containing `hwnd`, `inspection_error`, and
`matches_filter=None`. Such records are **not confirmed matches**: missing
identity or process data must never be treated as proof of unique selection.
Deadlines and enumeration/response limits still fail the call, rather than
silently returning a truncated inventory. All returned records are immutable.

Window state is sampled, not an atomic desktop snapshot. A window can disappear
immediately after discovery; capture independently resolves and revalidates its
target. Discovery does not activate, move, resize, or capture windows.

### Window context in capture events

On Windows, `CaptureResult.window` contains the same description sampled after
unique target resolution, before any capture-related desktop changes. It is
also available on `CaptureEvent.window`, including capture failures after
resolution. If inspection detects a changed identity, optional sampled fields
are discarded and `inspection_errors.identity` reports `TARGET_CHANGED`.
This context does not replace `identity_evidence` or prove capture succeeded.

`result.requested_target` and `event.requested_target` retain the requested
selectors even when no window could be resolved or the worker failed. In that
case `window` is `None`. Portal tokens are excluded. Discovery, capture and monitor events
include titles and full process paths whenever readable. Window details and requested selectors are omitted
from object representations and can be marked `{"truncated": True}` to preserve
the metadata budget. Events retain metadata only, without images.

### Poll every window, capture selected windows

`WindowMonitor` separates discovery from capture through `capture_when`.
Omit the target to poll all visible top-level windows, or supply the usual
process selectors to narrow discovery:

```python
from window_capture import DiscoveryOptions, WindowMonitor

with WindowMonitor(
    discovery=DiscoveryOptions(include_hidden=True, include_children=True),
    max_windows=512,
    capture_when=lambda window: (window.process_name or "").lower() == "myapp.exe",
) as monitor:
    for event in monitor.poll():  # schedule subsequent polls in your application
        print(event.kind, event.window.hwnd, event.window.pid)
        if event.image is not None:
            event.image.save("capture-{}.png".format(event.window.hwnd))
```

The predicate receives an immutable window description and must return `bool`.
It runs once per discovered window per poll, on the caller's thread. All decisions
are evaluated before any capture starts; an exception or invalid return leaves
history unchanged and starts no captures. Predicate execution is outside native
capture deadlines. Title/path-based predicates receive readable metadata automatically.

A `False` decision performs no pixel acquisition for that window. It still emits
`opened`, `metadata_changed`, and `disappeared` events with `result=None` and
`image=None`. Unchanged windows produce no repeated event. Switching from capture
to observation emits `capture_skipped` and releases that window's pixel baseline;
selecting it again starts a fresh baseline. Predicate decisions and observation
history commit only after the sink succeeds, matching the existing retry contract.

`max_windows` bounds all discovered windows, including those excluded from capture.
Windows monitoring tolerates per-window inspection failures. Unconfirmed candidates
emit `inspection_failed` with `window.inspection_error`; they are not passed to the
capture predicate and are not captured. Previously observed identities and image
baselines remain intact while the same handle cannot be inspected. Successful
inspection emits `inspection_recovered`. Repeated identical inspection failures
are suppressed. An absent handle in a completed enumeration can then disappear.
Global discovery failures, deadlines and resource limits still fail the poll without
committing partial history. Direct `find_windows()` remains strict unless passed
`best_effort=True`. Linux discovery remains strict.

For an explicit fresh screenshot from an observation-only event:

```python
result = event.capture(mode=recommended_capture_mode())
if result.accepted:
    result.require_image().save("debug.png")
    # Optional: requires Pillow on the machine executing this call.
    result.require_image().to_pillow().show()
else:
    print(result.reason, result.message)
```

`event.capture()` creates a separate capture session and leaves the event and monitor
history unchanged. It validates the sampled identity natively before acquisition
or desktop changes; stale identities fail with `TARGET_CHANGED`. Unconfirmed
inspection events fail with `TARGET_UNVERIFIED` until a fresh verified event exists.
Use `CaptureSession.capture_window(event.window)` to reuse your own session.
`require_image()` continues to return only existing pixels. `to_pillow()` returns
an independent loaded image; `.show()` explicitly opens a viewer on its executing
host, including when invoked through RPyC.

Minimized windows and owned popups are already included in Windows top-level
inventory when their visible flag is set. `include_minimized=True` is the default;
set it to `False` to exclude minimized windows. `include_hidden=True` includes
windows whose visible flag is unset, and `include_children=True` includes nested
controls/rendering windows. These same `DiscoveryOptions` apply to `find_windows()`
and `WindowMonitor`. Observing a minimized or hidden window does not establish
that a requested capture backend can acquire its pixels.

`WindowMonitor` accepts any native `Target` selector, a sequence of selectors, or
no selector. `ProcessMonitor` remains an alias for existing callers; new code can
use the clearer `WindowMonitor` name. Capture options still control how pixels are
acquired; discovery options only control which windows are observed.

### Typed windows, events, and images

`find_windows()` returns immutable Pydantic `Window` models. `WindowEvent`,
`CaptureEvent`, `CaptureResult`, `CapturedImage`, `CapabilityReport`, and
`CloseReport` are Pydantic models too. Use attributes such as `window.pid`,
`window.process_path`, and `window.bounds.left`; unknown or unreadable
window fields are `None`. Constructors use named arguments. `.schema()` exposes
types and field descriptions; `.dict()` and `.json()` serialize models. Image
bytes are excluded from model serialization and object representations.
`Window.metadata()` serializes supplied fields only, preserving omitted fields.

For example, choose specific executables and save only available screenshots:

```python
from window_capture import WindowMonitor

paths = {r"C:\Apps\First.exe", r"C:\Apps\Second.exe"}
with WindowMonitor(
    capture_when=lambda window: window.process_path in paths,
) as monitor:
    for event in monitor.poll():
        if event.image is not None:
            event.image.save("capture-{}.png".format(event.window.hwnd))
        else:
            print(event.kind, event.window.pid)
```

Membership above compares exact strings; normalize spelling/case explicitly if
that is your desired policy. `Target(process_path=...)` remains the native
file-identity selector when aliases/junctions must resolve to the same file.

`event.require_image().save("capture.png")` is the explicit alternative when an
image is required. It returns the existing screenshot and never starts another
capture. It raises `CaptureNotRequested` when `event.result is None`, `CaptureError`
(with `.result`) when capture failed, or `ImageUnavailable` for a metadata-only
recovery notification. Merely polling an excluded window raises nothing;
`event.image` is `None`. The metadata-only `CaptureEvents` buffer does not retain
images; use `WindowMonitor` image events or the returned `CaptureResult`.

Models use Pydantic's v1-compatible API (also provided by Pydantic 2). Python 3.8
installations require Pydantic 1.x, avoiding the newer pydantic-core binary's
[Windows 7 import incompatibility](https://github.com/pydantic/pydantic-core/issues/1418).
Options such as `Target`, `DiscoveryOptions`, and `CaptureOptions` remain immutable
dataclasses. Model updates are revalidated by the library; Pydantic's ordinary
`copy(update=...)` is not a validation boundary.

### HDR desktops and approximate colors

The default `ImageOptions(hdr_policy="allow")` permits GDI, PrintWindow, and
DXGI capture when HDR is enabled. Colors may be clipped or shifted; legacy
backend results describe this in `result.diagnostics["color_evidence"]`.
WGC continues to tone map its floating-point HDR frames.

Use `hdr_policy="tone_map"` to require explicit HDR tone mapping, or `"reject"`
to reject active HDR. Color checks apply to displays overlapping the capture
rectangle. Unknown legacy display color state is reported without blocking the
default policy. Buffer validation, unwritten-pixel detection, blank-region checks,
window identity checks, and desktop obstruction checks still apply. Blank checks
are heuristics: they cannot prove that every nonuniform screenshot is correct.

Window display-affinity restrictions reject compositor/desktop attempts, which
could otherwise return blank pixels or the background behind an excluded window.
Fallback continues to configured application-rendered backends such as PrintWindow.
Those attempts must pass the same pixel validation; the library never changes the
window's display-affinity flag. A fallback can still fail if the application does
not provide usable pixels. Inspect `result.attempts` for each backend's outcome.

Presentation preparation has its own budget:
`event.bring_to_top(prepare_timeout_ms=10000, max_hold_ms=10000)`. The first
limit covers queueing, helper startup, and window preparation; the second covers
the ready interval. If entry times out, release is requested and a bounded cleanup
wait retains the native failure and recovery details when available.

Hidden Windows input helpers (`IME` and `MSCTFIME UI`) may migrate between owners
on the same UI thread when a window is restored. Their transient ownership is
excluded from popup rollback comparisons; visible popups and ordinary hidden
application windows remain checked. Off-screen PrintWindow targets use an SDR DIB
and do not require an overlapping display for `tone_map` or `reject`. On-screen
color checks include every overlapping output, even a narrow overlap across monitors.

`bring_to_top()` acquires no pixels, so display-affinity exclusion does not block
its external observation interval. It still checks window identity and obstruction,
and leaves display affinity unchanged. This does not make an excluded window
available to GDI or other screen-capture APIs.

### Windows desktop recovery

Desktop preparation applies to explicitly enabled capture backends, including
PrintWindow and PrintWindowFull. It restores minimized windows and may move,
resize, promote, or activate them according to `DesktopOptions`, then restores
recorded placement. Application-owned child layout is not rollback state.

If restoration cannot be confirmed, failures retain the native cause and the
journal location in `result.diagnostics`. The durable transaction records the
last stage and expected state. Further mutations first attempt bounded recovery
when a previous restoration worker completed. Source-isolated capture can still
attempt acquisition without preparation while a transaction remains pending.

```python
with CaptureSession() as session:
    report = session.recover_desktop(action="inspect")
    print(report.dict())
    report = session.recover_desktop()  # Bounded, identity-checked recovery.
```

`RecoveryReport.state` is `"clear"` or `"pending"`. Interrupted workers and old
intent-only journals cannot be safely replayed: while their application process
is alive they remain pending. Close the affected application completely, then
retry recovery. Recovery can retire the journal after positively verifying that
every affected process lifetime has ended. For activation transactions this also
includes the saved active and foreground windows' processes. Access denial is not proof of process exit. There
is no force-delete operation, and inspection never changes window state.
Do not terminate unrelated applications blindly; the report identifies the
processes involved. An OS sign-out ends those desktop application lifetimes.

Windows 7 build/import audits and hosted Windows tests do not establish behavior
on every Chrome build, physical GPU, HDR configuration, or legacy OS desktop.
