No description
  • C 96.4%
  • Makefile 3.6%
Find a file
2026-07-26 10:46:20 +11:00
.gitignore rewrite as a one-shot c99 app stacking via a _DWM_TOAST property 2026-07-25 22:19:59 +11:00
config.h progress bar along the bottom; toasts down to 1s 2026-07-26 10:46:20 +11:00
dwm-toast.c progress bar along the bottom; toasts down to 1s 2026-07-26 10:46:20 +11:00
Makefile rewrite as a one-shot c99 app stacking via a _DWM_TOAST property 2026-07-25 22:19:59 +11:00
README progress bar along the bottom; toasts down to 1s 2026-07-26 10:46:20 +11:00

dwm-toast — standalone toast overlay for dwm
============================================

A small Xlib app, sibling to dwm-flash / dwm-range / dwm-grid: a solid gruvbox
rectangle with an optional bold title over a wrapped body, in Iosevka Term
Slab, in the top-right corner of the screen.

It is completely decoupled from dwm: it holds no shared state with it, links
against no dwm code, and reads no dwm properties. Its window is
override-redirect, so dwm never manages, tiles or focuses it.

Like its siblings it is one-shot. Something spawns it, it maps a window,
sleeps, and exits. There is no daemon, no listening socket, and nothing to
keep running.

Build & run
-----------
  make                  # cc -std=c99 -pedantic -Wall -Wextra -Os
  make install          # installs to /usr/local/bin (override PREFIX=)

Requires libX11 and libXft.

Showing a toast
---------------
  dwm-toast <title> <text...>

The title may be empty, which omits the title line. The remaining arguments
are joined with single spaces to form the body:

  dwm-toast "BUILD FAILED" "3 tests in parser_test.zig are red"
  dwm-toast "" "deploy ok"

In practice the caller is tdm, whose spawn sink expands a route's argv
template — its "{}" splits the frame value on spaces, which is exactly why the
body is spelled as trailing arguments here:

  .{ .topic = .build_status, .from = .server, .when = .active,
     .sink = .{ .spawn = &.{ "dwm-toast", "BUILD", "{}" } } },

From a script it is just a command:

  notify() { dwm-toast "$1" "$2"; }
  make 2>&1 | tail -1 | xargs -I{} notify BUILD "{}"

Stacking
--------
Toasts stack downward from the top-right, each hugging its own text width up
to maxw. A starting toast takes the topmost vertical slot that no live toast
occupies.

The stack is X's state, not a shared list: each toast advertises its own
[y, y+h) in a _DWM_TOAST property (two CARDINALs) on its own window, and a
starting toast reads them off the root's children. There is consequently
nothing to clean up and nothing to leak — the property lives on the window,
the window dies with the process, so even a SIGKILLed toast frees its slot.
Claiming a slot is grabbed (XGrabServer) so two toasts spawned at the same
instant cannot both read the same free y.

Nothing slides up when a toast above expires; the gap is left. At the usual
zero-or-one toasts that case barely arises, and it keeps positions stable
while you are reading. Sliding would mean every toast process watching root
for DestroyNotify — a small window manager in each one.

A new toast that finds no free slot fitting on screen kills the topmost one
and takes its place, rather than running off the bottom.

Text
----
Text wraps on spaces at maxw, honours explicit newlines (blank lines
included), and hard-breaks words too long to fit — at codepoint boundaries, so
a glyph is never cut in half.

Drawing
-------
The whole toast is painted once into a pixmap, which becomes the window's
background pixmap. The X server then repaints it on expose, so this program
reads no X events at all: it maps the window, holds it for toastms, and exits.
No event loop, no compositing, no GPU.

Along the bottom sits a barh-px progress bar that fills orange from left to
right over toastms. Each frame repaints only that strip of the pixmap and then
XClearArea's the matching strip of the window, so the pixmap stays the single
source of truth and expose repaints keep working with no extra code. The strip
is only pushed when its pixel width actually changes, which for a 400px toast
is a few hundred requests over its whole life; progress is read from
CLOCK_MONOTONIC rather than accumulated sleeps, so the bar lands at full width
exactly when the toast goes away. Set barh = 0 to drop the bar and go back to a
single sleep.

Configuration
-------------
Edit config.h (fonts, gruvbox colors, padding, max width, lifetime, gap, bar
height and frame rate) and rebuild, exactly as its C siblings do.

Limitations
-----------
No font fallback: one XftFont per weight is loaded, so codepoints Iosevka Slab
lacks (CJK, most emoji) render as tofu boxes. dwm's drw.c does FcCharSet-based
fallback; that machinery is deliberately absent here.

One lifetime for every toast (toastms), since the caller passes no duration.

Geometry comes from the whole X screen, not from the focused Xinerama monitor:
on a multi-head setup toasts land in the top-right of the rightmost screen.
dwm-range makes the same simplification.

No dismiss-on-click, no actions, no icons, no libnotify/D-Bus. It is not a
notification daemon and does not implement org.freedesktop.Notifications;
point dunst at that if you want one.

History
-------
This was a Zig daemon that received toasts over HTTP (JSON body, port 7777).
That made sense while it had to receive messages itself; once tdm existed, the
HTTP server was a second message bus inside a message bus — and its only
caller was tdm, posting the one required field. See the first commit for that
version.