slowshell
A wayland desktop shell. Tweakable, pluggable, lightweight, etc etc.

[!CAUTION] Status: Slowshell is in its early stages and moving pretty quickly. Expect rough edges and breaking changes to the config schema and the plugin ABI between versions.
What's in the box?
Simply:
- Lightwight
- Panels
- Application launcher
- Notifications
- Wallpapers
- Plugins
Why?
Well, as someone who likes customizing their shell, I have tinkered around a few wayland desktop shells before. Simply put, I wanted a lightweight, tweakable/pluggable wayland shell that had my most liked features.
So, I made slowshell. Simply: It's supposed to have decent defaults, but also supposed to be able to be tweaked significantly to one's liking.
Core principle
Slowshell is majorly update-based. It is made in-mind so that things only refresh when they absolutely need to. Giving us an event-driven system where even communication between components requires "waking up" the sleeping process in order to make a change.
Modules
The built-in stuff.
Desktop Items
Everything in slowshell is a DesktopItem, and desktop items are required to provide their
cause for updates otherwise they will be stagnant and sleeping as per the update system.
Simply: They tell slowshell when to update them, they draw when necessary, they mutate/change when needed, they draw when needed, otherwise they sleep.
Panels
Ah, yes. The panels. Almost every platform has these, we use them as the central location of which we keep track of live stats: time, battery, network, etc. Following that, I decided to make bars a component-based arena for these stats.
panels { panel "Main" position="top" transparent=#true monitor="HDMI-A-1" {} }
Panel Components
If you have used something like waybar before, you already know what these mean. In slowshell, they are like modular Desktop Items, but specific to being socketed to a bar to turn it into a status bar from just a- bar.
panel "Main" { right { // A component component "core/workspaces" { label "Workspaces" text #true } // A group group "SomeGroup" { item "core/tray" { label "Tray" } } } }
Note: run
slowshell list componentsto see all components
Notifications
A notification daemon. You can enable it with:
notifications { enabled #true }
Wallpapers
I won't bore you with text- just look at this:
wallpaper { backend "swaybg" // "slowshell" | "swaybg" | "swww" | "hyprpaper" | "custom" file "/path/to/something" // paths { // "HDMI-A-1" "/path/to/something" // "eDP-1" "/path/to/something-else" // } // spawn-args "command $PATH" // for backend = "custom" // max-width 1920 // max-height 1080 }
If this isn't in config, then no wallpapers will be set.
Spotlight
A launcher thingie. As of today, fully-keyboard based, no other way to control it. Has "modes" where for example "applications" mode is an application launcher, "clipboard" mode is a clipboard list (using cliphist).
spotlight { cache #true }
To launch, you need to send an IPC command:
# or
Note: Do slowshell list spotlights for all the modes.
Getting started
Optional Requirements
- Audio: PipeWire /
wpctl - Bluetooth: BlueZ
- Networking: NetworkManager
- Power: UPower + power-profiles-daemon
- Brightness:
brightnessctl - Wallpapers:
swaybg,swww, orhyprpaper - Clipboard:
cliphist
Installing
Nix
Since slowshell has a flake, you could either run it or install it from github:
Or as an input in your flake:
{
inputs.slowshell.url = "github:bushyice/slowshell";
outputs = { self, nixpkgs, slowshell, ... }: {
nixosConfigurations.machine = nixpkgs.lib.nixosSystem {
modules = [{
environment.systemPackages = [
slowshell.packages.pkgs.system.default
];
}];
};
};
}
Other distros
|
# or
|
# or
|
Building
Nix
Cargo
Requires wayland platform libs, xkbcommon, GL and vulkan.
Usage
slowshell daemon run in the foreground slowshell start start the daemon in the background slowshell stop stop the running daemon slowshell ipc <Command> [args] send a command to the daemon slowshell list <resource> styles | plugins | renderables | components | items | spotlights slowshell config [--validate] [--show] [--current]
Examples:
The socket is at $XDG_RUNTIME_DIR/slowshell.sock (or
/tmp/slowshell.sock), so you can use socat too:
|
Config
Configuration is looked up in $HOME/.config/slowshell/config.kdl (falling back to
$HOME/.local/share/slowshell/config.kdl and /usr/share/slowshell/config.kdl)
and hot-reloads on change. A full example is in example.kdl;
font "Lexend" theme "catppuccin-mocha" panels { panel "Main" position="top" transparent=#true { components { left { component "core/workspaces" { label "Workspaces" text #true } } center { component "core/clock" { label "Clock" format "%I:%M %p" } } right { component "core/network" { label "Network" icon #true ssid #true } } } } }
Plugins (overview and docs)
The decision for plugins was pretty messy- Wasm has an overhead and is sandboxed, and i was trying to avoid scripting languages. I thought about luajit but sounded like just as much work as C-ABI native plugins.
And so, native .so plugins it is! They are supposed to be able to provide modules
and components that the core doesn't, and are able to use most features that the core
has.
Currently, plugins are designed with C-ABI, but my target is to support rust plugins initially with all the wrappers and wiring required to make it rusty.
- Note:
- Plugins'
.sofiles are discovered from$SLOWSHELL_PLUGIN_PATH,~/.local/share/slowshell/plugins,$XDG_DATA_DIRS/slowshell/plugins. - They can be enabled/disabled with:
// config.kdl plugins { enabled "example-hello" "git-status" disabled "broken-thing" }
- Plugins'
Versioning
The ABI version is a single u32 checked once at load time. A plugin with a different
version than the host willn't be loaded.
SL_PLUGIN_ABI_VERSION · c · crates/plugin/include/slowshell-plugin.h
SL_PLUGIN_ABI_VERSION · rust · crates/plugin/src/lib.rs
pub const SL_PLUGIN_ABI_VERSION: u32 = 1;
The symbols the host looks up are:
slowshell_plugin_init · c · crates/plugin/include/slowshell-plugin.h
int32_t ;
SlPluginMeta · c · crates/plugin/include/slowshell-plugin.h
typedef struct SlPluginMeta;
SlPluginMeta · rust · crates/plugin/src/lib.rs
The host exports the API to the plugin through a single SlHostApi table of
function pointers:
SlHostApi · c · crates/plugin/include/slowshell-plugin.h
typedef struct SlHostApi;
SlHostApi · rust · crates/plugin/src/lib.rs
Ownership and lifetimes
The rules from the header:
- plugin -> host strings and arrays are borrowed for that specific call.
- host -> plugin handles are valid for the call except the
SlHostApipointer and the plugin'sctxpointer, which are'static. - Canvas buffers returned are host-owned and reused and the pointer is valid only until the next view begins.
- Vtables must be
'staticand the SDK leaks them for that reason.
The core string type is SlStr: a borrowed pointer + length, NOT
NUL-terminated, and allowed to be null.
SlStr · c · crates/plugin/include/slowshell-plugin.h
typedef struct SlStr;
SlStr · rust · crates/plugin/src/lib.rs
Registration surfaces
A plugin registers things inside slowshell_plugin_init.
With the rust sdk, you get these from the registerar:
component::<T>("name")renderable::<T>("name")payload::<T>("command")desktop_item::<T>("name")compositor::<T>("name")spotlight::<T>("name")style("name", &sheet)config_parser::<T>(…)
The C-level vtables are declared in the header, for example a component:
SlComponentVtable · c · crates/plugin/include/slowshell-plugin.h
typedef struct SlComponentVtable;
SlComponentVtable · rust · crates/plugin/src/lib.rs
Writing a rust plugin
Add the SDK and build a cdylib:
[]
= "example-hello" # this will be the plugin id
= "0.1.0"
= "2024"
[]
= ["cdylib"]
[]
= { = "https://tangled.org/bushyice.com/slowshell" }
And then implement a trait and export it (requires Send + 'static).
use ;
export_plugin!;
export_plugin! emits slowshell_plugin_meta, slowshell_plugin_init and
slowshell_plugin_shutdown, and calls your function with Registrar.
Component · rust · crates/plugin-sdk/src/lib.rs
Registrar · rust · crates/plugin-sdk/src/lib.rs
export_plugin · rust · crates/plugin-sdk/src/lib.rs
The Context passed to callbacks is the plugin's view of the host: options,
logging, timers and fds, canvas allocation, config access, styles and themes,
compositor state, service snapshots, notifications, the shared registry and
dispatch.
Context · rust · crates/plugin-sdk/src/lib.rs
Other traits
| Trait | Purpose | Required methods |
|---|---|---|
Component | Panel component. | new, view |
Renderable | Standalone surface (popups, widgets). | new, view |
Payload | Command handler. | new, invoke |
DesktopItem | Layer-shell desktop item. | new, settings, view |
Compositor | Compositor backend adapter. | new |
Spotlight | Spotlight mode provider. | new |
Nodes
The SDK also provides Node and its constructors (Node::row, Node::column, Node::text, Node::icon,
Node::progress, Node::image, Node::canvas, ...), Style, Border,
Color, Theme, StyleSheet, Notification, Options, Args, Event,
EventMask and ItemEffect.
Writing a C plugin
Include the header and export the three symbols. A minimal component:
typedef struct Hello;
static void *
static void
static uint32_t
static void
static SlEffect
static bool
static void
static void
static const SlComponentVtable VT = ;
int32_t
void
SlPluginMeta
Build and install:
Config parsers
A plugin can add a root-level kdl block. The callback receives the raw block as string (or a null pointer if the block is absent) and returns non-zero to report that the entry was rejected.
SlConfigParserFn · c · crates/plugin/include/slowshell-plugin.h
typedef int32_t ;
SlConfigParserFn · rust · crates/plugin/src/lib.rs
pub type SlConfigParserFn =
unsafe extern "C" fn ;
In rust:
reg.;
// later
let config: = ctx.;
If a plugin's config block is named after its id (in rust, the crate's name), the host exposes
its children through ctx.config_str/f64/i64/bool (long as they are just scalars).