wlroots-based Wayland compositor with virtual outputs and physical cursor continuity. Originally forked from dwl.
LOC: 7626 total, 2960 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
- 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 fullscreen, workspace moves, and display reconnects
with separate headless compositors and a Wayland test app. The tests use
temporary sockets and do not change your running desktop. They also require
the wayland-client development files.
./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.
Seamless cursor tracking across monitor gaps. Set physical dimensions in monrules[].
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.
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.