Wayland, with depth

A compositor for spatial desktops.

Shady turns ordinary Wayland windows into objects in a shared 3D world — and keeps a conventional scene-graph path for the days you just need to work.

  • Golden hour
  • Afterglow
  • Blue hour
  • Night
Live Wayland surfaces · GLES2 · one C plugin
wlroots 0.20.2Wayland core
GLES2Spatial renderer
C ABI v2Hot-reload plugins
Lua 5.4Config + runtime

What's inside

Experiments, with boundaries.

The compositor owns Wayland objects, GPU resources, lifetime and validation. Plugins and scripts own the effects, layouts and simulations layered on top, and can be swapped while it runs.

A real perspective world

xdg-shell clients become lit 3D panes with rounded glass frames, depth, wobble and shadows. Drawing, picking and collision share one geometry.

Hot-reloadable C plugins

Window representations, render hooks with the live camera, input and per-window shaders. ABI v2 migrates state across reloads.

Lua, twice

config.lua shapes the compositor before modules resolve; init.lua reacts to windows, workspaces and keys at runtime.

Headless, sanitized tests

Drive input, timers and screenshots from Lua against a headless GLES2 backend, under AddressSanitizer and UBSan.

A safe scene-graph path

--safe drops the spatial renderer for a conventional wlroots scene graph, so a broken experiment never locks you out.

A standalone shell

shady-shell adds a taskbar, workspaces, launcher, window menu and quick settings over its own protocol, themeable per rice.

Make it yours

Configure in Lua. Extend in C.

Everything above is built from these two layers. The same Lua runtime drives the headless test suite, and plugins never touch raw GL state.

config.lua + init.lua
-- config.lua — shape the compositor
shady.set("window_corner_radius", 14.0)
shady.set("window_border_focus_color", "#FFB070")
shady.set("floor_horizon_fog", 1.0)
shady.plugin("build/libshady-plugin-afterglow.so")

-- init.lua — react to the desktop
shady.bind("Super+Return", function()
    shady.spawn("foot")
end)

shady.on("window.mapped", function(window)
    shady.log("arrived: " .. window.app_id)
end)
plugins/afterglow.c
/* A sky that knows where the camera is. */
static void draw(shady_host host,
        const struct shady_render_context *ctx, void *data) {
    if (!SHADY_RENDER_CONTEXT_HAS(ctx, aspect)) return;
    api->shader_uniform_vec4(host, sky, "u_cam_fwd",
        ctx->camera_forward[0], ctx->camera_forward[1],
        ctx->camera_forward[2], 0.f);
    api->shader_uniform_float(host, sky, "u_time",
        ctx->time_seconds);
    api->shader_draw_fullscreen(host, sky);
}

hook = api->render_hook_add(host,
    SHADY_RENDER_STAGE_AFTER_BACKGROUND, draw, NULL);

Fast path

From checkout to a nested compositor.

The Nix shell pins wlroots and the whole toolchain. Start nested inside your current session, then move to a real VT once the basics are solid.

terminal
nix develop
./tools/build.sh

# nested, with the Afterglow rice
./rices/afterglow/run.sh