A library-first Rust toolkit (hytte) for composing GTK4 + libadwaita +
layer-shell desktop shells, and trollshell — the personal shell built on it,
targeting the Niri compositor.
"Composable, not configurable": there is no config DSL. The shell is wired up
in plain Rust in trollshell/src/main.rs. hytte services are thin async
clients to existing system daemons (systemd-networkd, BlueZ, PipeWire, UPower,
logind, niri-ipc, …), so persistent state lives in those daemons and a
cargo run restart of the shell reconnects without losing system state.
A top-edge bar on every Niri monitor — workspaces, window list, media
controls, system tray, network/Wi-Fi/VPN, bluetooth, volume/mic/brightness,
battery, CPU/memory/GPU/disk stats, clock, and notification/settings/power
chips. Clicking a chip opens a slide-out drawer with a matching panel.
Plus a left sidebar, on-screen displays (OSD), a notification daemon +
toasts, password prompts, and out-of-process widget plugins (a clock, a
kaomoji pet, a timer, a terminal, transit departures, weather, …) that mount
into the bar and sidebar over a local socket. (Polkit authentication is
delegated to a standalone agent — see the flake / etc/.)
trollshell ships no in-shell lock screen: an idle → dim → lock → suspend
timeline runs natively in-process, but the actual locking is delegated to
swaylock via logind's Lock signal (see etc/README.md).
A separate windowed companion app, trollshell-control-center, handles
heavier settings/management (picking the weather place, toggling plugin
units, …) over a D-Bus link to the running shell — gnome-control-center-style,
never linked into the shell itself.
See docs/superpowers/specs/2026-04-24-hytte-trollshell-design.md for
the founding design and the dated specs/plans alongside it for each feature.
Development uses the Nix flake's devShell, which provides the Rust toolchain
and the GTK/PipeWire/EDS native deps and sets the env the build and
runtime need (libclang for bindgen, icon-theme + GSettings-schema paths).
.envrc is use flake, so direnv enters it
automatically; otherwise run nix develop first.
cargo build --release -p trollshell # build the binary (inside the devShell)
cargo run -p trollshell # run it — requires a live Niri session
nix build # build the packaged binary (.#trollshell)trollshell is a real Wayland shell: it connects to $NIRI_SOCKET and to
live system daemons, so it only does anything meaningful inside a Niri
session.
The flake exposes the package (packages.default) plus NixOS and home-manager
modules. To try the binary without installing it (still needs a live Niri
session):
nix run github:vibec0re/trollshellNixOS — add the flake as an input and import its module:
{
inputs.trollshell.url = "github:vibec0re/trollshell";
# …then in your nixosConfigurations' modules list:
imports = [ inputs.trollshell.nixosModules.default ];
programs.trollshell.enable = true;
}This installs the package — and, by default, the trollshell-control-center
companion app alongside it (programs.trollshell.controlCenter.enable,
default true) — plus a bundle of recommended-but-optional daemons — the
agent-name D-Bus policy, the polkit agent, UPower, power-profiles-daemon,
geoclue, and the GNOME Online Accounts → evolution-data-server stack (with
gnome-control-center to add accounts) that the calendar + tasks panels read
from — gated behind programs.trollshell.enableRecommendedServices (default
true). Set it to false for a bare bar, where each chip simply hides when
its daemon is absent.
Calendars/tasks have no in-shell account UI: add an account in Settings →
Online Accounts (gnome-control-center online-accounts) and trollshell reads
it back from evolution-data-server — see
etc/calendar/README.md.
Home-manager — import the module to run the shell as a user service:
{
inputs.trollshell.url = "github:vibec0re/trollshell";
imports = [ inputs.trollshell.homeModules.default ];
programs.trollshell.enable = true;
}When home-manager runs as a NixOS module, the NixOS module wires the
home-manager one in automatically, so per-user programs.trollshell settings
just work. Non-NixOS session integration (systemd user units, niri binds,
kanshi, cliphist, …) ships under etc/ —
see etc/README.md. (The idle → dim → lock → suspend pipeline
is native to trollshell — #204 retired swayidle — so it needs no etc/
config; swaylock's own PAM stack, wired separately, is what actually
authenticates an unlock — see etc/README.md's "Idle & screen locking".)
The Appearance drawer page writes the picked image to
~/.config/trollshell/wallpaper.path, then tells a wallpaper daemon to reload.
wallpaper.backend picks the daemon and the reload command from a single enum
value (so two daemons can never run at once):
programs.trollshell.wallpaper.backend = "swaybg"; # the default — today's behavior
# or "awww" / "none"swaybg(default): the bundledswaybgunit; the picker restarts it. No reload command needed.awww: the swww successor (upstream renamed swww → awww at 0.12; swww itself is deprecated). Setswallpaper.reloadCommandtoawww img {}. The daemon is run by home-manager'sservices.awww; the NixOS module only exports the reload command (a NixOS-only user without home-manager runs the awww daemon themselves). Needs a home-manager channel withservices.awww(0.12+) — otherwise the build fails with a clear assertion.none: trollshell manages no daemon; the picker only writes the path file (and runsreloadCommandif you set one). Use this to wire your own daemon.
You can still set wallpaper.reloadCommand by hand to override the per-backend
default; a {} in it is replaced with the chosen path (shell-quoted, not a
$VAR — sessionVariables would expand it away at login):
programs.trollshell.wallpaper.reloadCommand = "awww img {}";The legacy home-manager programs.trollshell.swaybg.enable still works as a
standalone swaybg toggle (gently deprecated in favor of backend = "swaybg").
crates/hytte-reactive/—Servicetrait, thread-local handle registry, process-wide tokio runtime, and thebind*GTK↔futures-signalshelpers.crates/hytte-ui/—App,Bar,LayerWindow,Popup,Monitorprimitives (layer-shell) + the default stylesheet.crates/hytte-bus/— shared D-Bus capability layer (pooled session/system connections;call/property/proxy/signals/own_namebuilders).crates/hytte-services/— async clients to system daemons exposed as services (clock, niri, pipewire, networkd/resolved/wifi, bluetooth, upower, mpris, tray, notifications, calendar, sensors, idle_notify, …).crates/hytte-ecal/— hand-written FFI to evolution-data-server (calendar); the only crate allowedunsafe.crates/hytte-ai-providers/— shared OpenAI-compatible chat client + key-file loader for the plugins that talk to an LLM (e.g. the pet plugin).crates/hytte/— umbrella re-export crate (bus,reactive,services,ui) + aprelude.crates/hytte-plugin-proto/+crates/hytte-plugin/— the plugin side (out-of-process, never linked into the shell): a GTK-free wire protocol and the Rust plugin runtime SDK a plugin binary depends on.crates/hytte-plugin-*/(nine binaries:clock-demo,bar-clock-demo,pet,preem-demo,timer,terminal,caw,departures,weather) — the plugin binaries themselves, each its own systemd user unit.crates/trollshell-control-center/— the external settings/management companion app (see "What it does" above); a separate GTK4/libadwaita binary, not linked into the shell.trollshell/— the binary:widgets/(bar chips),panels/(drawer pages),overlays/(frame, OSD, notifications, dialogs, sidebar),modal.rs(the drawer),plugins.rs(the plugin host transport),control.rs(the control-center's D-Bus endpoint),commands.rs(the keybind-facing command surface),components/(shared building blocks).
CLAUDE.md documents the architecture (the handle/work reactive split, the
per-service pattern, the bus layer, the strict lint gate) in more depth.
RUST_LOG=hytte_services=debug,trollshell=debug cargo run -p trollshell