No description
  • Rust 99.1%
  • Nix 0.4%
  • Shell 0.3%
  • Python 0.2%
Find a file
Kasper J. Seweryn 248b96be76
All checks were successful
Test / test (push) Successful in 10m53s
chore: release 0.4.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:41:28 +02:00
.forgejo/workflows ci: skip the checks for documentation-only changes 2026-10-17 11:32:46 +02:00
assets feat: initial commit of waff-shell 2026-10-17 11:05:29 +02:00
crates fix(calendar): file tasks only in calendars that take them, and move refused ones out 2026-10-22 09:39:18 +02:00
docs docs(design): describe the Tasks view's new header 2026-10-18 00:15:03 +02:00
nix fix(privacy): light the camera Firefox opens itself, the screen from PipeWire, and pulse a red dot 2026-10-21 15:34:24 +02:00
tools fix(bench): stop the session bus the sway wrapper starts 2026-10-18 01:24:45 +02:00
.gitignore feat: initial commit of waff-shell 2026-10-17 11:05:29 +02:00
Cargo.lock chore: release 0.4.5 2026-10-22 09:41:28 +02:00
Cargo.toml chore: release 0.4.5 2026-10-22 09:41:28 +02:00
devenv.lock feat: initial commit of waff-shell 2026-10-17 11:05:29 +02:00
devenv.nix feat: initial commit of waff-shell 2026-10-17 11:05:29 +02:00
devenv.yaml feat: initial commit of waff-shell 2026-10-17 11:05:29 +02:00
flake.lock build: add flake.lock 2026-10-17 11:30:25 +02:00
flake.nix feat: initial commit of waff-shell 2026-10-17 11:05:29 +02:00
LICENSE feat: initial commit of waff-shell 2026-10-17 11:05:29 +02:00
README.md docs: say what the shade's pull and a key repaint, and how to measure it 2026-10-18 01:19:32 +02:00
rust-toolchain.toml feat: initial commit of waff-shell 2026-10-17 11:05:29 +02:00

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

The bar Calendar panel
The bar: menu, clock, keyboard, notifications, CPU, volume, battery Calendar: month with per-calendar colours, the day's timeline, up next
Tasks Event editor
Tasks: due today and later, priorities, completed Event editor: calendar, time, timezone, repeat
Audio panel Wi-Fi panel
Audio: outputs, inputs and per-app volume Wi-Fi: networks, saved ones out of range, VPN
Join a Wi-Fi network New VPN
Joining a secured network A new WireGuard or OpenVPN profile
Bluetooth panel Bluetooth pairing
Bluetooth: connected, paired and nearby devices Pairing: confirm the passkey the device shows
Notifications panel Power panel
Notification history and Do Not Disturb Power: system load, profiles, sleep, device mode, on-screen keyboard

Launcher and calculator

Runner Runner script
The runner: apps and commands as you type Shift+Enter opens a multi-line shell script
Calculator Pinned apps
The calculator: units, currencies and bindings across lines, with a history Pinned apps, the list behind the pie launcher

Notifications and calls

Notification toasts Call on the desktop
Toasts, expanded: actions and a quick reply An ongoing call: the card and the bar's call timer
Low battery
Low battery: a warning, and the bar's edge turns orange

Phone

Home Shade being pulled Calendar Wi-Fi
Home: clock, up next, the gesture bar Pull the status strip down for the shade Calendar, agenda by colour Wi-Fi
Bluetooth Audio Notifications Power
Bluetooth Audio Notifications Power
Pie launcher Arranging the pie Quick reply Low battery
Hold on an empty home screen for the pie Hold a slot to carry it; drop it on the band to unpin Reply to a message from its notification Low battery on the status strip
Incoming call Ongoing call Muted call Call over an app
An incoming call rings full screen, with quick replies In a call: a green strip with the timer, and the call card Mute, Speaker and Hang up on the card The card stays above an app; the strip returns to the call

Keyboard

Docked keyboard Floating keyboard
The on-screen keyboard, docked under the windows Detached, it floats and can be dragged anywhere
Phone keyboard
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.