Mirror one or two specific application windows onto a second display — a TV, a projector, or any extended monitor — without mirroring your whole desktop.
Unlike Win + P screen duplication, PaneCast captures individual
windows. Your email, notifications and browser tabs stay private on your
laptop while only the window(s) you chose appear on the big screen.
Platform support: Windows is fully supported and tested. macOS and Linux backends exist but are experimental and unverified on real hardware - see Platform support.
- Per-window capture — project a specific app, not the entire desktop.
- Dual-window projection — show two applications at once.
- Three layout modes — side-by-side, top/bottom, or picture-in-picture.
- Isolated capture — captured windows keep rendering correctly even when covered by other windows or moved off-screen.
- Child-canvas detection — automatically finds the real rendering surface inside a host window (e.g. a Java/AWT or OpenGL canvas), so games and IDEs capture cleanly instead of showing a blank frame.
- DPI-correct — per-monitor DPI awareness keeps pixels 1:1 on mixed-DPI setups.
- Configurable framerate — cap the render loop at 15, 30 or 60 fps to trade smoothness for CPU, or run unlimited.
- Adjustable picture-in-picture — put the inset in any corner at 25%, 33%, 38% or 50% of the screen.
- Remembers your setup — window, display, layout and framerate choices are restored the next time you launch.
- Press
Escorqon the projected screen to stop at any time.
The whole app is one window. Pick what to project, choose how to lay it out, select the target display, and press the button. The chosen windows appear on the TV; nothing else does.
Find your platform below.
| Your system | What to do | Status |
|---|---|---|
| Windows 10 / 11 | Download the ready-made app — no setup | Supported and tested |
| macOS | Run from source | Experimental, unverified |
| Linux (X11) | Run from source | Experimental, unverified |
| Linux (Wayland) | Not supported — switch to an X11 session | Not supported |
Everything needs two displays, with the second one set to Extend rather than Duplicate.
The easy way. Download one file and run it — no Python, no setup:
~29 MB · Windows 10 (1903 or newer) and Windows 11
Windows SmartScreen will warn you the first time, because the file is not code-signed. Click More info → Run anyway. If you would rather not run a prebuilt binary, use the source route below instead.
From source, if you prefer:
git clone https://github.com/fbugri-rgb/PaneCast.git
cd PaneCast
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
python panecast.pyYour Python must include Tcl/Tk — it is a checkbox in the official installer labelled "tcl/tk and IDLE". If it was unchecked, PaneCast cannot start; see Troubleshooting for the fix.
| Reference | |
|---|---|
| Capture backend | backends/windows.py |
| Dependencies | requirements.txt |
| Fast capture engine | windows-capture — optional, but without it PaneCast uses a slower fallback |
Experimental. This backend is written but has never been run on a Mac. Expect rough edges, and please open an issue with what you find. There is no prebuilt
.appyet.
git clone https://github.com/fbugri-rgb/PaneCast.git
cd PaneCast
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python3 panecast.pyYou must grant Screen Recording permission, or macOS quietly hands back images of your wallpaper instead of the window you picked — with no error:
System Settings → Privacy & Security → Screen Recording → enable it for your terminal (or for PaneCast), then restart the app.
| Reference | |
|---|---|
| Capture backend | backends/macos.py |
| Dependencies | requirements.txt |
| Required package | pyobjc-framework-Quartz — installed automatically by the command above |
Experimental. This backend is written but has never been run on a Linux desktop. Expect rough edges, and please open an issue with what you find.
git clone https://github.com/fbugri-rgb/PaneCast.git
cd PaneCast
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python3 panecast.pyIf Python complains that tkinter is missing, install it from your package
manager — it ships separately from Python on most distributions:
sudo apt install python3-tk # Debian / Ubuntu
sudo dnf install python3-tkinter # Fedora
sudo pacman -S tk # ArchKeep a compositing manager running, otherwise windows hidden behind others may capture as blank or stale.
| Reference | |
|---|---|
| Capture backend | backends/linux.py |
| Dependencies | requirements.txt |
| Required package | python-xlib — installed automatically by the command above |
Not supported, and not an oversight. Wayland deliberately prevents one application from reading another's pixels, so there is no way to capture a window the way PaneCast needs to. PaneCast detects a Wayland session and tells you, rather than silently showing black.
Check which session you are in:
echo $XDG_SESSION_TYPE # prints "wayland" or "x11"To use PaneCast today, log out and pick an X11 / Xorg session at the login screen. Applications running under XWayland are capturable from an X11 session.
Proper Wayland support means PipeWire via xdg-desktop-portal — it is
on the roadmap.
| Python | 3.9 or newer (only needed when running from source) |
| Tcl/Tk | Required — bundled with Python on Windows and macOS, a separate package on Linux |
| Displays | At least two, with the second set to Extend |
python panecast.py- Set your second display to Extend (
Win + P→ Extend). - Pick the window you want to project under 1st Application Window. Click 🔄 Refresh List if you opened it after launching.
- Optionally pick a second window and choose a layout style.
- Choose the target display under Target TV / Extended Display, using 🔄 Detect Screens if it isn't listed.
- Click START PROJECTING.
- Press
Escorqon the projected screen to stop.
| Mode | Behaviour |
|---|---|
| Side-by-Side | Two windows split left / right. |
| Top / Bottom | Two windows stacked vertically. |
| Picture-in-Picture | Window 1 fills the screen; window 2 floats in a corner as an inset. |
In picture-in-picture mode the Inset size and Corner controls become available, letting you place the inset in any of the four corners at 25%, 33%, 38% or 50% of the screen. They are greyed out in the other layouts.
Two windows selected, picture-in-picture chosen, with the inset controls active.
Target framerate caps the render loop. Frames are only redrawn when the capture thread delivers a new one, so a lower cap costs nothing in responsiveness but meaningfully reduces CPU. 30 fps is the default; drop to 15 fps on a laptop running on battery, or raise it if you are projecting at 1080p and want maximum smoothness.
Your selections are written to %APPDATA%\PaneCast\settings.json when you
start projecting and when you close the app, then restored on the next launch.
A remembered window is only reselected if a window with that title is open
again; a remembered display only if it is still connected. Delete the file to
reset to defaults.
Note that the file records the titles of the windows you projected. It stays on your machine, but delete it if you would rather not keep that history.
Selecting "(None - Single Window Mode)" for the second window projects a single window scaled to fit, preserving aspect ratio.
PaneCast uses two capture backends and prefers the faster one:
- Windows Graphics Capture (WGC) — the modern, GPU-accelerated OS capture
API, via the
windows-capturepackage. Runs on its own thread and delivers frames asynchronously. This is the path that keeps working when a window is occluded. PrintWindow(GDI) — a purectypesfallback used when WGC is unavailable. It requires no extra dependencies but is slower and some applications refuse to render into it.
Window and monitor enumeration is done directly against the Win32 API
(EnumWindows, EnumDisplayMonitors, DwmGetWindowAttribute) through
ctypes, so there are no heavyweight GUI or automation dependencies. The
interface itself is plain tkinter, and captured frames are scaled with
Pillow and blitted onto a tkinter.Canvas.
Window bounds come from DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS)
rather than GetWindowRect, which avoids the invisible drop-shadow border that
would otherwise show up as a dark band around the projected window.
Frames are only rescaled and re-uploaded when the capture thread actually
delivers a new one, so an idle projection costs almost nothing. The PhotoImage
for each slot is allocated once and reused via .paste() rather than rebuilt
per frame, and canvas coords/itemconfigure calls are skipped unless
something moved.
The remaining bottleneck is Tk itself. Pasting into a canvas-attached image forces a synchronous redraw of the whole item, which dominates at high resolutions:
| Output resolution | Cost per rendered frame | Practical ceiling |
|---|---|---|
| 1920 x 1080 | ~20 ms | ~50 fps |
| 3840 x 2160 | ~78 ms | ~13 fps |
If your TV is 4K, setting it to 1080p for projection is by far the biggest single win available. Genuinely high framerates at 4K would require replacing the Tk canvas with a GPU-backed presentation surface.
Capture is isolated behind a small interface in backends/, so
each platform plugs in its own implementation. backends.get_backend() picks
the first one that reports itself available.
| Platform | Backend | Status |
|---|---|---|
| Windows 10 / 11 | Windows Graphics Capture, GDI fallback | Supported and tested |
| macOS | Quartz (backends/macos.py) |
Experimental, unverified |
| Linux (X11) | X11 + Composite (backends/linux.py) |
Experimental, unverified |
| Linux (Wayland) | none | Not supported by design |
The macOS and Linux backends were written against the Quartz and Xlib APIs but have never been executed on those platforms. They are structurally complete and conform to the backend interface, but expect to fix details on first contact with real hardware. Bug reports and fixes are very welcome. PaneCast states plainly in its own UI when it is running an experimental backend.
- macOS needs
pyobjc-framework-Quartzand Screen Recording permission (System Settings > Privacy & Security > Screen Recording). Without it macOS silently returns wallpaper-only images rather than raising. Quartz'sCGWindowListCreateImageis used rather than ScreenCaptureKit: it is deprecated as of macOS 14 but synchronous and far simpler. ScreenCaptureKit is the right long-term target. - Linux needs
python-xliband an X11/Xorg session. Capturing occluded windows relies on the X Composite extension, so a compositing manager should be running. - Wayland deliberately forbids one client from reading another client's
pixels. The sanctioned route is PipeWire via
xdg-desktop-portal, which needs an interactive permission prompt per capture. PaneCast detects a Wayland session and says so, rather than silently capturing black frames. Apps running under XWayland are still capturable from an X11 session.
Subclass CaptureBackend in backends/base.py and
implement is_available(), list_windows(), list_monitors(),
window_bounds() and grab(). Implementing open_stream() is optional: if it
returns None, the render loop polls grab() each tick instead. Then register
the class in backends/__init__.py.
Users then need no Python installation.
pip install pyinstaller
pyinstaller --onefile --windowed --name PaneCast --noconfirm --exclude-module cv2 --runtime-hook pyi_rth_stub_cv2.py panecast.pyThe binary appears in dist/PaneCast.exe (~29 MB).
--onefilebundles everything into a single.exe.--windowedsuppresses the console window.--exclude-module cv2together with the runtime hook drops OpenCV, whichwindows-captureimports at module scope but only uses insave_as_image()— a function PaneCast never calls. Bundling it anyway costs 45 MB (70 MB → 29 MB) for code that never runs. The hook installs a stub module so the import still succeeds. Verified: the resulting binary still reportsHAS_WINDOWS_CAPTURE = Trueand captures live frames.
Pushing a v* tag runs .github/workflows/release.yml,
which builds the executable on a windows-latest runner, verifies it, and
publishes a release with the binary and its SHA256 attached:
git tag v0.2.0
git push origin v0.2.0Running the workflow manually (Actions → Build and release → Run workflow) performs the same build but uploads the exe as a workflow artifact instead of publishing a release, which is useful for testing a change to the pipeline.
The workflow asserts that HAS_WINDOWS_CAPTURE is true and that the binary
stays under 45 MB, so a regression that silently drops the fast capture engine
or re-bundles OpenCV fails the build rather than shipping.
A code-signing step is present but disabled. To enable it, add your certificate
as the CERT_PFX_BASE64 secret (base64-encoded PFX) and its password as
CERT_PASSWORD, then remove the if: false from the Sign executable step.
PyInstaller cannot cross-compile. A Windows .exe must be built on
Windows, a macOS .app on macOS, and a Linux binary on Linux. Once other
platforms are supported, a GitHub Actions matrix build across
windows-latest, macos-latest and ubuntu-latest is the usual way to
publish all three from one workflow.
Single-file builds unpack to a temporary directory at startup, so they launch more slowly than
--onedir. Some antivirus engines also flag freshly-built, unsigned PyInstaller binaries; code-signing the executable avoids this.
ModuleNotFoundError: No module named 'tkinter'
Python was installed without Tcl/Tk. Re-run the official Python installer,
choose Modify, and tick tcl/tk and IDLE. Note that upgrading Python
will not add it — the installer preserves your existing feature selection, so
you must explicitly use Modify.
"Only 1 Screen Detected"
Press Win + P and choose Extend (not Duplicate or Second screen
only), then click 🔄 Detect Screens.
Projected window is black or frozen
Some applications — particularly ones using hardware-accelerated or protected
rendering — refuse to draw into the GDI fallback. Install windows-capture to
enable the WGC backend, which handles most of these correctly.
The window I want isn't in the list Only visible, titled, non-tool windows are listed. Click 🔄 Refresh List after opening the app. Minimised windows may not capture reliably.
- Cross-platform capture backend architecture
- Verify the macOS and Linux backends on real hardware
- macOS ScreenCaptureKit backend (replacing deprecated Quartz calls)
- Wayland support via PipeWire / xdg-desktop-portal
- Configurable target framerate instead of a fixed timer
- Remember last-used window/display selection between runs
- Adjustable picture-in-picture size and corner
- Automated release builds via GitHub Actions
- Code-signed binaries (needs a code-signing certificate)
Issues and pull requests are welcome. Capture is isolated behind the interface
in backends/base.py, which is the natural seam for new
platforms; see Adding a backend.
The most useful contribution right now is running the macOS or Linux backend on real hardware and reporting what breaks. Both are written, neither has ever been executed.
Released under the MIT License — see LICENSE.

