Skip to content

Omarchy on Kandelo — the desktop shell: layer-shell bar, launcher, themes - #1241

Open
mho22 wants to merge 14 commits into
explore-dri-toolkitfrom
explore-dri-omarchy
Open

Omarchy on Kandelo — the desktop shell: layer-shell bar, launcher, themes#1241
mho22 wants to merge 14 commits into
explore-dri-toolkitfrom
explore-dri-omarchy

Conversation

@mho22

@mho22 mho22 commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Why

explore-dri-toolkit (#1318) puts a real C/C++ toolkit on the wasm32 sysroot, and #976 gives Kandelo a tiling compositor. This PR is the desktop shell that turns them into an Omarchy-shaped desktop: a layer-shell bar, a launcher, a theme engine, notifications, and the identity work that makes it look and feel like Omarchy rather than a compositor test.

Top of the six-PR DRI stack (#698#709#948#976#1318#1241). It stacks on explore-dri-toolkit.

This PR was split. It previously carried both the toolkit ports and the desktop built on them, at 62 commits. The ports moved to #1318; what remains here is the desktop, at 13 commits.

What changed

The shell

  • wlr-layer-shell, a bar, a launcher and themingzwlr_layer_shell_v1 with anchors, margins, exclusive zones and keyboard-interactivity, plus the arrangement engine that reserves strips and retiles beneath them. The file-based theme system (/usr/share/kandelo/themes/<name>/theme.conf, kwlctl dispatch theme, theme>> broadcast), kbar, klauncher, and kwl_layer_create() in libkwl. Includes a SIGPIPE fix: a control client exiting without reading its reply killed the desktop.
  • The Omarchy desktop — themes, menu and notifications. Image wallpapers (wallpaper = <file>, KWLP raw pixels bilinear-scaled by the compositor; raw because nothing in C decodes PNG or JPEG, so the page renders per-theme aurora wallpapers at staging time) and six themes: Tokyo Night, Catppuccin Mocha, Gruvbox, Nord, Everforest, Rosé Pine. The launcher registry offers Vim, NetHack, Nano and Bash running unmodified in wlterm, lazy-fetched from the shell image's archives. klauncher --menu is the Omarchy menu (Apps/Theme, ESC back to root). notify = <path> spawns a themed overlay toast on every theme switch, and kbar shows the kernel's monotonic uptime.
  • wp_presentation, and typed letters map by key — foot asks for presentation feedback, and the keymap resolves letters by key rather than by position.
  • The surface a stock client expects — the protocol surface foot, Waybar and mako actually require.
  • The desktop runs unmodified Waybar, mako and foot.
  • The desktop boots bare, and the presenter survives GL loss — wallpaper and bar, no windows. host_gl_present returns -EIO on a lost WebGL2 context; a lost context silently no-ops every GL call, so the pump had been presenting frozen pixels while the flip counters advanced. The loss and restore listeners arm at canvas attach rather than at presenter build.
  • The desktop renders at the output's device resolution, and the wldesktop package lands on the rebased kernel.

The fixes driving it exposed

  • A layer surface never enters the window stack. surface_at() returns layer surfaces so a click reaches the bar, and click-to-focus raises whatever it returns — so clicking Waybar pushed the bar into the window stack, where retile() handed it a tile: the whole usable area whenever the workspace held no windows.
  • A click cannot take an exclusive keyboard grab. wlr-layer-shell-unstable-v1 v4 states that for the top and overlay layers "the seat will always give exclusive keyboard focus to the top-most layer which has keyboard interactivity set to exclusive", and names the case it protects: "a lock screen or a password prompt". Click-to-focus ignored that; the other three kbd_set_focus() sites already consulted layer_kb_grab(). Verified end to end in a browser: kandelo-omarchy.spec.ts clicks the bar at (5,5) and then types, and KLAUNCHER_FILTER q=te n=1 now appears, so the assertion that used to fail passes.
  • An ARGB8888 buffer composites source-over, so a popup's edges reveal the wallpaper rather than black.
  • The wlpaint canvas blits at the output scale, so the palette swatches and the clear button are not clipped at device pixel ratio 2.

Both hosts

  • Node: the wlcompositor-{layer-shell,theme,keybind,resize,…} smokes, including coverage for the wallpaper triple (image, gradient fallback, broken file), the grid-preserving-resize recommit regression, CTRL-ALT exact-mods binds, the menu flow, and the notify hook. Plus dri-kms-registry (flipCount counts every latch), dri-kms-stats-sab (the flip gate, and the loss/rebuild pair against a fake GL) and kernel-blocking-retry-snapshot (-ESRCH is the export's contract, not corruption).
  • Browser: kandelo-omarchy.spec.ts covers the bar, launcher, themes, the vim launch through the launcher with an ABI-mismatch guard, the menu flow and the toast lifecycle. kandelo-kms-context-loss.spec.ts drives the loss/rebuild path against a real GL context.

Rebased and rebuilt

This branch sits on explore-dri-toolkit at 55eea05d0. The history was rebuilt from 62 commits to 13, and the toolkit ports moved to #1318; one test commit added since brings it to 14.

Against the previous tip, 77 files differ outside those main itself moved. Of those, 51 are deleted session handoff docs (docs/plans/*handoff*.md, 9,437 lines): nothing in the repo referenced any of them, and main's docs/plans/ holds 89 files of which exactly one is a handoff, so shipping 51 more contradicted the project's own convention. Every *-plan.md was kept. One more deletion is the scratch crash-repro spec, and one is docs/plans/2026-06-22-scummvm-port-plan.md, unrelated to this stack. The rest are comment-only: plan labels that no longer resolve, replaced by the fact each stood for.

The A/B split had one real bug, now fixed. packages/registry/wayland-protocols/build.toml declares xml/wlr-layer-shell-unstable-v1.xml and xml/presentation-time.xml as build inputs, and the first file assignment put both XMLs in this PR — so #1318 would not have built standalone. Waybar is a layer-shell client and foot asks for presentation feedback, so both moved to #1318. Caught by xtask build-deps program-index-context-check.

Verification

  • cargo test -p kandelo --target aarch64-apple-darwin --lib1665 passed, 0 failed
  • bash scripts/check-abi-version.shexit 0, reporting "snapshot is in sync with sources" and "ABI_VERSION and snapshot are consistent". The 43 → 44 bump now lives in Toolkit on Kandelo — the C/C++ library stack behind foot, Waybar and mako #1318, not here.
  • The keyboard-grab fix is verified in a real browser, as described above.
  • scripts/run-libc-tests.sh302 passed, 20 XFAIL, 0 build failures, and 1 unexpected failure: regression/raise-race. That test forks 100 children inside a signal handler; 5 waits return ECHILD, reproducibly. scripts/run-libc-tests.sh:163 skips it whenever CI=true, so it does not gate this PR, and whether it regressed or predates the stack is not yet established.
  • scripts/run-posix-tests.sh174 passed, 0 FAIL, 3 XFAIL, 2 SKIP

Both suites need WASM_POSIX_RESOLUTION_POLICY=source-only-v1 and WASM_POSIX_SOURCE_ONLY_BINARY_ROOT=<repo>/local-binaries/source-only-v1. binaries-abi-v44/index.toml does not exist until the toolkit tier merges, so the binaries/ provenance tier cannot be materialized locally. The full host vitest suite needs both provenance tiers resolvable at once, which only CI's prepared workspace provides — it is the gate for that suite and for the browser demos.

The stack has since been rebased onto main at 888e628d5. Two gates were re-run at the new stack tip ccc7c6214 (explore-dri-omarchy): bash scripts/check-abi-version.shexit 0, snapshot in sync and the 43 → 44 bump consistent; scripts/ci-run-test-suite.sh vitest exact-abi-source3049 passed, 0 failed, with two files failing to collect on paths #1321 moved to crates/runtime-core/. That collection failure is a main defect and #1337 fixes it.

Known gaps, carried not introduced

  • kandelo-omarchy.spec.ts fails waiting for a sixth tile: foot opens, but no tile arrives. This line was unreachable before the keyboard-grab fix, so there is no earlier baseline for it. Not yet investigated.
  • The exclusive-zone release path — bar exits, tiling area grows back — is untested. A prototype dispatch closelayer exposed a host-layer deadlock when a layer client exits in reaction to a compositor event, which needs its own investigation.
  • host/test/wlcompositor-layer-shell-smoke.ts gained two cases that have never executed: the file needs wlcompositor.wasm, kbar.wasm, klauncher.wasm, kwlctl.wasm and wlclient-test.wasm from one provenance tier, and no tier holds all five. wldesktop owns the first three; build-programs.sh builds the other two. Closing this means adding kbar, kwlctl and wlclient-test as wldesktop outputs and bumping its build.toml revision.
  • apps/browser-demos/test/kandelo-kms-presenter.spec.ts was deleted with its CI step, and kandelo-kms-context-loss.spec.ts cannot take the vacated slot: it needs the built desktop binaries, and that job runs a bare checkout with no binary fetch. So the pixel-store fix is pinned only against a fake GL, which accepts an upload a real context would reject — and that rejection is the failure mode. docs/browser-support.md carries the note and the repair route: createCentralizedKernelWorkerTestDouble is the sanctioned replacement for the writable-property and cast tricks the old fixture used.

Docs: architecture.md (desktop shell, themes, menu, knotify), browser-support.md (Omarchy demo section), presets.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Phase B-1 matrix build status — pr-1241-staging

ABI v17. 0 built, 110 failed, 110 total.

Package Arch Status Sha
alsa-lib wasm32 failed
atk wasm32 failed
atkmm wasm32 failed
basu wasm32 failed
cairo wasm32 failed
cairomm wasm32 failed
expat wasm32 failed
fcft wasm32 failed
fmt wasm32 failed
fontconfig wasm32 failed
freetype wasm32 failed
fribidi wasm32 failed
gdk-pixbuf wasm32 failed
glib wasm32 failed
glibmm wasm32 failed
gtk-layer-shell wasm32 failed
gtk3 wasm32 failed
gtkmm3 wasm32 failed
harfbuzz wasm32 failed
jsoncpp wasm32 failed
libcurl wasm32 failed
libcxx wasm32 failed
libcxx wasm64 failed
libdrm wasm32 failed
libepoxy wasm32 failed
libevdev wasm32 failed
libffi wasm32 failed
libinput-lite wasm32 failed
libinput wasm32 failed
libpng wasm32 failed
libsigcxx wasm32 failed
libudev wasm32 failed
libwayland wasm32 failed
libxkbcommon wasm32 failed
libxkbregistry wasm32 failed
libxml2 wasm32 failed
libxml2 wasm64 failed
mtdev wasm32 failed
openssl wasm32 failed
openssl wasm64 failed
pango wasm32 failed
pangomm wasm32 failed
pcre2 wasm32 failed
pixman wasm32 failed
sdl2 wasm32 failed
spdlog wasm32 failed
sqlite wasm32 failed
sqlite wasm64 failed
tllist wasm32 failed
utf8proc wasm32 failed
zlib wasm32 failed
zlib wasm64 failed
bc wasm32 failed
bzip2 wasm32 failed
coreutils wasm32 failed
curl wasm32 failed
dash wasm32 failed
dbus wasm32 failed
diffutils wasm32 failed
dinit wasm32 failed
fbdoom wasm32 failed
file wasm32 failed
findutils wasm32 failed
foot wasm32 failed
gawk wasm32 failed
git wasm32 failed
grep wasm32 failed
gzip wasm32 failed
kandelo-sdk wasm32 failed
kernel wasm32 failed
less wasm32 failed
lsof wasm32 failed
m4 wasm32 failed
make wasm32 failed
mako wasm32 failed
mariadb wasm32 failed
mariadb wasm64 failed
msmtpd wasm32 failed
nano wasm32 failed
ncurses wasm32 failed
netcat wasm32 failed
nginx wasm32 failed
php wasm32 failed
posix-utils-lite wasm32 failed
sed wasm32 failed
spidermonkey wasm32 failed
tar wasm32 failed
tcl wasm32 failed
unzip wasm32 failed
userspace wasm32 failed
vim wasm32 failed
waybar wasm32 failed
wget wasm32 failed
xz wasm32 failed
zip wasm32 failed
zstd wasm32 failed
bash wasm32 failed
mariadb-test wasm32 failed
mariadb-vfs wasm32 failed
mariadb-vfs wasm64 failed
nethack wasm32 failed
node wasm32 failed
spidermonkey-node wasm32 failed
vim-browser-bundle wasm32 failed
nethack-browser-bundle wasm32 failed
rootfs wasm32 failed
shell wasm32 failed
lamp wasm32 failed
node-vfs wasm32 failed
wordpress wasm32 failed

Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.

@mho22
mho22 force-pushed the explore-dri-hyprland branch from 3104fbc to 66c47da Compare August 21, 2026 10:03
@mho22
mho22 force-pushed the explore-dri-omarchy branch from 29d653f to 272be07 Compare August 21, 2026 10:03
@mho22
mho22 marked this pull request as ready for review August 21, 2026 14:17
@mho22
mho22 force-pushed the explore-dri-hyprland branch from 66c47da to 050f429 Compare August 24, 2026 19:08
@mho22
mho22 force-pushed the explore-dri-omarchy branch from 7ee2af2 to 43031c2 Compare August 24, 2026 19:09
@mho22
mho22 force-pushed the explore-dri-hyprland branch from 050f429 to df918dd Compare August 26, 2026 14:37
@mho22
mho22 force-pushed the explore-dri-omarchy branch from 43031c2 to ab7ab2b Compare August 26, 2026 14:37
@mho22
mho22 changed the base branch from explore-dri-hyprland to explore-dri-toolkit August 26, 2026 14:38
@mho22
mho22 force-pushed the explore-dri-toolkit branch from 267319d to 55eea05 Compare August 28, 2026 20:15
@mho22
mho22 force-pushed the explore-dri-omarchy branch from ab7ab2b to ccc7c62 Compare August 28, 2026 20:15
mho22 added 14 commits August 29, 2026 10:12
Three things a desktop has that a window manager does not. wlr-layer-shell
is the protocol a bar or a launcher uses to own a strip of the output
instead of a window: it anchors to an edge, reserves an exclusive zone
the tiler then works around, and picks its own keyboard interactivity.

kbar and klauncher are the first two clients of it, and the theme engine
is what lets the compositor and its own clients share one palette.
`/?demo=omarchy` is the imitation the stack is aimed at: six themes with
their own wallpapers, an Omarchy-style menu, real applications in the
launcher, and knotify toasts with a bar uptime module.
A client that paces itself needs to know when its frame actually
reached the screen, which is what wp_presentation reports. The keymap
also grows the F-keys and the navigation cluster.

Browser input maps a typed letter through `KeyboardEvent.key` rather
than physical position: the guest applies its own keymap, so sending
position would apply a layout twice and a non-QWERTY user would type
the wrong letter.
Unmodified clients bind more of wl_compositor than in-tree ones did:
subsurfaces to compose their own decorations, a data-device to satisfy
clipboard setup, seat version 5 for axis events, and
`wl_surface.enter` so a client learns which output it is on.
The point the whole toolkit port was for: the bar is Waybar, the
notification daemon is mako, the terminal is foot, and none of them
were changed to run here.

Getting there needed the protocols they assume — xdg-output for output
naming, viewporter and fractional-scale for a scaled surface — and a
Hyprland-compatible IPC socket, because Waybar's workspace module is
written against hyprctl's event stream. foot's fonts are staged into
the image while it is composed.
… GL loss

A desktop that boots into windows it opened for itself is a demo; this
one boots to a wallpaper and a bar, and the user opens what they want.

The KMS presenter also rebuilds after a GL context loss instead of
staying dead, restoring the pixel-store state it inherited rather than
assuming defaults.
The compositor and its clients become one package with six outputs
rather than loose build products, so the browser demo resolves them the
way it resolves everything else.

A surface with no xdg_toplevel role is not a window and must not be
tiled; the klauncher exec column truncates instead of overwriting the
name beside it.
On a HiDPI output every surface has a logical size and a device size,
and drawing at the logical one gives a soft desktop. The compositor now
renders at device resolution and tells each surface the scale it is
being composited at, so libkwl clients, the gradient wallpaper and the
staged image wallpapers all rasterise at full resolution.

A layer surface renegotiates its size when the output changes rather
than keeping the box it was first given, and a surface learns its
output when it takes a role, which is what a client needs before it can
pick a buffer scale.
The desktop's packages have to be placed in the source-build roster,
and the SpiderMonkey darwin host link has to find libiconv — a host
build that fails takes the whole graph with it even though the package
is unrelated to the desktop.
`g.zorder` is the window stack. `retile()` walks it and overwrites each
member's x/y/w/h with a tile, and on a workspace holding no windows
that tile is the whole usable area.

A layer surface reached it through click-to-focus: `surface_at()`
returns layer surfaces on purpose, so a click can land on the bar, and
click-to-focus raised whatever it returned. Pressing one of Waybar's
workspace buttons therefore pushed the bar into the window stack, where
it took a tile and drew its clock across the canvas. No xdg_toplevel
configure is sent for a layer surface, so Waybar never learned its new
box and kept painting bar content into it.

The guard sits in `zorder_raise()`, the only door into `g.zorder`.
`retile()` still selects on `surface_visible()`, which is true for every
mapped layer surface by design; with the door shut that path is
unreachable, so a second check there would guard a state that can no
longer occur.
Both repaint paths treated every client buffer as opaque. An ARGB8888
surface therefore punched its own background through whatever sat
behind it: a translucent Omarchy popup drew its corners as black
instead of revealing the wallpaper.

wl_shm ARGB8888 is premultiplied by the Wayland spec, so the source
contributes unscaled and the destination is attenuated by 1 - alpha.
`blend_premultiplied()` applies that to the software blit, on both the
memcpy row path and the nearest-sampling scale path. The GL path
enables GL_ONE / GL_ONE_MINUS_SRC_ALPHA and gives the fragment shader a
third `u_use_tex` state, so a sampled texture keeps its alpha only when
its format carries one; XRGB8888 still forces alpha to 1 because its
fourth channel holds no coverage.

`buffer_has_alpha()` checks both the wl_shm and the DRM format enums.
The two share one field and cannot collide: WL_SHM_FORMAT_ARGB8888 is 0
and the DRM constants are fourcc codes.
Click-to-focus called `kbd_set_focus()` on whatever was under the
cursor, ignoring any exclusive layer-shell grab. Clicking the bar while
klauncher held the keyboard handed focus back to the window underneath,
so the launcher stopped receiving keystrokes.

wlr-layer-shell-v1 does not allow that. Its `exclusive` entry states
that for the top and overlay layers "the seat will always give
exclusive keyboard focus to the top-most layer which has keyboard
interactivity set to exclusive", and names the case it protects: "a
lock screen or a password prompt". A click that moves focus away is how
such a surface gets bypassed. klauncher is an overlay-layer surface
asking for exactly that.

The other three `kbd_set_focus()` sites already consult
`layer_kb_grab()` — the map path and both destroy paths — so this makes
the outlier agree with them. `zorder_raise()` stays outside the guard:
raising is stacking, and the protocol governs keyboard focus only.
CTRL+SHIFT+Space repainted the compositor's half of the desktop but
never the bar's. The hook wrote Waybar's stylesheet, then walked /proc
to send the bar SIGUSR2 — and never matched it.

Two reasons, both in how it read /proc/<pid>/cmdline. `read -r cmd`
stops at a newline, but cmdline separates its arguments with NUL and
ends with one, so cmd came back as the whole command line with its
separators dropped. And the bar reaches its own exec through the socket
gate's `exec /usr/local/bin/waybar`, which leaves argv[0] absolute, so
`case $cmd in waybar*)` could not have matched even on argv[0] alone.

Read to the first NUL with `-d ''`, which yields argv[0] by itself, and
match on its basename. The switch now runs end to end: THEME_HOOK
reports the bar's pid, Waybar reloads on the signal, and
kandelo-omarchy.spec.ts clears gate 6 through to the notification.
`scripts/ci-vitest-evidence-classes.tsv` must exactly cover the live
Vitest file inventory, so `ci-run-test-suite.sh vitest exact-abi-source`
refuses to enumerate while the two drift. This branch raises
ABI_VERSION, which is exactly when that group runs.

Classify the test files this branch adds, and drop the rows for files it
no longer carries. A file is `source-only` only when its whole import
closure stays clear of the binary resolver; the runner enforces that
separately from the cover check. The smokes exec staged wasm, so they
are `prepared-product`; the remaining unit tests import only `host/src/`.
@mho22
mho22 force-pushed the explore-dri-omarchy branch from ccc7c62 to 9072b53 Compare August 29, 2026 08:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant