Skip to main content
The Ryoku shell is Quickshell (QML) under ryoku/shell/quickshell/, driven by the ryoku-shell Go daemon under ryoku/shell/ipc/. This page is the contract for changing or adding UI: where things live, which tokens to reach for, and the motion language popouts have to honor. Hands-on background: read docs/structure.md, docs/ui-ux.md, docs/frame.md, and docs/conventions.md in the repository before a non-trivial change.

View and logic stay apart

The shell is split deliberately:
  • QML renders and animates. Each surface lives in its own directory under ryoku/shell/quickshell/ and each component is a single .qml file.
  • ryoku-shell decides. The Go daemon supervises the Quickshell components, owns wallpaper, clipboard, and lock state, registers as the GNOME keyring system prompter (ipc/prompter.go, ipc/secretexchange.go), and serves the control socket.
QML never holds policy. When the UI needs a system mutation (package, service, display, compositor, power, network, update, rollback, cursor, font, wallpaper, hardware), it talks to ryoku-shell or calls a named ryoku-* helper through a narrow service. No ad hoc hyprctl calls from a component; for compositor state, go through a service that wraps the IPC.

One component per file

The QML tree mirrors the surfaces from docs/ui-ux.md: Compose with components; do not merge unrelated UI into one file. A new concern is a new .qml next to the surface it belongs to, not a branch inside an existing component.

Shared tokens

Each surface ships its own singleton pack under <surface>/Singletons/ and exposes them through a qmldir. They are the single source of truth for colors, motion, and shell-wide configuration; literal one-off values are opting out of theming. In the pill (ryoku/shell/quickshell/pill/Singletons/qmldir): Practical rules:
  • Never hardcode a color, font, duration, or radius that should follow the theme. Read from the surface’s singletons. Brand orange and the 力 mark are fixed (Theme.brand / #F25623); everything else is themed.
  • Motion lives in Motion. Pick morph for the project’s signature morph, standard for everyday transitions, fast for snappy feedback. Match the curve already in pill rather than inventing a new one; consistency is the aesthetic.
  • Geometry that drives a motion (a morph progress, a clip width) is fine to compute; only the timing and curve must come from the tokens.
  • When a value needs to become user-configurable, add it to Config, expose it through Ryoku Settings, and make the view read the shared value.

Motion is the frame swelling open

The pill, the edge popouts, the screen border, and the centre surfaces share one signed-distance field (Ryoku.Blobs, the compiled QML plugin under ryoku/shell/plugin/). Every popup that emerges from the frame must animate as the frame expanding, never as a separate panel flying in. The hard rules, from docs/frame.md:
  • Pin the frame-side edge. Grow only the perpendicular dimension; clip the content and reveal it edge-first as the body widens.
  • Fuse with a neck into the border. A BlobRect body in the same BlobGroup extends a neck clamped to its own width, so it retracts in lockstep instead of snapping off as a flickering sliver.
  • Open with the morph curve, close with a spring. Open uses the project’s cubic-bezier(0.16, 1, 0.3, 1) with no end-overshoot. Close uses a lightly-damped SpringAnimation: it melts fully flush into the border, then springs back a touch, so the resting state stays flush.
Forbidden in any popup that emerges from the frame:
  • Sliding a full panel in from off-screen.
  • Fading with opacity.
  • Center-zoom of the whole panel.
  • A standalone BlobRect whose width is held constant on close (it leaves a band wider than the island in the last frames; morph width down to the neck width).

Adding a popout

Popout.qml under pill/popouts/ is the reusable machinery: the blob body, a content slot, the pixel-perfect edge hover trigger, and the reveal clip. To add a new one (the same shape Mixer.qml and Power.qml use):
  1. Add ryoku/shell/quickshell/pill/popouts/Foo.qml: a transparent Item (anchors.fill: parent, an s scale property) holding the content, reading Theme and Motion for tokens and styling. The blob behind it is the surface, so do not paint a background.
  2. In pill/shell.qml, inside the overlay’s blob field, add Popout { group: blobGroup; frameThickness: 16; radius: 16; smoothing: 30; edge: "left"|"right"; Foo {} }.
  3. Union the popout’s triggerX/Y/W/H and bodyX/Y/W/H into the overlay input mask so its edge and open body catch input while the rest stays click-through.
  4. Trigger by hover (built in). For a keybind, route the ryoku-shell command to togglePopout rather than a centre surface.

Adding a centre surface

Centre surfaces (the toolkit deck, the launcher, the keyring prompt, the voice panel) live in pill/ itself. They keep the same motion contract as edge popouts (anchored to the pill, growing the blob field, no off-screen slide), and they read state from the daemon over the shell socket. The view never asks Hyprland or PipeWire directly; the daemon does, and the QML binds.

Layout discipline

Operational surfaces stay dense and predictable:
  • Use stable width, height, or implicit* sizing for repeated controls.
  • Use shared spacing tokens between rows and sections.
  • Cards for repeated items, menus, modals, and framed tools; avoid nested cards.
  • Keep text inside controls short enough to fit at narrow widths.
  • Do not use viewport-scaled font sizes.

The dev loop

ryoku/shell/dev-run.sh launches the shell from the checkout via qs -p with hot reload, so QML edits show as you save. Tune timing against the running surface; do not eyeball it. dev-stop.sh releases the dev shell, dev-binds.sh reloads Hyprland binds for a faster bind iteration. For non-trivial changes touching the daemon or a shared service, trace the flow from Config or IPC into every consumer rather than patching one visible page. The shared tokens and the daemon are the contracts; everything else is view.
Last modified on July 4, 2026