No backward compatibility. A release may change or remove a configuration key, a private RFB extension's wire format or a feature, with nothing that reads the old form. Check
wlshare.example.tomlagainst your config when you upgrade, and upgrade the zDesk gateway with it when the protocol versionwlshare --versionnames has changed.
A VNC server for wlroots-based Wayland compositors, built for the
zDesk gateway. It captures one
output through wlr-screencopy, serves it over RFB 3.8 with Raw or ZRLE — or, to
a client that asks, as zDesk does for its browsers, as one H.264 stream whose
quality and frame rate follow the link, or for a link with room to spare as lossless PNG
tiles with a video stream for each region that moves, each H.264 by the host's hardware or libx264,
or, following the link, as RemoteFX Progressive tiles, still or moving (alpha),
or for a LAN as lossless planar tiles, still or moving, as a Windows host draws under its Lossless policy (experimental) — injects input through the virtual keyboard and pointer protocols,
shares the clipboard through wlr-data-control, carries the desktop's sound from
PipeWire over the connection itself — and tells the client what pixel density the
framebuffer is drawn at, which standard RFB cannot, so a scale 2 output is
shown sharp at 2x and a client's own density becomes the output's. A desktop with
more than one monitor sends the client the list, so the one being shared is the
client's to choose, and a second connection from that client is shown another
output beside it, so two are seen at once. The compositor pointer is excluded from captured frames, so
wlshare captures the cursor image on its own — whatever shape the application
under it chose, at the output's pixel density — and sends it through the RFB
Cursor pseudo-encodings, with its alpha to a client that lists Cursor With
Alpha. The client moves it without waiting for a framebuffer update.
See it in action on its page, and set it up with Setting up wlshare.
It is compositor-independent within that protocol surface: any wlroots-based
compositor exposing the required protocols is the same kind of peer.
wlr-screencopy version 2 or later is required, and so are ext-image-copy-capture
with output capture sources, which the cursor image comes from, and xdg-output
version 3, which says where each output lies in the layout, where the pointer is
aimed. Output resizing
and rescaling additionally need wlr-output-management; input and clipboard use
the virtual keyboard, virtual pointer, and wlr-data-control protocols when the
compositor offers them. Keeping the cursor separate on a headless output, and
capturing it, requires wlroots 0.19 or newer.
At the framebuffer layer, any RFB 3.8 client that advertises the standard Cursor
pseudo-encoding and can draw Raw rectangles can connect. ZRLE is used when
listed unless the client also asks for wlshare's private video stream or its
tiles. A configured
login additionally requires RSA-AES. Cursor support is required because wlshare
never puts the pointer in framebuffer pixels. The density and outputs extensions
are asked for by the client and stay silent otherwise; zDesk asks for both,
and for the video stream, or with the picker's Tiles with motion, Progressive
tiles (alpha) or Lossless tiles (experimental) the tiles, on a
vnc target with subtype = "wlshare", and reads
wlshare as any VNC server on a target without it. Audio is a private extension
that zDesk asks for in a session on such a target started with sound: while it
listens wlshare's own sink is the default, so every
stream that follows the default plays there rather than on the host's speakers,
and that sink's monitor is sent encoded as lossless FLAC, or as Opus at the
rate the client names where it asks for that instead. An application pinned
to a sink of the host's stays there and is heard on the host. A client
that does not list it hears nothing.
The sound, the camera and the microphone are always on, with no configuration
to turn them on: the package depends on the PipeWire, WirePlumber and
pipewire-pulse they run through. A client that lists the camera extension —
zDesk does, for a session started with the picker's Camera tick — can lend the
desktop its camera:
the H.264 it sends is decoded with the system's libavcodec into a PipeWire video
source, "wlshare remote camera", which applications reaching cameras through
PipeWire can open, and the client is asked for frames only while one has it open.
A browser is one through xdg-desktop-portal's Camera interface, which the
portal offers only beside a backend that implements Access, such as
xdg-desktop-portal-gtk — xdg-desktop-portal-wlr alone does not. The package
recommends both, so an install that takes its Recommends has the interface.
Chrome also needs chrome://flags/#enable-webrtc-pipewire-camera enabled;
without the flag or the portal it looks only at /dev/video* and lists no
camera. A
client that lists the microphone extension can lend the desktop its microphone
the same way: a PipeWire audio source, "wlshare remote microphone", fed with the
16-bit PCM the client sends while an application records from it.
One client is on the desktop at a time: a
connection that finishes the handshake takes it from whoever holds it, the way
Windows Remote Desktop does, and the RFB shared flag changes nothing. See
docs/architecture.md for how it works and what it
deliberately leaves out.
Trixie ships Sway 1.10 and labwc 0.8.3 on wlroots 0.18, whose headless backend paints the cursor into captures. GitHub Pages serves a signed APT repository with Sway 1.11, labwc 0.9.7 and wlroots 0.19 rebuilt for trixie against its own libraries. The repository does not carry wlshare itself.
sudo mkdir -p /etc/apt/keyrings
sudo curl -fsSL -o /etc/apt/keyrings/wlshare.gpg https://andrewtheguy.github.io/wlshare/wlshare.gpg
sudo tee /etc/apt/sources.list.d/wlshare.sources <<'EOF'
Types: deb
URIs: https://andrewtheguy.github.io/wlshare
Suites: trixie
Components: main
Signed-By: /etc/apt/keyrings/wlshare.gpg
EOF
sudo apt update && sudo apt install sway # or labwcThe packages are Debian's own source packages, pinned by their .dsc on
snapshot.debian.org in packaging/apt/sources.env, with the series in
packaging/apt/patches/<source>/ applied after Debian's patches. wlroots stays
on 0.19, the newest series trixie's libdrm and wayland-protocols can build, and
labwc on 0.9, its last series built against wlroots 0.19. They
are versioned <upstream>+<YYYYMMDD>-<N>~trixie, above both trixie's packages
and Debian's builds of the same release. scripts/build-sway-debs.sh builds them
in Docker into dist/sway/<arch>/.
The Release Sway packages workflow builds both architectures and publishes
them as the prerelease sway-<YYYYMMDD>-<N>, which only stores them. Publish
APT repository runs after it: it indexes the three most recent sway-*
releases, signs the index with the key in packaging/apt/pubkey.asc, installs
Sway and labwc from the result in a trixie container, and deploys it as the whole Pages
site. It needs the private key as the GPG_PRIVATE_KEY secret. That key is the
one podman-package's repository uses; its private half stays in the gitignored
keys/. To run the assembly locally, import that key first:
gpg --import keys/apt-signing-key.private.asc
./scripts/apt-repo-build.sh dist/sway site https://andrewtheguy.github.io/wlshare
./scripts/apt-repo-smoke.sh siteUbuntu 26.04 (resolute) ships Sway 1.11 and labwc 0.9.3 on wlroots 0.19.2, which wlshare runs on as they are, so the APT repository has no suite for it.
wlshare's pointer is bound to no output and aimed over the whole layout, so a
reload of the compositor's configuration (swaymsg reload,
labwc --reconfigure), which unbinds every pointer from its output, changes
nothing for it, on trixie and Ubuntu 26.04 alike
(docs/architecture.md, "Input and clipboard").
The daemon runs inside the Wayland session it captures, as the user who owns it:
WAYLAND_DISPLAY=wayland-1 wlshare --config ~/.config/wlshare/config.tomlor as the systemd user unit the package installs, started with the graphical
session after its environment contains WAYLAND_DISPLAY:
systemctl --user enable --now wlshare.serviceConfiguration is one TOML file, $XDG_CONFIG_HOME/wlshare/config.toml by
default. An empty file uses defaults, and
wlshare.example.toml lists every
setting.
Who may connect is one setting, and there are three answers. Nothing set: anyone
who reaches the port is in, and the session is in the clear, so listen stays on
loopback or behind a VPN or an SSH tunnel. [pam]: RSA-AES, RealVNC's security
type that TigerVNC and zDesk speak — the client sends the username and
password of the account wlshare runs as, PAM checks them under the service
wlshare (the package installs /etc/pam.d/wlshare), and everything after the
key exchange is encrypted. No other account is accepted, since the desktop
behind the port is that one user's. [password]: the same RSA-AES, asking for a
password alone, checked against an Argon2 hash in the configuration file —
for a host where no system account's password should be the way in. Print the
hash with
wlshare hash-passwordand paste it into the table; the password itself is never written down.
Classic VncAuth is deliberately not offered in any of the three: it names
nobody, truncates the password to eight characters, proves only knowledge of a
machine's secret, and leaves the session in the clear.
The server's RSA key is generated on first start into rsa_key_file and its
fingerprint logged, so it can be compared with the one the client shows.
Under [pam], because the password PAM verifies is the account's, the stack can
pass it on — a pam_exec ... expose_authtok line there is how a headless
session gets its keyring unlocked at VNC login.
state_socket names a Unix socket on which the daemon says whether a client is
on the desktop, and wlshare watch --held COMMAND --free COMMAND follows it
from a process of its own, so a sway session can move its windows onto the
headless output the client is shown and turn its monitors off while they are
watched from elsewhere, then put them back — also when the daemon was killed
with a client on the desktop, which the watcher reads as the desktop left. The
headless output can itself be off while nobody is watching: output names a
preference, not a requirement, and output_wait_secs keeps a client taking the
desktop waiting for it while the watcher enables it. The
example configuration shows the two swaymsg lines, and
docs/architecture.md says when each
runs.
A client may ask for the number of virtual displays it wants, as an RDP client
asks a Windows host for its monitors and a Mac's viewer asks for its virtual
displays: one or two. On sway the daemon makes and unplugs headless outputs
through sway's IPC, found in SWAYSOCK, until there are that many, with
nothing to configure: a session started with one headless output is given a
second when the zDesk gateway's target asks for two (virtual_displays = 2).
On any other compositor the outputs are what the compositor has, and the list
says so
(docs/architecture.md).
A custom keyboard layout — for a modifier remap the session's only keyboard has
to carry — goes under [xkb], with XKB_CONFIG_EXTRA_PATH in the unit's
environment naming the directory that holds it. Each key must keep its keysym:
the server resolves the client's keysyms through the same keymap it uploads.
The workspace has two crates: wlshare, the daemon, with the RFB protocol
it speaks inside it, and wlshare-proto, the bytes of wlshare's private
extensions, which the zDesk gateway depends on too, pinned by its
proto-v<N> tag, the protocol version the release that brought it tagged.
The second has no OS APIs and builds and tests anywhere. The daemon needs libxkbcommon, libpam, libpipewire,
libavcodec, libx264 and libva — the Wayland protocol is spoken in Rust, with
no libwayland — and
only runs under a wlroots-based Wayland compositor; its audio tests
additionally need ffmpeg, the independent decoder that reads the Opus
packets back. The FLAC coding is
sound-flac and the Opus coding
sound-opus, both shared with
the zDesk gateway, each pinned by its release tag. Nothing asks of the
processor more than its target's baseline. The daemon is the package at
the repository's root, the protocol crate under crates/. A bare cargo test covers both; elsewhere cargo test -p wlshare-proto covers the
second. Its H.264 tests decode with libavcodec, libx264's frames on every
machine and the hardware's where there is a VA-API encoder. Build the daemon with cargo build --release on a
Linux host with
libxkbcommon-dev, libpam0g-dev, libpipewire-0.3-dev, libspa-0.2-dev,
libavcodec-dev, libx264-dev, libva-dev, libclang-dev and
pkg-config. libclang links nothing: PipeWire's, FFmpeg's and x264's -sys
crates generate their bindings with bindgen, which loads it at build time;
libva's are the daemon's own, written out by hand.
scripts/capture-frames.sh builds and runs the capture container
(docker/capture/), with docker or with podman: a headless sway and the daemon started with
--capture-frames, playing a scripted desktop at each size, so that what the
video encoder was handed — every frame's pixels and damage — lands losslessly in
dist/captures/; the video scenarios play full screen the file --video
names. examples/wlcap.rs says what a capture holds and plays it out or
cuts it.
scripts/capture-samples.sh cuts each capture to 120 frames under its own
name, into dist/capture-samples/, for an encoder measured on a part of
each. --capture-frames is for a daemon run by hand, and one
systemd started refuses it (docs/architecture.md, "Capturing what the
encoder is handed").
Packages for Debian trixie and Ubuntu 26.04 (resolute), each on amd64 and
arm64, are built in Docker by scripts/build-debs.sh, into
dist/<suite>/<arch>/wlshare-<suite>-<arch>.deb. The
Release wlshare workflow builds the same and publishes them as the GitHub
release v<version>, the version being the workspace's in Cargo.toml; bump it
before running the workflow. The package version is the crate's, and the
distribution is in the file name only: each package names its own suite's
libraries, so it installs there alone. The package depends on
libwlroots-0.19 (>= 0.19.0), the first wlroots that keeps the cursor out of a
headless capture, and on pipewire, wireplumber and pipewire-pulse: the
PipeWire server the speaker, the camera and the microphone live in, the session
manager that makes the speaker the default and links streams to it, and the
PulseAudio server most applications play through. Without them the desktop's
sound never reaches the speaker and the client hears nothing.