1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
|
# Notification Images
The daemon and the renderers gain content images: a screenshot or an
application image attached to a notification, shown large in the balloon, plus
inline images inside the body markup, plus resolution of app icons given as
theme names.
This extends the shipped daemon design
(`2026-09-15-notification-daemon-design.md`) and the renderers built from
`../plans/2026-09-15-notification-renderers.md`.
## Why
The daemon currently reads only the `app_icon` parameter. It ignores every
image hint, so content images never arrive. Two live consumers show the gap:
opencode its notifier passes its logo with notify-send --icon
grimblast it passes the screenshot with notify-send -i
libnotify 0.8.8 splits two flags that were once one: `-i/--icon` is the content
image (it lands in the `image-path` hint) and `-n/--app-icon` is the
`app_icon` parameter. Both consumers use the content image. The daemon drops
that hint, so opencode shows no logo and a screenshot shows nothing.
Separately, `app_icon` is only usable when it is an absolute path. Apps that
send a theme name (the spec's other allowed form) render nothing, because the
renderer builds `file://<name>`.
## What it is not
It is not a change to the daemon's or renderer's suppression, timeout, replace
or history behaviour. It is not animated images, multiple images, progress
bars, or body hyperlink handling. It is not a resolver for `desktop-entry`.
## The freedesktop contract
The specification defines one image per notification. An implementation that
can display both the app icon and the image shows `app_icon` as the icon and
picks the image in this order:
1. image-data (raw pixels, a (iiibiiay) struct)
2. image-path (a URI or a theme icon name)
3. icon_data (deprecated, the same struct as image-data)
An implementation that can show only one image picks from image-data,
image-path, app_icon, then icon_data. The daemon here shows both, so it uses
the first order and keeps `app_icon` as the icon.
`image-data` and `icon_data` are a D-Bus structure `(iiibiiay)`:
width (i) width in pixels
height (i) height in pixels
rowstride (i) bytes between row starts
has_alpha (b) whether there is an alpha channel
bits_per_sample (i) always 8
channels (i) 4 with alpha, 3 without
data (ay) pixels, RGB byte order
`image-path`, and `app_icon`, are each either a `file://` URI or a name in a
freedesktop icon theme. A name must be resolved against a theme.
## The daemon
### Hints
`Notify` gains hint parsing beside the existing urgency and stack-tag reads:
- `image-data` and the deprecated `icon_data` (struct): decode to an image.
- `image-path` (string): a `file://` URI, an absolute path, or a theme name.
`image-data` wins when both a data and a path hint are present, per the
priority above. The underscore spelling `image_data`, which older libnotify
sent, is accepted as an alias.
### Materialisation and lifecycle
A raw `image-data` is decoded in Go and written as a PNG to
`$XDG_RUNTIME_DIR/notifyd/img/<id>.png`. A replaced notification reuses its id
and overwrites the same file. The file is removed when the notification leaves
the live queue: on dismiss, on eviction, and on expiry. Only the balloon shows
an image, and the drawer row does not, so nothing needs it once the balloon is
gone. This bounds the directory to the live balloons.
An `image-path` is the client's file and is published as-is; the daemon never
deletes it. A theme name is resolved to a file first. A `file://` URI is
converted to a path.
### Capabilities
`GetCapabilities` adds `body-images`, which is the spec's token for inline
image support. It keeps `actions`, `body-markup`, `icon-static` and
`persistence`.
### Icon and image theme-name resolution
`app_icon` and `image-path` values without a `/` are theme names and are
resolved to a file. A value that is already a path or a `file://` URI is used
as-is.
The theme is read from qt6ct, `~/.config/qt6ct/qt6ct.conf`, key `icon_theme`.
On this machine that is `Material-Black-Plum-Suru`, and it is authoritative;
GTK3, gsettings and qt6ct agree, and the one dissent (GTK4's `breeze-dark`)
has been corrected to match. If qt6ct has no value, fall back to the GTK3
setting `gtk-icon-theme-name` (or `gsettings`), then `hicolor`.
Lookup, once a theme name is known:
1. Search `$XDG_DATA_HOME/icons/<theme>`, then each `$XDG_DATA_DIRS/icons/<theme>`
in order.
2. Within the theme, prefer `apps/scalable`, then the largest available size
under `apps/`.
3. Follow the theme's `Inherits` chain from its `index.theme`.
4. Fall back to `hicolor`.
5. If nothing matches, leave the value empty, which renders no image. This is
today's behaviour and stays the honest outcome.
## The published contract
`Popup` gains one field:
{
"id": 57,
"app": "grimblast",
"summary": "Screenshot of Area",
"body": "...",
"urgency": "normal",
"icon": "", // app identity, a resolved path
"image": "/run/user/1000/notifyd/img/57.png", // content image, empty if none
"actions": [],
"created": 1758000000000,
"expires": 1758000010000
}
`icon` keeps its meaning and its consumers. `image` is new and empty when the
notification has none. History objects carry the same field; history rows do
not render it, so a stale path there is inert.
## The renderers
### Balloon
When `image` is non-empty, the balloon shows it below the text block,
`Image.PreserveAspectFit`, the balloon's width, capped at 240px tall. The
32px `icon` slot is unchanged, and both can appear together. The balloon grows
to fit, exactly as it does for a long body.
### Inline images in the body
The body is already rendered as `Text.RichText`, and Qt's rich text engine
loads `<img>` from a local path or a `file://` URI (verified: a 200px
`<img>` raised the text's `contentHeight` from 52 to 307). No new rendering
code is needed for the common case.
Policy: only local sources render. Before display, the renderer strips any
`<img>` whose `src` begins with `http:` or `https:`, so a notification from an
untrusted sender cannot make the shell fetch a URL. Remote inline images are
not shown. Sources that are absolute paths or `file://` URIs are left alone.
The existing body line caps still apply: three lines in the balloon, two in a
drawer row.
### Drawer rows
No image. The reserved space and the history page stay text only, as they are
today.
## Privacy
A notification is untrusted input. The only new outward action the design can
cause is a network fetch, and it is closed by the inline policy above: remote
image sources are stripped, never fetched. The daemon writes a decoded image
only under its own runtime directory.
## Verification
Daemon (Go, table tests, no bus or clock):
- image hint parsing: `image-data` beats `image-path`; `icon_data` and
`image_data` aliases are read; a `file://` URI becomes a path.
- `image-data` decode to PNG for 3- and 4-channel data with a rowstride.
- theme-name resolution against a fake icon directory: a plain name, a name
found only through `Inherits`, a missing name, and the largest-size
preference.
`test-notifyctl.sh` is extended if the contract change reaches it.
End to end, by hand:
- a grimblast screenshot and an opencode event each draw a large preview.
- `notify-send -n firefox` draws the themed Firefox icon.
- a `gdbus` call sending `image-data` draws the image.
- a body containing `<img src="https://example.org/x.png">` draws no image and
makes no request.
- the renderer smoke checks stay clean.
## Out of scope
- Icon caches and theme re-scan; a name is resolved per notification.
- `desktop-entry` resolution.
- Animated images, more than one image, an image in a drawer row.
- Progress bars and body hyperlink handling, unchanged deferrals.
|