Skip to content

Latest commit

 

History

History
219 lines (177 loc) · 10.1 KB

File metadata and controls

219 lines (177 loc) · 10.1 KB

wispd architecture (living doc)

This file explains how the project works today and should be updated as implementation changes.

1) D-Bus basics for this project

notify-send and most Linux apps send desktop notifications over D-Bus to:

  • bus name: org.freedesktop.Notifications
  • object path: /org/freedesktop/Notifications
  • interface: org.freedesktop.Notifications

Only one process can own that bus name at a time.

  • If mako/dunst already owns it, wisp-debug/wispd cannot start as server.
  • The notification daemon is the server (name owner).
  • Apps are clients (they call methods like Notify).
  • wispd-monitor is passive and does not own org.freedesktop.Notifications.

2) Workspace layout

crates/
  wisp-types    # shared Rust types/events
  wisp-source   # D-Bus server + notification store + event stream
  wisp-monitor  # shared passive D-Bus monitoring/parsing helpers

bins/
  wispd         # iced + layer-shell frontend with queue policy + popup rendering
  wisp-debug    # debug daemon entrypoint that logs incoming notifications
  wispd-monitor # passive D-Bus monitor for notifications traffic
  wispd-forward # host->VM notification forwarder via SSH

3) Current runtime flow

  1. wisp-debug or wispd starts the daemon process (manually, systemd autostart, or D-Bus activation via org.freedesktop.Notifications.service).
  2. wispd calls WispSource::start_dbus(config).
  3. wisp-source connects to session bus, requests name org.freedesktop.Notifications, and serves interface at /org/freedesktop/Notifications.
  4. Client apps call Notify.
  5. wisp-source converts D-Bus args into wisp_types::Notification.
  6. Notification is inserted/replaced in in-memory store.
  7. wisp-source schedules timeout expiry (if applicable).
  8. wisp-source emits NotificationEvent through tokio::mpsc.
  9. wispd runs wisp-source on a dedicated Tokio runtime thread and forwards events to the UI via a std channel.
  10. wispd applies queue policy (max visible, newest on top, replacement in-place).
  11. wispd opens one layer-shell window per visible notification and reflows their margins for stacking.
  12. For timed notifications, wispd renders a progress edge bar (top/bottom) using elapsed time vs effective timeout.

4) wisp-source responsibilities

Implemented now:

  • Owns notification state (HashMap<u32, StoredNotification>) with generation counters
  • Allocates IDs
  • Replacement semantics:
    • replaces_id == 0: new ID
    • existing replaces_id: replace in place, keep same ID, increment generation
    • missing replaces_id: create new ID
  • Timeout/expiry scheduler
    • expire_timeout > 0: uses requested timeout
    • expire_timeout < 0: uses default_timeout_ms
    • expire_timeout == 0: no automatic expiry
  • Exposes snapshot API (snapshot())
  • Exposes action API (invoke_action(id, action_key))
  • D-Bus methods:
    • Notify
    • CloseNotification
    • GetCapabilities
    • GetServerInformation
  • Declares D-Bus signals:
    • NotificationClosed
    • ActionInvoked
  • Parses core hints (urgency, category, desktop-entry, transient) and preserves unknown hints as debug strings
  • Emits NotificationClosed signal for close paths handled by source (CloseNotification, timeout expiry, action dismiss)
  • Emits ActionInvoked signal when an action is invoked

Not implemented yet:

  • click-to-dismiss behavior in wispd UI
  • richer hint coverage (images/sound/etc beyond current parsed subset)
  • polished visual styling/layout behavior expected from mature daemons

Known bug (tracked):

  • Multi-display focused-output behavior is still unreliable in some compositor setups.
    • Symptom: notifications can appear on a non-focused monitor even with ui.output = "focused".
    • Current behavior is intentionally mako-like: when no popup stack exists, the first popup is compositor-chosen (or uses focused_output_command if configured); while a stack is visible, later popups stay on that same output.
    • wispd clears sticky stack-output state when the popup stack becomes empty or windows are compositor-closed (for example after an output disappears), which avoids getting stuck on a disconnected monitor.
    • wispd also subscribes to Wayland output registry changes and rebuilds visible popup windows only when the removed output could invalidate the current stack binding.
    • This improves mako-style stack stickiness/recovery, but does not fully solve compositor-agnostic focused-output tracking.

5) Types and events

Main shared types in wisp-types:

  • Notification (includes app_icon, actions, hints)
  • NotificationHints (category, desktop_entry, transient, extra)
  • NotificationAction
  • Urgency
  • CloseReason
  • NotificationEvent (Received, Replaced, Closed, ActionInvoked)

Event transport is currently tokio::mpsc (single consumer stream per source instance).

wispd currently applies queue behavior:

  • max visible: 5
  • newest notifications at top
  • replacement updates existing item in-place (keeps slot)
  • close removes item

6) Config surface (current)

Config file is loaded from:

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

source config currently supports:

  • capabilities list (reported by GetCapabilities)
  • default_timeout_ms (used when incoming timeout is negative)
    • if unset, negative incoming timeouts are treated as persistent

ui config currently supports:

  • format string with placeholders ({id}, {app_name}, {summary}, {body}, {urgency})
  • max_visible
  • width
  • height (minimum popup height; windows grow based on formatted content)
  • gap
  • padding
  • font_size
  • font_family (alias: font)
  • anchor
  • output (focused, last-output sticky, none/default, or exact output name like DP-1)
  • focused_output_command (optional shell command override; first stdout line used as focused output name when output = "focused"; if unset, focused uses compositor-picked output for first popup and sticky last-output while stack is visible)
  • margin (top, right, bottom, left)
  • urgency colors (low, normal, critical) plus base background, text, and timeout_progress
  • timeout progress indicator controls:
    • show_timeout_progress
    • timeout_progress_height
    • timeout_progress_position (top / bottom)
  • click action controls:
    • left_click_action (dismiss / invoke-default-action)
    • right_click_action (dismiss / invoke-default-action)

Runtime reload:

  • wispd listens for SIGHUP.
  • On SIGHUP, it reloads config.toml and applies updated UI settings in place.
  • Reload is applied only when config loading passes TOML parsing and basic sanity validation (for example valid anchors, timeout-progress position, colors, and non-zero popup size).
  • If reload validation fails, wispd keeps the current configuration and emits a local critical notification describing the reload failure.
  • Source runtime settings (capabilities, default_timeout_ms) are updated without restarting D-Bus ownership.

7) Testing status

Implemented tests in wisp-source:

  • replacement keeps same ID
  • missing replaces_id allocates a fresh ID
  • replacement resets timeout generation (old expiry does not win)
  • timeout expiry emits Closed(Expired) event
  • negative timeout without configured default remains persistent
  • zero timeout remains persistent (no expiry scheduled)
  • action invoke emits ActionInvoked + Closed(Dismissed)
  • unknown action returns false and emits no extra events
  • invoking actions after replacement targets the current notification generation/actions
  • duplicate action keys and empty/odd action lists are handled safely
  • snapshot reflects replacement and close state
  • closing unknown IDs is a safe no-op
  • hint parsing unit coverage for known fields (urgency, category, desktop-entry, transient)
  • D-Bus integration tests (skip when session bus unavailable):
    • Notify emits received event (including parsed icon/hints)
    • rapid Notify bursts preserve ordering and allocated IDs
    • replace storms over D-Bus converge to one final live notification state
    • CloseNotification during active timeout races results in one final close event/signal
    • CloseNotification emits closed event with ClosedByCall
    • NotificationClosed signal is emitted with expected reason code
    • ActionInvoked signal is emitted for action invocation
    • GetCapabilities returns configured capabilities
    • GetServerInformation returns configured values
    • runtime config updates are reflected in GetCapabilities while server info remains stable

Implemented tests in wispd UI logic:

  • newest notification goes to front
  • replacement keeps its visible slot
  • replacing a hidden-but-still-visible notification does not corrupt visible ordering
  • closing/removal compacts visible UI state correctly
  • stack output policy resets when the last notification/window goes away
  • later notifications retain the current stack output binding while the stack is visible
  • output removal rebuilds visible windows only when the active stack binding is affected
  • config application updates UI settings and source runtime settings
  • applying config while notifications are visible preserves sane popup ordering
  • applying config does not strand windows on stale output bindings
  • placeholder formatting, icon-path helpers, timeout normalization, and click action routing have unit coverage

8) How to run debug daemon

cargo run -p wisp-debug

In another terminal:

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

wisp-debug also accepts stdin commands:

  • list
  • close <id>
  • action <id> <action-key>
  • help
  • quit

If startup fails with "name already taken on the bus", stop the currently running notification daemon first.

wispd-forward uses BecomeMonitor to observe host Notify method calls and replays them in the VM via SSH (notify-send on guest). This allows host mako to stay active while testing wispd in a VM.

wispd requires a Wayland session and Wayland runtime libraries. If you see NoWaylandLib, run inside nix develop, verify WAYLAND_DISPLAY is set, and ensure Wayland runtime libs are available in the runtime environment.