# wl-mirror: mirroring one Wayland output onto a window or a second display

> wl-mirror is a GPL-3.0 C client that captures a wlroots output through one of several protocols and displays it on a resizable window or fullscreen on another output, handling fractional scaling, rotations, flips and custom regions, with a stream mode that lets a script change the mirror while it runs. It suits a sway user who wants to share a laptop screen, and it depends on wlroots protocols rather than on the Wayland core.

**Ferdi265/wl-mirror** — a simple Wayland output mirror client

- Repository: https://github.com/Ferdi265/wl-mirror
- Stars: 535 · Forks: 22
- Language: C
- License: GPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/ferdi265-wl-mirror

## The gap it fills, and what a mirror client actually has to do

wl-mirror is a simple Wayland output mirror client, and the project states its motivation plainly: it attempts to provide a solution to sway's lack of output mirroring by mirroring an output onto a client surface.

The reason this is not built into the compositor is worth understanding, because it shapes the whole design. Under the old X11 model, a client could ask for the contents of a root window and display it anywhere, because X11 gave every client the ability to read the framebuffer of any drawable. Wayland deliberately removed that, because it is a security hole and a compositing mess. A Wayland client gets the surfaces it has been given, and reading the contents of another client's output requires an explicit protocol negotiated with the compositor.

So a mirror client is a program that asks the compositor for a copy of one output and displays it somewhere else, and the interesting engineering is in how it asks and in how it presents.

The feature list covers the presentation side. It mirrors an output onto a resizable window, and it mirrors an output onto another output by fullscreening the window. It reacts to changes in output scale, including fractional scaling, and it preserves aspect ratio. It corrects for flipped or rotated outputs, and it supports custom flips or rotations. It supports mirroring custom regions of outputs. And it supports receiving additional options on stdin for changing the mirrored screen or region on the fly, which the documentation says works best when used with pipectl.

That last one is the feature that turns a static mirror into something usable. A mirror you have to restart to move to another display is a mirror you set up once and leave; a mirror whose target can be changed while it runs is a tool you use during a meeting.

The project is GPL-3.0 licensed, the primary language is C, the build is CMake with OpenGL, and the recent releases are v0.18.5 from 2026-02-09, v0.18.4 from 2026-02-01 and v0.18.3 from 2025-08-06, with the last push to the repository on 2026-09-26. The tree has src/, include/, deps/ for vendored dependencies, glsl/ for the shaders, proto/ for the protocol definitions, man/ for the manual page, scripts/ for the examples, and a version/ directory.

## The capture backends, and why there are seven of them

The backends are the part of the design that a reader should spend the most time on, because they explain both the performance and the compatibility story.

The auto backend automatically tries the backends in order of efficiency and use the first that works, and it is the default. Behind it sit three protocol families, each with three variants.

The export-dmabuf backend uses the wlr-export-dmabuf-unstable-v1 protocol to capture outputs. The screencopy family uses wlr-screencopy-unstable-v1, in a plain form, a DMA-BUF form and an SHM form. The extcopy family uses ext-image-copy-capture-v1, again in a plain form, a DMA-BUF form and an SHM form.

Two of those three families are marked auto in the list, and the reason is a change in the ecosystem. The wlr protocols were the original way to do this under wlroots, and ext-image-copy-capture-v1 is the newer standard path, and the plain forms of both are tried first with the DMA-BUF and SHM variants as fallbacks for compositors or drivers that only implement part of the protocol.

That is why the backend list is a compatibility table as much as a performance table. A compositor might implement the newer protocol and not the old one, or the old one and not the new one, and the variants cover the case where the base protocol is present but a particular buffer mechanism is not.

The naming is worth noting as well. Two of the protocol names end in unstable-v1, and the documentation does not pretend otherwise. The extcopy and export-dmabuf protocols are still carrying an unstable marker in their names, which is a fact about the ecosystem rather than about this client, and it is the reason a version of wl-mirror that works on one compositor can fail on another even at the same wl-mirror release.

The --backend option lets you pin a specific backend rather than trusting the auto order, which is what you want when diagnosing a capture problem or when a particular path is known to work on your hardware.

## Scaling, transforms and regions, and the sway compatibility layer

The presentation options are grouped into three families, and each family is a small specification.

The scaling modes are fit, which is the default and scales to fit, cover, which scales to cover and crops if needed, exact, which only scales to exact multiples of the output size, and the filter choice between linear scaling, which is the default, and nearest neighbour scaling. The exact mode is the one worth knowing about, because it is how you get pixel-perfect text on a high density display, at the cost of a much smaller image.

The transforms are specified as a dash-separated list of flips followed by a rotation, and flips are applied before rotations. The named forms are normal, flipX and flipY, and the rotations 0cw, 90cw, 180cw and 270cw, plus the counter-clockwise set 0ccw, 90ccw, 180ccw and 270ccw.

And then there is a second, redundant set of names: flipped for the X flip and 0, 90, 180 and 270 for clockwise rotation, provided for compatibility with sway output transforms. That is a small piece of pragmatism that says the tool is meant to be driven by a compositor configuration, and a compositor writes its transforms in sway's vocabulary.

The regions use the format used by the slurp utility, so a region is given as '<x>,<y> <width>x<height> [output]'. On start the region is translated into output coordinates, and when the output moves the captured region moves with it, and when a region is specified the output argument is optional, which is how you mirror a region of one screen onto a different screen.

The transformation of a region from screen coordinates into output coordinates at startup, and following the output afterwards, is the part that makes it usable. A region that is anchored to the output rather than to the screen keeps pointing at the same thing when the layout changes.

## The command line, and the options that matter during a call

The usage block is long, and the pattern in it is that most options come in pairs: a positive form and a negated form, with the negated one marked as the default.

That is a deliberate interface choice for a compositor tool, because the natural way to drive one is to rewrite the command line when the state changes. If the tool only had --freeze, then a script that wanted to freeze and unfreeze would have to remember the current state and choose the opposite flag, and a keybinding would have to be rebound. With both forms, the same key can run the same command twice.

The pairs are visible in the listing. There is --show-cursor and --no-show-cursor, with showing the cursor on the mirrored screen as the default, and --invert-colors and --no-invert-colors, with not inverting as the default. The freeze family is the interesting one, because it is a three-way rather than a pair: --freeze freezes the current image on the screen, --unfreeze resumes the screen capture after a freeze, and --toggle-freeze toggles the freeze state of screen capture.

A freeze is not a pause of the capture, it is a freeze of the picture, and the distinction is what makes it useful. A screenshot of a video call, a slide deck or a terminal that you want to keep on screen while you talk about something else, held still while the underlying output continues, is a genuinely useful thing that a mirror tool can do almost for free.

The fullscreen family is also paired. --fullscreen displays wl-mirror as fullscreen and --no-fullscreen displays it as a window, which is the default. There is then --fullscreen-output O, which sets the fullscreen target output to output O and implies --fullscreen, and --no-fullscreen-output, which unsets the fullscreen target output and implies --no-fullscreen, which is the default.

So there are two distinct fullscreen ideas here. One is fullscreening the mirror window on the output you are already on, and the other is choosing which output the mirror should cover, and the second is what lets you mirror your laptop screen onto a projector, which is the use case the whole tool exists for.

The remaining options are the quiet ones. --verbose and --no-verbose control debug logging, with quiet as the default, and the region, transform, scaling and backend options are the ones already covered. The --title option is the one that changes behaviour rather than appearance, because a title is what a window manager parses.

The --stream option is the outlier, since it turns the process into something that reads commands for the rest of its life, and it is the reason the tool is a building block rather than an application.

## Stream mode, the title placeholders and the interactive workflow

The most interesting option is the stream mode, and it is what makes the tool composable with a script.

In stream mode, wl-mirror interprets lines on stdin as additional command line options. The rules are spelled out: arguments can be quoted with single or double quotes, but every argument must be fully quoted or fully unquoted, unquoted arguments are split on whitespace, and no escape sequences are implemented.

The no escape sequences rule is the one to internalise, because it is where a script that works in a shell breaks. A quoted argument cannot contain an escaped quote, so a window title or a path with a quote in it has to be quoted the other way, and a value that mixes quoting styles will not do what the script expects.

The title option is the other piece of the composability story, and it supports placeholders:

```bash
--title='Wayland Mirror Output {target_output}'
--title='{target_output}:{width}x{height}+{x}+{y}'
--title='resize set {width} {height} move position {x} {y}'
```

The placeholders are {width} and {height} for the size of the mirrored area, {x} and {y} for the offsets on the screen, and {target_width}, {target_height} and {target_output} for information about the mirrored device. The third example is the giveaway that this is aimed at a window manager that can parse a title, because it is emitting a command as a window title, which is a way of driving a compositor without a socket.

The scripts/ folder contains the example that ties it together. wl-present is a small script to demonstrate the use of the -S option to interactively present on Sway, and the documentation says it is especially useful when binding the wl-present subcommands to keyboard shortcuts. The Sway keybinding example binds a mode called present in which different keys run mirror, set-output, set-region, unset-region, set-scaling, toggle-freeze and custom.

The dependencies for that workflow are named too: it requires wl-mirror, the wl-present script, pipectl as optional, slurp, and one of wofi, wmenu, rofi or dmenu. wl-present only allows one instance by default, but multiple instances can be used at the same time using the --name option or the WL_PRESENT_PIPE_NAME environment variable, which is how two mirrors on two screens get separate control channels.

There is also a release.sh script that generates a release tarball for the currently checked out commit if there is a release tag on it, and a Kanshi configuration example that launches wl-mirror in fullscreen on an external output mirroring the internal output when the profile is switched with kanshictl switch mirror-hdmi, which is the profile-driven way to do the same thing that the keybindings do interactively.

## Conclusion

wl-mirror is worth trying if you run sway and want a second view of an output, because the capture is done through the wlroots protocols rather than by screen scraping, which is why it keeps up with fractional scaling and why several capture paths exist, including the newer ext-image-copy-capture-v1 alongside the wlr screencopy and export-dmabuf protocols. It is the wrong tool if your compositor is not wlroots based, and it is the wrong tool if you only need to record the screen. Before you build it, check three things: that your compositor implements the protocols it uses, because the backends are wlr-export-dmabuf-unstable-v1, wlr-screencopy-unstable-v1 and ext-image-copy-capture-v1, and a compositor without any of them has nothing for the client to talk to; that you install the optional dependencies for the interactive mode, because the wl-present script needs pipectl, slurp and one of wofi, wmenu, rofi or dmenu, and the streaming mode works best with pipectl; and that you read the protocol naming carefully, because the newer extcopy and export-dmabuf protocols are marked unstable in their names and the auto backend tries them in order of efficiency, so what works on one compositor may not work on another.

## FAQ

### What does wl-mirror do and which compositors does it support?

It captures a wlroots output and mirrors it onto a resizable window or fullscreen onto another output, reacting to output scale changes including fractional scaling, preserving aspect ratio, and correcting for flipped or rotated outputs. It talks to the compositor through the wlr-export-dmabuf-unstable-v1, wlr-screencopy-unstable-v1 and ext-image-copy-capture-v1 protocols, so it needs a wlroots based compositor that implements one of them.

### How do I change what wl-mirror is showing while it is running?

Use the stream mode, where wl-mirror interprets lines on stdin as additional command line options. The documentation notes that arguments can be quoted with single or double quotes but every argument must be fully quoted or fully unquoted, unquoted arguments are split on whitespace, and no escape sequences are implemented. The bundled wl-present script demonstrates this interactively on Sway and needs pipectl, slurp and one of wofi, wmenu, rofi or dmenu.

### Which scaling and region options does wl-mirror support?

The scaling modes are fit, which is the default, cover, which crops if needed, and exact, which only scales to exact multiples of the output size, with linear or nearest neighbour filtering. Regions use the format used by the slurp utility, as '<x>,<y> <width>x<height> [output]', are translated into output coordinates on start, and move with the output when it moves, and the output argument becomes optional when a region is given.

## Sources

- [Ferdi265/wl-mirror on GitHub](https://github.com/Ferdi265/wl-mirror)
- [Issues](https://github.com/Ferdi265/wl-mirror/issues)
- [License: GPL-3.0](https://github.com/Ferdi265/wl-mirror/blob/main/LICENSE)
- [README](https://github.com/Ferdi265/wl-mirror/blob/main/README.md)
- [Releases](https://github.com/Ferdi265/wl-mirror/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ferdi265-wl-mirror
