Architecture
GPUi Shell is a single process. crates/services talks to the system
(D-Bus, compositor IPC, PulseAudio, PipeWire) and exposes plain reactive
state. crates/app turns that state into GPUI layer-shell windows built from
crates/ui.
Why services don’t depend on GPUI. GPUi Shell came after earlier shells built with iced and Tauri + Svelte. Keeping the system integrations framework-agnostic means they survive a change of UI framework. That’s a preference, not a rule: if tying them closer to GPUI’s state model would help performance or stability, open an issue or PR.
Crates
Section titled “Crates”| Crate | Owns |
|---|---|
services | One module per integration: state, listener, commands, lifecycle. No UI. |
app | The binary: windows, widgets, keybindings, config, IPC. |
ui | components (use ui::*), shell patterns (ui::patterns::X), theme. No IO. |
assets | Embedded Lucide SVG icons. |
story | cargo run -p story: a gallery of every component and pattern. Not shipped. |
State flow
Section titled “State flow”Every service works the same way (the strip at the bottom of the diagram):
- Read:
AppState::audio(cx).subscribe(), passed tostate::watch, which updates the widget and callscx.notify(). Use.get()for a one-off read. - Write: widgets call
dispatch(Command), never mutate state. It queues the command on the service’sJobQueue(services/src/jobs.rs), whose worker runs commands one at a time; the result comes back through the same signal.request(Command)awaits the outcome instead, and the queue’s in-flight list drives progress indicators. - Lifecycle:
ManagedService(services/src/lifecycle.rs). Eachstart()gets a freshRunToken; the run, and its command worker, end the moment it is stopped or restarted (token.run_until_cancelled), not at the next event. Startup mode per service iseager,lazyoroff([services]in config), and the;slauncher module controls them at runtime.
The compositor is the exception: it connects at startup, can’t be stopped, and picks its backend (Hyprland, Niri or Mango) at runtime.
Windows
Section titled “Windows”Every surface is its own layer-shell window:
| Surface | Layer | Instances |
|---|---|---|
| Bar, Dock | Top | One per display |
| Panels | Overlay | One at a time (control center, tray, media, sysinfo) |
| Launcher | Overlay | Singleton |
| OSD, notifications | Overlay | Transient; the shell is the notification daemon |
| Frame | Top | One per display: draws bar.style = "frame" and, while a panel is open, catches clicks outside it to close it |
| Frame exclusion | Overlay | Three invisible strips per display that keep windows inside the frame |
Windows are matched to compositor monitors by output name
(state::monitor_for_display), never by position or order.
Config and IPC
Section titled “Config and IPC”- The shell never writes
config.toml. What it persists goes tostate.toml, which overrides config. The theme module also savestheme.toml. - Both files hot-reload. A broken file keeps the last good value and raises a
notification, and
gpuishell --validateshows the error. - A second
gpuishellinvocation talks to the running one over$XDG_RUNTIME_DIR/gpuishell.sock(it toggles the launcher).
Adding things
Section titled “Adding things”- Bar module: a directory in
app/src/bar/modules/exposing aBarWidgetDescriptor, listed once inmodules/mod.rs. - Launcher module: a directory in
app/src/launcher/modules/implementingLauncherPage, registered inmodules/mod.rs. - Service: a module in
services/src/implementingManagedService, registered inapp/src/state.rsand the[services]config. - Component or pattern: in
crates/ui, with a story incrates/story.
If you add or remove a service or window, update
docs/public/architecture.svg in the same PR. It’s plain SVG written by
Claude, so ask an AI agent to update it or edit it by hand. The header comment
documents the grid.