wlroots-based Wayland compositor with virtual outputs and physical cursor continuity. Originally forked from dwl.
LOC: 8126 total, 1270 vwl.c
- virtual outputs (split physical monitors into independent workspaces)
- physical cursor continuity (smooth cursor movement across monitor gaps)
- master/stack tiling
- tabbed layout
- fullscreen modes (virtual/monitor)
- per-workspace layout state
- child dialogs centered over their parent window
- XWayland support
mod = logo key
mod+returnspawn terminalmod+dspawn menumod+qkill clientmod+j/kfocus next/prevmod+h/ladjust master widthmod+mzoom (swap master)mod+fcycle fullscreen (skips virtual fullscreen when it duplicates monitor fullscreen)mod+ttoggle tabbed layoutmod+spacecycle layoutmod+shift+equit compositormod+commafocus virtual output leftmod+periodfocus virtual output rightmod+shift+</>move client to monitor left/rightmod+0-9view workspace0-9mod+shift+0-9move client to workspace0-9mod+ctrl+shift+h/j/k/lmove workspace to virtual output
makeRun make check to test floating sizes, child dialogs, fullscreen, focus/tab order, workspace moves, display reconnects,
keep-awake requests, and captured cursor pixels with separate headless
compositors and Wayland test apps. The tests use
temporary sockets and do not change your running desktop. They also require
the wayland-client development files. The capture test uses software rendering
by default; VWL_TEST_RENDERER=gles2 ./tests/capture-test checks the GPU renderer
when a render device is available.
vwl.c owns window lifetimes, focus, floating/fullscreen behavior, and startup.
The desktop features live in these files:
| File | Owns |
|---|---|
workspace.c |
Workspaces, virtual outputs, and workspace moves |
output.c |
Monitor discovery, removal, screen geometry, and monitor focus |
cursor.c |
Pointer motion, window dragging, and physical screen crossings |
layout.c |
Tiled/tabbed layouts and tab order |
debug.c |
The human-readable state dump |
plumbing.c |
Wayland callbacks, input devices, layers, and session locks |
vwl.h declares the shared types and functions, grouped by source file.
The desktop modules read small views of config.h; the user configuration format
is unchanged. tests/compositor.h supplies the private compositor and client
process helpers used by the integration tests.
./vwlShell-facing IPC over a Unix socket.
- socket:
$XDG_RUNTIME_DIR/vwl.sock - client:
vwlctl - docs:
docs/ipc.md,docs/waybar-howto.md
vwlctl get-state
vwlctl subscribe
vwlctl set-workspace 3wlroots 0.20wayland-serverxkbcommonlibinputpixman- optional:
xcb,xcb-icccm(for XWayland)
Edit config.def.h and recompile.
Key settings:
- physical cursor gap jumps:
enable_physical_cursor_gap_jumps - virtual output rules:
vorules[] - monitor rules:
monrules[] - keyboard/trackpad settings
Split physical monitors into named regions. Each region gets its own workspace.
Move workspaces between regions with mod+ctrl+shift+hjkl.
The compositor keeps those moves when a display reconnects. It restores each
workspace to its last display and region, with the previously visible workspace
active again. Workspace lists in vorules[] set startup defaults for workspaces
that have no current or saved home.
static const VirtualOutputRule vorules[] = {
/* monitor name x y w h mfact nmaster lt[0] lt[1] */
{ "DP-1", "left", 0, 0, 960, 1080, 0.55f, 1, &layouts[0], &layouts[1] },
{ "DP-1", "right", 960, 0, 960, 1080, 0.55f, 1, &layouts[0], &layouts[1] },
};w/hof0= expand to monitor's remaining spacemfact= master area factor (0.0-1.0)nmaster= number of master windowslt[0]/lt[1]= primary/secondary layout functions
- virtual fullscreen: fills the virtual output's usable area, keeping panels and tab headers visible
- monitor fullscreen: fills the entire physical monitor, covering ordinary panels and tab headers
mod+f normally cycles through off -> virtual -> monitor -> off. When the
virtual output fills the physical monitor with no reserved panel space, it uses
off -> monitor -> off. Tabbed workspaces keep the virtual step so you can
still switch tabs, even if the tab header is hidden.
The compositor checks the current workspace and display on each keypress. Moving a workspace or changing its layout resizes a fullscreen window within its chosen scope; the next keypress skips any virtual-to-monitor step that would now repeat the same area and keep no tab controls.
An app's fullscreen request enters the same first mode as mod+f. Repeated
requests keep its current mode. A normal window that already fills its
workspace still enters fullscreen, removing its border and letting the app
hide its own controls. Floating windows regain their original size when
fullscreen ends. Opening another ordinary window in the same workspace exits
monitor fullscreen; virtual fullscreen stays active.
The first time a window switches from tiled to floating, vwl uses the app's initial content size, capped by the configured floating width/height factors and the current virtual output's usable area.
When a floating window returns to tiling, vwl remembers its content size. The
next switch to floating restores that size, shrinking it only if needed to fit
the current usable area, including space reserved for tab headers. The size
factors do not override this saved size. center_floating_windows still controls
whether vwl centers the window when it floats.
Each window keeps its saved size in memory until it closes. Fullscreen uses a separate saved rectangle; changing fullscreen modes does not replace this size.
vwl opens floating child dialogs centered over their parent, keeping them inside
the parent’s virtual output when they fit. Larger dialogs keep their requested
size and start at the output’s top-left corner. XWayland dialogs keep their
requested screen position unless it is (0, 0), which uses the same centering
rule. vwl places dialogs when they open; later redraws do not recenter them.
Set physical dimensions in monrules[] to keep the pointer at the same real-world
height or horizontal position when it crosses screens. vwl also uses these
coordinates for the cursor image on each screen, so a fragment visible across an
edge appears in the right place before the pointer tip crosses.
Without explicit origins, vwl places each screen beside a neighbour using their
physical sizes. Explicit origins anchor nearby automatic screens. vwl keeps
automatic screens from overlapping, but desktop pixels cannot describe every
mixed-density layout exactly. Set origin_is_set and x_mm/y_mm on each screen
when you need to match its exact position on your desk. Explicit origins are
never moved, even if they overlap. Rotated screens need physical dimensions in
the orientation in which you use them.
static const MonitorRule monrules[] = {
/* name scale transform x y phys{} */
{ "DP-1", 1.0f, WL_OUTPUT_TRANSFORM_NORMAL, 0, 0, {
.width_mm = 520, /* physical width in mm */
.height_mm = 320, /* physical height in mm */
.x_mm = 0, /* physical X position */
.y_mm = 0, /* physical Y position */
.size_is_set = 1, /* use explicit size */
.origin_is_set = 1, /* use explicit origin */
}},
{ NULL, 1, WL_OUTPUT_TRANSFORM_NORMAL, -1, -1, {} }, /* fallback */
};x/y= pixel position (-1,-1 = auto)scale= HiDPI factortransform= rotation (NORMAL/90/180/270/FLIPPED_*)phys{}= real-world dimensions for cursor math
~/.config/vwl/run
#!/bin/sh
waybar &
swayidle -w \
timeout 300 'swaylock -f' \
timeout 600 'wlopm --off \*' \
resume 'wlopm --on \*' \
before-sleep 'swaylock -f' &config.h
static const char *lockcmd[] = { "sh", "-c", "sleep 1 && killall -USR1 swayidle", NULL };
/* ... */
{ MODKEY|WLR_MODIFIER_SHIFT, XKB_KEY_L, spawn, {.v = lockcmd} },SIGUSR1 triggers swayidle's lock timeout, ensuring wake-on-input works.
config.h
static const char *screenshotcmd[] = { "sh", "-c",
"grim -g \"$(slurp)\" ~/Pictures/Screenshots/$(date +'%Y-%m-%d_%H-%M-%S').png", NULL };
static const char *screenshotclipboardcmd[] = { "sh", "-c",
"slurp | grim -g - - | wl-copy", NULL };
/* ... */
{ 0, XKB_KEY_Print, spawn, {.v = screenshotcmd} },
{ MODKEY, XKB_KEY_Print, spawn, {.v = screenshotclipboardcmd} },~/.config/vwl/run
export XDG_CURRENT_DESKTOP=vwl:wlroots
dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP
systemctl --user restart xdg-desktop-portal.service xdg-desktop-portal-wlr.service >/dev/null 2>&1 &This lets the portal services bind to the current vwl session.
Window and virtual-output shares include the actual pointer when the capture client requests it, including client-drawn and animated cursors. The pointer is clipped at the shared area's edges. Capture clients can also request frames without a pointer, or receive its image and position separately.
Virtual-output sharing additionally requires a patched xdg-desktop-portal-wlr:
https://github.com/wegel/xdg-desktop-portal-wlr/tree/vwl-support
Optional xdg-desktop-portal-wlr chooser override:
~/.config/xdg-desktop-portal-wlr/vwl
[screencast]
chooser_type=dmenu
chooser_cmd=wofi -d --prompt='Select a source to share:' -L 15Work in progress, used daily by the dev.