wispd is an experimental Wayland notification daemon for org.freedesktop.Notifications.
It includes:
wispd: layer-shell popup UI (one popup window per notification)wisp-debug: CLI/debug daemon to inspect incoming notifications and test close/action flowswispd-monitor: passive D-Bus monitor for notifications traffic (does not ownorg.freedesktop.Notifications)wispd-forward: forwards host notifications into a VM over SSH (keeps host daemon likemakoactive)wisp-random: sends randomized test notifications overorg.freedesktop.Notifications- Reusable crates:
wisp-source(D-Bus server + notification lifecycle)wisp-types(shared notification/event types)
- Current status
- freedesktop.org Notifications API coverage
- Requirements
- Quick start
- Configuration
- Home Manager module
- Niri + wispd MicroVM (QEMU)
- Development
Early-stage but functional:
- Implements
Notify,CloseNotification,GetCapabilities,GetServerInformation - Supports replacement (
replaces_id), timeouts, actions, and D-Bus close/action signals - Configurable popup layout/colors/format via TOML
- Optional timeout progress bar (top/bottom edge) for timed notifications
Checklist for org.freedesktop.Notifications support right now:
-
Notify -
CloseNotification -
GetCapabilities -
GetServerInformation
-
NotificationClosed -
ActionInvoked
- Replacement via
replaces_id - Action invocation from UI/debug path
- Timeout handling (
> 0,0, and< 0+ configurable default timeout) - Basic hints parsing:
urgency,category,desktop-entry,transient - [~] Extra hints preserved as debug strings (not fully interpreted)
- Rich hints/attachments (images, sound, progress, etc.)
- Markup rendering
- Icon rendering in UI (path/file URI icons)
- Linux Wayland session (for
wispdUI) - Session D-Bus
- No other daemon owning
org.freedesktop.Notifications(e.g. stopmako/dunstfirst)
cargo run -p wisp-debugcargo run -p wisp-random
cargo run -p wisp-random -- --count 10 --interval-ms 500
cargo run -p wisp-random -- --loop --interval-ms 750 --actions-always --icons-never
cargo run -p wisp-random -- --replace-id 1 --persistent-onlyIn another terminal:
notify-send "hello" "from notify-send"cargo run -p wispdIf Wayland libraries are missing, use the flake dev shell:
nix develop
cargo run -p wispdcargo run -p wispd-monitorOr via flake app:
nix run .#wispd-monitor# defaults: wisp@127.0.0.1:2222 and remote notify-send
cargo run -p wispd-forward
# or
nix run .#wispd-forwardUseful env vars:
WISPD_FORWARD_SSH_HOST(default:127.0.0.1)WISPD_FORWARD_SSH_PORT(default:2222)WISPD_FORWARD_SSH_USER(default:wisp)WISPD_FORWARD_SSH_PASSWORD(default:wisp)WISPD_FORWARD_NOTIFY_SEND(default:notify-send)WISPD_FORWARD_SSH_STARTUP_WAIT_SECS(default:60)WISPD_FORWARD_SSH_STARTUP_POLL_MS(default:500)
Config file path:
$XDG_CONFIG_HOME/wispd/config.toml- fallback:
~/.config/wispd/config.toml
Runtime reload:
- Send
SIGHUPtowispdto reload config without restarting:pkill -HUP -x wispd- or
systemctl --user kill -s HUP wispd
Example:
left_click_action / right_click_action allowed values:
"dismiss""invoke-default-action"(invokes action keydefault)
[source]
default_timeout_ms = 5000
capabilities = ["body", "actions"]
[ui]
format = "{app_name}: {summary}\n{body}"
max_visible = 5
width = 420
height = 64
gap = 8
padding = 10
font_size = 15
# `font` is an alias for `font_family`
font = "sans-serif"
show_icons = true
max_icon_size = 32
anchor = "top-right"
# focused (recommended), last-output (sticky), any/none/default, or exact output name (e.g. "DP-1")
output = "focused"
# optional override: command that prints the currently focused output name (first line)
# when unset, "focused" uses compositor-picked output for first popup and sticky last-output for stack
# focused_output_command = "niri msg -j outputs | jq -r '.[] | select(.is_focused) | .name'"
show_timeout_progress = true
timeout_progress_height = 3
timeout_progress_position = "bottom"
left_click_action = "dismiss"
right_click_action = "invoke-default-action"
[ui.margin]
top = 16
right = 16
bottom = 16
left = 16
[ui.colors]
low = "#6aa9ff"
normal = "#7dcf7d"
critical = "#ff6b6b"
background = "#1e1e2ecc"
text = "#f8f8f2" # fallback text color
timeout_progress = "#f8f8f2"
[ui.text.app_name]
color = "#a89984"
font_size = 15
[ui.text.summary]
color = "#f8f8f2"
font_size = 15
[ui.text.body]
color = "#f8f8f2"
font_size = 15
[ui.buttons]
text_color = "#ebdbb2"
background = "#3c3836"
border_color = "#665c54"
hover_background = "#504945"
hover_text_color = "#fbf1c7"
# optional: defaults to ui.font
font = "Recursive Mono Casual Static"
# optional: defaults to ui.font_size
font_size = 15
# optional: defaults to (font_size or ui.font_size - 2)
close_font_size = 13This flake exports homeManagerModules.wispd.
Example:
{
inputs.wispd.url = "github:dmnt/wispd";
outputs = { self, nixpkgs, home-manager, wispd, ... }: {
homeConfigurations.me = home-manager.lib.homeManagerConfiguration {
pkgs = import nixpkgs { system = "x86_64-linux"; };
modules = [
wispd.homeManagerModules.wispd
({ ... }: {
services.wispd.enable = true;
services.wispd.rustLog = "info,wispd=debug,wisp_source=debug";
# services.wispd.package = wispd.packages.${pkgs.system}.wispd; # optional override
})
];
};
};
}The module creates a systemd --user service (wispd.service) and sets a runtime
LD_LIBRARY_PATH/PATH for Wayland and libxkbcommon dependencies.
It also provides a D-Bus activation service (org.freedesktop.Notifications) via dbus.packages
and wires wispd.service as a D-Bus service (BusName=org.freedesktop.Notifications).
Useful module options:
services.wispd.autostart = true|false(default:true)services.wispd.dbusActivation.enable = true|false(default:true)
A ready-to-run MicroVM configuration is included via github:microvm-nix/microvm.nix.
What it configures:
- QEMU MicroVM with graphics enabled
- Niri compositor started at boot via
greetd wispdstarted as asystemd --userservice from/work/wispd/target/debug/wispd- host workspace is shared into the guest at
/work/wispdvia a relative9pshare (source = ".") alacrittyinstalled (and exported asTERMINAL)- SSH enabled in guest, forwarded as host
127.0.0.1:2222 -> guest:22(forwispd-forward)
Run it:
nix run .#wispd-microvmInside the VM:
- user:
wisp - graphical login: passwordless (auto-login via
greetd) - SSH password (for
wispd-forward):wisp
Test notifications in alacritty:
notify-send "hello" "from wispd microvm"Hot-reload-ish dev loop (no VM reboot):
# host
cargo build -p wispd
# guest
systemctl --user restart wispdRust checks:
cargo fmt
cargo clippy --workspace --all-targets
cargo test --workspaceNix package build (uses ipetkov/crane for faster incremental dependency reuse):
nix build .#wispdMicroVM development / validation:
# evaluate exposed outputs
nix flake show
# dry-run build of the runnable qemu microvm package
nix build .#wispd-microvm --dry-run
# run the VM
nix run .#wispd-microvmMicroVM config source:
flake.nix(inputs +nixosConfigurations.wispd-microvm+ app/package outputs)nix/microvm/wispd-microvm.nix(Niri/greetd/wispd/alacritty VM config)
Dev convenience setup:
wispd-forwarddefaults are aligned with the bundled microvm (127.0.0.1:2222, userwisp, passwordwisp).- Forwarder startup polls until VM SSH is reachable, so it can be started before or during VM boot.
See docs/ARCHITECTURE.md for implementation details and current behavior.