Posted on :: README :: 678 Words

wispd

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 flows
  • wispd-monitor: passive D-Bus monitor for notifications traffic (does not own org.freedesktop.Notifications)
  • wispd-forward: forwards host notifications into a VM over SSH (keeps host daemon like mako active)
  • wisp-random: sends randomized test notifications over org.freedesktop.Notifications
  • Reusable crates:
    • wisp-source (D-Bus server + notification lifecycle)
    • wisp-types (shared notification/event types)

Table of contents

  • Current status
  • freedesktop.org Notifications API coverage
    • Core D-Bus methods
    • D-Bus signals
    • Behavior/details
  • Requirements
  • Quick start
      1. Run debug daemon (easiest first test)
      1. Run UI daemon
      1. Run passive monitor (no name ownership)
      1. Forward host notifications into VM (while keeping host mako)
  • Configuration
  • Home Manager module
  • Niri + wispd MicroVM (QEMU)
  • Development

Current status

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

freedesktop.org Notifications API coverage

Checklist for org.freedesktop.Notifications support right now:

Core D-Bus methods

  • Notify
  • CloseNotification
  • GetCapabilities
  • GetServerInformation

D-Bus signals

  • NotificationClosed
  • ActionInvoked

Behavior/details

  • 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)

Requirements

  • Linux Wayland session (for wispd UI)
  • Session D-Bus
  • No other daemon owning org.freedesktop.Notifications (e.g. stop mako/dunst first)

Quick start

1) Run debug daemon (easiest first test)

cargo run -p wisp-debug

Send randomized test notifications

cargo 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-only

In another terminal:

notify-send "hello" "from notify-send"

2) Run UI daemon

cargo run -p wispd

If Wayland libraries are missing, use the flake dev shell:

nix develop
cargo run -p wispd

3) Run passive monitor (no name ownership)

cargo run -p wispd-monitor

Or via flake app:

nix run .#wispd-monitor

4) Forward host notifications into VM (while keeping host mako)

# defaults: wisp@127.0.0.1:2222 and remote notify-send
cargo run -p wispd-forward

# or
nix run .#wispd-forward

Useful 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)

Configuration

Config file path:

  • $XDG_CONFIG_HOME/wispd/config.toml
  • fallback: ~/.config/wispd/config.toml

Runtime reload:

  • Send SIGHUP to wispd to 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 key default)
[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 = 13

Home Manager module

This 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)

Niri + wispd MicroVM (QEMU)

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
  • wispd started as a systemd --user service from /work/wispd/target/debug/wispd
  • host workspace is shared into the guest at /work/wispd via a relative 9p share (source = ".")
  • alacritty installed (and exported as TERMINAL)
  • SSH enabled in guest, forwarded as host 127.0.0.1:2222 -> guest:22 (for wispd-forward)

Run it:

nix run .#wispd-microvm

Inside 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 wispd

Development

Rust checks:

cargo fmt
cargo clippy --workspace --all-targets
cargo test --workspace

Nix package build (uses ipetkov/crane for faster incremental dependency reuse):

nix build .#wispd

MicroVM 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-microvm

MicroVM 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-forward defaults are aligned with the bundled microvm (127.0.0.1:2222, user wisp, password wisp).
  • 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.