A minimal Wayland window manager built on top of RiverDelta — a fork of
River vendored in-tree at compositor/ —
written in C# / .NET 10. The bar/shell is provided by the external
Noctalia project.
Aqueous is a single-repo project: the .NET window manager and the Zig
compositor live side-by-side. No submodules, no extra clone steps —
git clone is enough. See compositor/ORIGIN.md and
docs/architecture.md for the why.
| Component | Description |
|---|---|
Aqueous |
Wayland/River compositor client (the window manager) |
Aqueous.Tests |
Unit tests for Aqueous |
| Noctalia (external) | Bar / shell (qs -c noctalia-shell) |
| tuigreet (external) | Login greeter |
- .NET 10 SDK
- Zig ≥ 0.16.0 — needed to build the in-tree
RiverDelta compositor at
compositor/. On Arch,zig-master-bin(AUR) currently provides 0.16.x. - Noctalia (
qs/ Quickshell) - xwayland-satellite — rootless XWayland bridge (River has no built-in XWayland; satellite is launched by Aqueous via
[[exec]]inwm.toml). - tuigreet (optional, for login)
wayland,wayland-protocols,libxkbcommon,libinput,pixman,libdrm,libevdev(full list mirrorscompositor/PACKAGING.md).
# Window manager (.NET)
dotnet build Aqueous.slnx
# Compositor (Zig). Produces ./bin/riverdelta.
scripts/build-compositor.shlaunch_river.sh rebuilds the compositor on demand if ./bin/riverdelta
is missing or older than the sources under compositor/, so for normal
dev iteration you can just ./launch_river.sh. For compositor-only
iteration: cd compositor && zig build.
To skip the in-tree compositor and use a system one (or a prebuilt
path), set AQUEOUS_RIVER_BIN=/path/to/riverdelta before running
launch_river.sh.
dotnet test Aqueous.Tests/Aqueous.Tests.csproj./launch_river.shThis starts a nested River instance, launches Aqueous, and spawns
Noctalia (qs -c noctalia-shell) as the bar. Logs land in /tmp/:
/tmp/river_log.txt— River compositor + WAYLAND_DEBUG trace/tmp/aqueous_wm.log— Aqueous stdout/stderr/tmp/noctalia.log— Noctalia stdout/stderr
wm.toml (place at ~/.config/aqueous/wm.toml) configures layouts, gaps,
keybindings, outputs, etc. See the file in this repo for an annotated example.
Aqueous launches supervised commands itself via [[exec]] blocks in
wm.toml. Each entry fires once after River advertises all its globals
(so layer-shell clients like the bar attach successfully on first
connect). Commands run via /bin/sh -c detached with setsid.
[[exec]]
name = "noctalia"
command = "qs -c noctalia-shell"
when = "startup" # "startup" (default) | "reload" | "always"
once = true # don't relaunch on --reload
restart = false # respawn (with backoff) on non-zero exit
log = "/tmp/noctalia.log"
env = { QT_QPA_PLATFORM = "wayland" }Backoff for restart = true follows 250 ms → 500 → 1 s → 2 s → 4 s →
8 s → cap 10 s, and resets on a clean (exit 0) termination. Setting
restart = true on a when = "reload" entry is allowed but rarely
useful.
A reference Arch PKGBUILD is included; it builds Aqueous AOT and ships:
/usr/bin/aqueous,/usr/bin/aqueous-wm(session launcher)./usr/share/wayland-sessions/aqueous.desktopso any Wayland-capable display manager (greetd/tuigreet, GDM, SDDM, …) lists Aqueous in its session picker./etc/xdg/aqueous/wm.tomlas the system default;aqueous-wmseeds~/.config/aqueous/wm.tomlon first login if missing.
Libinput configuration (pointer accel, tap-to-click, natural scroll, …)
is applied by Aqueous itself via the river_libinput_config_v1
Wayland protocol — no separate input daemon is involved.
For a turn-key login experience see
packaging/greetd/config.toml.example (greetd + tuigreet, with an
optional autologin snippet).
- Monocle layout crashes the WM —
LayoutProposerdropsVisible=falseplacements withRect.Emptyon the floor (zero-dimension guard fires before the visibility check). Seescratches/monocole_currently_causes_wm_crash.md. - Fullscreen demote path is fragile; there is no dedicated
exit_fullscreenaction — only thetoggle_fullscreenchord (Super+Shift+F) can leave fullscreen. Seescratches/currently_fullscreening_a_window_cannot.mdandscratches/keycombo_to_unfullscreen.md. - The scrolling layout is broken – it stacks windows on top of each other instead of scrolling them.
- Reserved space for the bar (currently hardcoded to 24px) — implement
proper
wlr-layer-shellexclusive-zone negotiation so bars of any height and on any edge work. - Support for multiple outputs (technically works but a bit hacky) — per-output tag state, hot-plug add/remove, per-output layout/scale/transform persistence, "move window/workspace to next output" semantics.
-
[[output]]config block: mode, scale, position, transform, VRR / adaptive-sync, DPMS / power management. - Fractional-scale (
wp-fractional-scale-v1) andviewporterstory for mixed-DPI setups — RiverDelta advertiseswp_fractional_scale_manager_v1,wp_viewporter, andwl_compositorv6;wlr_scenenotifies each surface of its per-output preferred (fractional) andpreferred_buffer_scalebased on the output(s) it intersects, recomputed on every scale commit. -
gamma-control/ night-light support. - Cursor theme and size configuration.
-
aqueousctlIPC socket: query focused tag/window/title, dispatch anyKeyBindingAction, trigger config reload, drive status bars and scripts (swaymsg-style). - On-the-fly config reload from a keybind / external tool, with error
reporting when
wm.tomlis malformed. - Man page, shell completions, log rotation (logs currently land in
/tmp/*.logfromlaunch_river.sh).
- Window rules (
[[rule]]matchingapp_id/class/title→ float / tile / tag / size / position / sticky). Required for things like "always floatpavucontrol" or "sendfirefoxto tag 2". - XWayland transport (rule-free):
xwayland-satellitelaunched via[[exec]]inwm.toml; session launcher exportsDISPLAY=:0,QT_QPA_PLATFORM=wayland;xcb,GDK_BACKEND=wayland,x11,SDL_VIDEODRIVER=wayland,x11,MOZ_ENABLE_WAYLAND=1,_JAVA_AWT_WM_NONREPARENTING=1, andXCURSOR_*. - XWayland policy / rules engine (Steam, JetBrains splash, Zoom, …) —
xwayland_shell_v1binding,RuleMatch.XWayland, auto-float heuristics for_NET_WM_WINDOW_TYPE(DIALOG,UTILITY,SPLASH, …) and non-nullWM_TRANSIENT_FOR. - Floating-window keybinds: toggle-float on focused tile, move/resize by keys, center-on-spawn, remembered geometry.
- Dedicated
exit_fullscreenaction (separate fromtoggle_fullscreen) and audit of all state-transition bindings. - Scratchpad / iconify-equivalent semantics.
-
xdg-activation-v1"demand attention" → tag urgency highlight for bars.
- Idle / lock / DPMS:
ext-idle-notify-v1,idle-inhibit-v1,lock_commandconfig key. Watching video should inhibit blanking. - Screencopy:
wlr-screencopy-unstable-v1(v3) is exposed by RiverDelta and bound in-process byWlrScreencopyClient(wl_shm+memfd_createpath).xdg-desktop-portal-wlrrides on the same global, so browser / Discord / OBS screen sharing works out of the box once the portal package is installed. - Clipboard-persistence daemon (or document
wl-clip-persist) and primary-selection guarantees beyond what River provides. - Per-seat keyboard layout switching exposed as a
KeyBindingAction(today only the input daemon applies static config).