- Rust 99.1%
- Nix 0.4%
- Shell 0.3%
- Python 0.2%
|
All checks were successful
Test / test (push) Successful in 10m53s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| assets | ||
| crates | ||
| docs | ||
| nix | ||
| tools | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| devenv.lock | ||
| devenv.nix | ||
| devenv.yaml | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
| rust-toolchain.toml | ||
waff-shell
A Wayland desktop shell for niri, written in Rust. One binary draws the bar, the slide-out panel, notifications, OSDs, the runner and calculator, and an on-screen keyboard. On a narrow screen, or when forced, it switches to phone chrome: a status strip, a pull-down shade, a home screen with edge gestures and a pie launcher.
Everything is drawn by the shell itself (tiny-skia + cosmic-text on layer-shell surfaces); there is no toolkit underneath.
Warning
This project is vibe-coded. I need a shell that works, every day, on my own machines — and I don't have the time to implement it, maintain it and fix its issues myself. Vibe-coding is what makes it exist at all. If that's something that's triggering you, then in words of Edsel Dope: I don't give a fuck about you, or your point of view, or your stupid selfish attittude. Cause it seems, to be, that you never really gave a fuck about me.
Screenshots
Rendered off-screen on niri, with niri's blur behind the shell's translucent surfaces, over a photo of a city at night. The Wi-Fi networks, Bluetooth devices, calendars, notifications and calls are demo data; the audio devices are those of the machine the shots were taken on.
Bar and panels
Launcher and calculator
Notifications and calls
![]() |
![]() |
| Toasts, expanded: actions and a quick reply | An ongoing call: the card and the bar's call timer |
![]() |
|
| Low battery: a warning, and the bar's edge turns orange |
Phone
Keyboard
![]() |
![]() |
| The on-screen keyboard, docked under the windows | Detached, it floats and can be dragged anywhere |
![]() |
|
| On a phone it attaches to the bottom edge |
Background: photo by Andre Benz on Unsplash (https://unsplash.com/photos/an-aerial-view-of-a-city-at-night-JBkwaYMuhdc), Unsplash License.
Building
cargo build --release
./target/release/waff-shell # the shell
./target/release/waff-shellctl # talk to a running shell
waff-shellctl --help lists the commands — opening panels, volume and
brightness steps, Do Not Disturb, the keyboard, the runner. Debug builds add
commands for driving the shell without input devices (phone on, touch,
pointer, snapshot, …).
With Nix, the flake provides the package and a Home Manager module; see
nix/README.md. The design rules and guidelines — tokens,
components, surfaces, interaction, efficiency — and the decisions behind
them are in docs/design/.
Battery and memory
At rest the shell sleeps: nothing polls, nothing animates, and it wakes only when something it shows has changed.
| Scenario | Wakeups/s | CPU | Instructions | RSS | PSS |
|---|---|---|---|---|---|
| Desktop, idle | 0.12 | 1.3 ms/min | 1.6 M/min | 37.7 MiB | 28.6 MiB |
| Phone, idle | 0.11 | 6.9 ms/min | 12.3 M/min | 42.6 MiB | 33.4 MiB |
| Desktop, idle after five rounds of use | 0.08 | 1.1 ms/min | 1.7 M/min | 51.2 MiB | 40.7 MiB |
| Calendar panel open | 0.07 | 1.2 ms/min | 1.0 M/min | ||
| Power panel open (live CPU, memory, sensors) | 3.05 | 87 ms/min | 219 M/min |
Peak RSS over the five rounds of use was 53.1 MiB.
At rest the only timer is the clock's, armed for the turn of each minute:
one repaint of the digits and the compositor's release of the buffer. The
clipboard and the primary selection are read and set through data control
(ext_data_control_v1, or zwlr_data_control_v1 version 2) on the
shell's own Wayland queue, so they cost no thread; a thread of their own is
started only on a compositor without that protocol. The rest is the odd NetworkManager or UPower signal, and only
the ones that change something drawn reach the UI thread. The bar's CPU
figure samples every five seconds only while someone is there: after 30 s
without input the compositor says so (ext_idle_notify_v1) and the sample
stops until input comes back. NetworkManager's per-network signal strength
and the Wi-Fi radio's own properties (its bitrate is republished every few
seconds while connected) are subscribed only while a Wi-Fi panel is open,
and the battery's time left and draw only matter while the Power tab is.
The Power tab's sensors are read on a thread of their own, since some of
them take the hardware milliseconds to answer; it sleeps until the tab asks
for a reading. That thread is about 0.5 of the Power panel's wakeups a
second, the price of the UI thread never waiting on a sensor.
In use, a surface repaints only what changed. A panel slides by its margin and is not redrawn while it moves; a dialog, toast or OSD is drawn once and shown whole, with no fade; the Power panel's once-a-second sample repaints only the blocks whose figures changed and commits nothing when none did. A Wi-Fi scan that only moved signal strengths repaints the rows whose figures changed, a volume echoed back while a slider is dragged repaints its row, and the pointer moving over the toasts repaints the cards it leaves and enters. The phone's shade is painted once at its full size and a pull shows a crop of it, as tall as the finger has pulled, so a frame of the pull or of its settle repaints only the band of the sheet's foot where it was and where it is, and the rows it uncovers the first time. A key that moves the keyboard's focus repaints the control it left and the one it reached, and a key that changes nothing repaints nothing. A repaint costs what it covers: glyphs are blended whole rather than a pixel at a time, a rounded shape is rasterised only where its corners are, and a path is drawn into the rows it touches rather than through a mask the size of the surface. Opening the calculator copies a prelude loaded once rather than loading it again.
How it was measured. The release build on a headless sway (pixman) at
1920×1200, scale 1.5 — 585×1266 for the phone — with a private session bus
and a filtered system bus that reaches the real NetworkManager, UPower,
logind and power-profiles daemons but not BlueZ. Wakeups are context
switches summed over every thread of the process; CPU is the time every
thread spent on a CPU (/proc/PID/task/*/schedstat); instructions are those
retired in user space as counted by perf stat, which unlike time does not
move with the clock speed or the rest of the machine's load; memory is RSS
and PSS from /proc/PID/smaps_rollup. Idle is measured over 120 s starting
40 s after start, use is five rounds of opening and closing the panels, the runner, the
calculator, the pinned list and the keyboard, and three notifications a
round. Machine: AMD Ryzen 5 7640U, Linux 7.2.
To reproduce:
cargo build --release -p waff-shell
tools/bench.sh up # 1920x1200 at 1.5; `up 585x1266` for the phone
sleep 40; tools/bench.sh measure 120
tools/bench.sh usage 5; tools/bench.sh measure 60
tools/bench.sh down
The shade's pull and the keyboard walking the panels have scenarios of
their own, driven through waff-shellctl's debug commands, so they want an
optimised build with debug assertions:
CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS=true cargo build --release -p waff-shell
tools/bench.sh up 585x1266; tools/bench.sh shade 3; tools/bench.sh down
tools/bench.sh up; tools/bench.sh nav 2; tools/bench.sh down
| Scenario | Instructions | Damaged | Copied |
|---|---|---|---|
| Three shade pulls, phone (585×1266 at 1.5) | 275 M | 3.3 Mpx a pull | 3.5 Mpx a pull |
| Three shade pulls, phone chrome at 1920×1200, 1.5 | 518 M | 10.4 Mpx a pull | 11.1 Mpx a pull |
| Two rounds of keys through four panels | 505 M | 87 kpx a key | 199 kpx a key |
Before the crop, the same pulls were 717 M and 1756 M instructions, with every frame the whole sheet damaged and copied; before the focus was narrowed, a key repainted the whole 675×1200 panel, and the two rounds were 1450 M.
Layout
| Crate | |
|---|---|
waff-shell |
the shell: surfaces, input, panels, phone chrome |
wui, wui-core, wui-widgets |
the Wayland client, rendering and text, widgets |
waff-services |
D-Bus services: NetworkManager, BlueZ, UPower, notifications, tray, niri IPC |
waff-audio |
PipeWire |
waff-calendar |
CalDAV / vdir calendars and tasks |
waff-config |
the TOML config and the palette |
waff-ipc |
the control-socket protocol |
waff-sysinfo |
CPU, memory and sensors |
Licence
GPL-3.0-or-later.





































