commit a7d0eec3224fe452ebbbd0d832a95c9b5240fe5b
parent 74e79f4d45fca3c8a802ca44cb0daebd225262c8
Author: Matthias Schoth <mschoth@gmail.com>
Date: Sat, 1 Aug 2026 02:37:43 +0200
[st][patches][background-image] Port to 0.9.3 and major overhaul
The new version loads the image via a server-side pixmap published in
the _ST_BACKGROUND_IMAGE root window property by the companion stbg
tool. The background is shared between all st instances and can be
updated live without restarting the terminal.
Includes the st-background-image-0.9.3.diff download as well as links
to the legacy 0.8.4/0.8.5 patches and their signal-reload variant.
Diffstat:
2 files changed, 447 insertions(+), 37 deletions(-)
diff --git a/st.suckless.org/patches/background_image/index.md b/st.suckless.org/patches/background_image/index.md
@@ -4,60 +4,79 @@ background image
Description
-----------
-Draws a background image in place of the defaultbg color.
+Draws a background image in place of the `defaultbg` color. Instead of
+loading an image file itself, st uses the pixmap published by the
+companion program `stbg` in the `_ST_BACKGROUND_IMAGE` root window
+property. Every st instance tiles the same server-side pixmap, so the
+image data is held in memory only once and all instances stay in sync.
-Notes
+Download
+--------
+
+* [st-background-image-0.9.3.diff](st-background-image-0.9.3.diff)
+
+Usage
-----
-The path to the image file has to be configured in `config.h` using the variable
-`bgfile` (patch modifies `config.def.h`, changes made there need to be ported to
-`config.h` if it already exists). The image format is expected to be
-[farbfeld](//tools.suckless.org/farbfeld). In case the background image
-is smaller than the window size the background will be tiled.
+The background image is set by the separate `stbg` tool, which reads a
+[farbfeld](//tools.suckless.org/farbfeld) image and publishes it on the
+root window:
+
+ cc -o stbg stbg.c -lX11
+ stbg wallpaper.ff
+ stbg - < wallpaper.ff
+
+Once the image is published, every st started afterwards tiles it across
+its window. Re-running `stbg` with a new image updates all running st
+instances immediately; there is no need to restart or reload them.
Pseudo Transparency
-------------------
-The variable `pseudotransparency` enables functionality which fixes the
-coordinates of the background image to the screen origin. This emulates the
-effect of transparency without the need for an *X composite manager*.
+The variable `pseudotransparency` in `config.def.h` fixes the
+coordinates of the background image to the screen origin. This emulates
+the effect of transparency without the need for an *X composite
+manager*.
-*Hint*: With the use of [farbfeld utilities](http://zzo38computer.org/fossil/farbfeld.ui/)
-effects can be applied to the desktop background in an automated fashion.
-Pictured below is an example of the result of a darken and blur operation
-invoked with the following command:
+*Hint*: Because image processing is done outside of st, effects can be
+applied to the desktop background in an automated fashion with external
+tools. Pictured below is an example of the result of a darken and blur
+operation using [farbfeld utilities](https://web.archive.org/web/20211205013032/http://zzo38computer.org/fossil/farbfeld.ui/index):
- jpg2ff < wallpaper.jpg | ff-border e 50 | ff-bright rgba 0 0.5 1 | ff-blur 50 15 > st_wallpaper.ff
+ jpg2ff < wallpaper.jpg | ff-border e 50 | ff-bright rgba 0 0.5 1 | ff-blur 50 15 | stbg -

-Download
---------
+Legacy versions
+---------------
+
+Older versions of this patch loaded the farbfeld image directly inside
+st. They still work but have several shortcomings that are fixed by the
+0.9.3 version above:
+
+* **Per-instance decoding.** Each st instance read and decoded the
+ image file from disk and created its own copy of the pixmap. With
+ several terminals open the same image was held in memory multiple
+ times.
+* **No live updates.** The background was only loaded at startup.
+ Updating it required the *signal reload* hack: sending a `USR1`
+ signal to every st process (`pidof st | xargs kill -s USR1`) after
+ regenerating the image file, assuming the extra
+ `st-background-image-signal-reload-0.8.5.diff` patch had been
+ applied on top.
+* **Recompilation to change the image.** The image path was baked in
+ at build time via the `bgfile` variable in `config.h`.
+* **File format parsing inside the terminal.** The farbfeld loading
+ code, including `arpa/inet` byte-swapping, lived in `x.c`, tying the
+ terminal to a single image format.
+
+Legacy downloads:
* [st-background-image-0.8.4.diff](st-background-image-0.8.4.diff)
* [st-background-image-0.8.5.diff](st-background-image-0.8.5.diff)
-
-Signal Reloading
-----------------
-
-Apply the following patch on top of the previous to enable reloading the
-background image when a USR1 signal occurs:
-
* [st-background-image-signal-reload-0.8.5.diff](st-background-image-signal-reload-0.8.5.diff)
-If you use the [xresources with signal reloading](//st.suckless.org/patches/xresources-with-reload-signal)
-patch ignore the patch above and simply add these two lines to the beginning of
-the `reload` function located in the file `x.c`:
-
- XFreeGC(xw.dpy, xw.bggc);
- bginit();
-
-*Hint*: You can send a USR1 signal to all st processes with the following
-command:
-
- pidof st | xargs kill -s USR1
-
Authors
-------
-* Matthias Schoth - <mschoth@gmail.com>
+* Matthias Schoth - <mschoth@gmail.com>
diff --git a/st.suckless.org/patches/background_image/st-background-image-0.9.3.diff b/st.suckless.org/patches/background_image/st-background-image-0.9.3.diff
@@ -0,0 +1,391 @@
+From 5f070227058901afd7525ffe6363c93faf163ef0 Mon Sep 17 00:00:00 2001
+From: Matthias Schoth <mschoth@gmail.com>
+Date: Fri, 31 Jul 2026 23:50:51 +0200
+Subject: [PATCH] Add background image support via root window pixmap
+
+Add a background image to st by tiling a server-side pixmap set as the
+_ST_BACKGROUND_IMAGE root window property. st watches the root window
+for property changes, so re-running the stbg setter updates all running
+st instances immediately.
+
+The pixmap is shared between all instances and no composite manager is
+required. A pseudotransparency config option fixes the tile coordinates
+to the screen origin so the terminal backgrounds line up with the
+wallpaper behind the window.
+
+stbg reads a farbfeld image from a file or stdin (using -) and sets the
+property. It is not built by default; compile separately with:
+
+ cc -o stbg stbg.c -lX11
+---
+ config.def.h | 6 +++
+ stbg.c | 122 ++++++++++++++++++++++++++++++++++++++++++++++++
+ x.c | 128 +++++++++++++++++++++++++++++++++++++++++++++++----
+ 3 files changed, 246 insertions(+), 10 deletions(-)
+ create mode 100644 stbg.c
+
+diff --git a/config.def.h b/config.def.h
+index 2cd740a..3d98c87 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -8,6 +8,12 @@
+ static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true";
+ static int borderpx = 2;
+
++/*
++ * pseudo transparency fixes coordinates to the screen origin
++ * requires _ST_BACKGROUND_IMAGE root window property (set via stbg)
++ */
++static const int pseudotransparency = 0;
++
+ /*
+ * What program is execed by st depends of these precedence rules:
+ * 1: program passed with -e
+diff --git a/stbg.c b/stbg.c
+new file mode 100644
+index 0000000..80bc4cf
+--- /dev/null
++++ b/stbg.c
+@@ -0,0 +1,122 @@
++/* See LICENSE file for copyright and license details. */
++/* Build: cc -o stbg stbg.c -lX11 */
++#include <errno.h>
++#include <stdarg.h>
++#include <stdio.h>
++#include <stdlib.h>
++#include <stdint.h>
++#include <string.h>
++#include <arpa/inet.h>
++#include <X11/Xlib.h>
++#include <X11/Xutil.h>
++#include <X11/Xatom.h>
++
++static char *argv0;
++
++static void
++die(const char *errstr, ...)
++{
++ va_list ap;
++
++ fprintf(stderr, "%s: ", argv0);
++ va_start(ap, errstr);
++ vfprintf(stderr, errstr, ap);
++ va_end(ap);
++ exit(1);
++}
++
++static void
++usage(void)
++{
++ die("usage: %s file.ff\n %s - < file.ff\n", argv0, argv0);
++}
++
++int
++main(int argc, char *argv[])
++{
++ uint32_t i, hdr[4], w, h, size;
++ uint64_t *data;
++ uint32_t *data32;
++ FILE *f;
++ int fromstdin;
++ Pixmap pmap;
++ Display *dpy;
++ Window root;
++ int scr;
++ Atom atom_stbg;
++ GC gc;
++ XGCValues gcvalues;
++ XImage *xi;
++
++ argv0 = argv[0];
++
++ if (argc != 2)
++ usage();
++
++ if (!(dpy = XOpenDisplay(NULL)))
++ die("can't open display\n");
++
++ scr = DefaultScreen(dpy);
++ root = RootWindow(dpy, scr);
++
++ fromstdin = !strcmp(argv[1], "-");
++ f = fromstdin ? stdin : fopen(argv[1], "rb");
++ if (!fromstdin && !f)
++ die("can't open %s\n", argv[1]);
++
++ if (fread(hdr, sizeof(*hdr), 4, f) != 4)
++ die("fread: unexpected end of file\n");
++
++ if (memcmp("farbfeld", hdr, sizeof("farbfeld") - 1))
++ die("invalid farbfeld magic\n");
++
++ w = ntohl(hdr[2]);
++ h = ntohl(hdr[3]);
++ if (w == 0 || h == 0)
++ die("invalid farbfeld dimensions\n");
++ size = w * h;
++
++ data = malloc(size * sizeof(uint64_t));
++ if (!data)
++ die("malloc: %s\n", strerror(errno));
++
++ if (fread(data, sizeof(uint64_t), size, f) != size)
++ die("fread: unexpected end of file\n");
++ if (!fromstdin)
++ fclose(f);
++
++ data32 = malloc(size * sizeof(uint32_t));
++ if (!data32)
++ die("malloc: %s\n", strerror(errno));
++
++ for (i = 0; i < size; i++) {
++ uint64_t p = data[i];
++ data32[i] = (p & 0x00000000000000FF) << 16 |
++ (p & 0x0000000000FF0000) >> 8 |
++ (p & 0x000000FF00000000) >> 32;
++ }
++ free(data);
++
++ xi = XCreateImage(dpy, DefaultVisual(dpy, scr),
++ DefaultDepth(dpy, scr), ZPixmap, 0,
++ (char *)data32, w, h, 32, w * 4);
++
++ pmap = XCreatePixmap(dpy, root, w, h, DefaultDepth(dpy, scr));
++
++ memset(&gcvalues, 0, sizeof(gcvalues));
++ gcvalues.graphics_exposures = False;
++ gc = XCreateGC(dpy, pmap, GCGraphicsExposures, &gcvalues);
++
++ XPutImage(dpy, pmap, gc, xi, 0, 0, 0, 0, w, h);
++ XDestroyImage(xi);
++ XFreeGC(dpy, gc);
++
++ atom_stbg = XInternAtom(dpy, "_ST_BACKGROUND_IMAGE", False);
++ XChangeProperty(dpy, root, atom_stbg, XA_PIXMAP, 32,
++ PropModeReplace, (unsigned char *)&pmap, 1);
++ XSetCloseDownMode(dpy, RetainPermanent);
++
++ XSync(dpy, False);
++
++ return 0;
++}
+diff --git a/x.c b/x.c
+index d73152b..c0ec757 100644
+--- a/x.c
++++ b/x.c
+@@ -81,6 +81,7 @@ typedef XftGlyphFontSpec GlyphFontSpec;
+ typedef struct {
+ int tw, th; /* tty width and height */
+ int w, h; /* window width and height */
++ int x, y; /* window location */
+ int ch; /* char height */
+ int cw; /* char width */
+ int mode; /* window state/mode flags */
+@@ -101,6 +102,9 @@ typedef struct {
+ XVaNestedList spotlist;
+ } ime;
+ Draw draw;
++ GC bggc; /* Graphics Context for background */
++ Atom stbg_atom; /* _ST_BACKGROUND_IMAGE atom */
++ Atom netwmstate_atom; /* _NET_WM_STATE atom */
+ Visual *vis;
+ XSetWindowAttributes attrs;
+ int scr;
+@@ -151,6 +155,9 @@ static void ximinstantiate(Display *, XPointer, XPointer);
+ static void ximdestroy(XIM, XPointer, XPointer);
+ static int xicdestroy(XIC, XPointer, XPointer);
+ static void xinit(int, int);
++static void updatexy(void);
++static void bginit(void);
++static Pixmap bginitfromroot(void);
+ static void cresize(int, int);
+ static void xresize(int, int);
+ static void xhints(void);
+@@ -515,6 +522,18 @@ propnotify(XEvent *e)
+ xpev->atom == clipboard)) {
+ selnotify(e);
+ }
++
++ if (xpev->atom == xw.stbg_atom) {
++ bginit();
++ redraw();
++ return;
++ }
++
++ if (pseudotransparency &&
++ e->xproperty.atom == xw.netwmstate_atom) {
++ updatexy();
++ redraw();
++ }
+ }
+
+ void
+@@ -545,7 +564,8 @@ selnotify(XEvent *e)
+ return;
+ }
+
+- if (e->type == PropertyNotify && nitems == 0 && rem == 0) {
++ if (e->type == PropertyNotify && nitems == 0 && rem == 0 &&
++ !pseudotransparency) {
+ /*
+ * If there is some PropertyNotify with no data, then
+ * this is the signal of the selection owner that all
+@@ -563,9 +583,11 @@ selnotify(XEvent *e)
+ * when the selection owner does send us the next
+ * chunk of data.
+ */
+- MODBIT(xw.attrs.event_mask, 1, PropertyChangeMask);
+- XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask,
++ if (!pseudotransparency) {
++ MODBIT(xw.attrs.event_mask, 1, PropertyChangeMask);
++ XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask,
+ &xw.attrs);
++ }
+
+ /*
+ * Deleting the property is the transfer start signal.
+@@ -851,9 +873,9 @@ xsetcolorname(int x, const char *name)
+ void
+ xclear(int x1, int y1, int x2, int y2)
+ {
+- XftDrawRect(xw.draw,
+- &dc.col[IS_SET(MODE_REVERSE)? defaultfg : defaultbg],
+- x1, y1, x2-x1, y2-y1);
++ if (pseudotransparency)
++ XSetTSOrigin(xw.dpy, xw.bggc, -win.x, -win.y);
++ XFillRectangle(xw.dpy, xw.buf, xw.bggc, x1, y1, x2-x1, y2-y1);
+ }
+
+ void
+@@ -1242,6 +1264,78 @@ xinit(int cols, int rows)
+ xsel.xtarget = XA_STRING;
+ }
+
++void
++updatexy(void)
++{
++ Window child;
++ XTranslateCoordinates(xw.dpy, xw.win, DefaultRootWindow(xw.dpy), 0, 0,
++ &win.x, &win.y, &child);
++}
++
++/*
++ * initialize background image from root window _ST_BACKGROUND_IMAGE
++ */
++static Pixmap
++bginitfromroot(void)
++{
++ Atom prop, atom_type;
++ int format;
++ unsigned long nitems, after;
++ unsigned char *data;
++ Pixmap pmap;
++
++ prop = XInternAtom(xw.dpy, "_ST_BACKGROUND_IMAGE", True);
++ if (prop == None)
++ return None;
++
++ if (XGetWindowProperty(xw.dpy, DefaultRootWindow(xw.dpy), prop,
++ 0, 1, False, AnyPropertyType, &atom_type,
++ &format, &nitems, &after, &data) != Success)
++ return None;
++
++ if (atom_type != XA_PIXMAP || format != 32 || nitems < 1) {
++ XFree(data);
++ return None;
++ }
++
++ pmap = *(Pixmap *)data;
++ XFree(data);
++
++ XSetTile(xw.dpy, xw.bggc, pmap);
++ XSetFillStyle(xw.dpy, xw.bggc, FillTiled);
++ return pmap;
++}
++
++void
++bginit(void)
++{
++ XGCValues gcvalues;
++ Pixmap bgpmap;
++
++ if (xw.bggc)
++ XFreeGC(xw.dpy, xw.bggc);
++
++ memset(&gcvalues, 0, sizeof(gcvalues));
++ xw.bggc = XCreateGC(xw.dpy, xw.win, 0, &gcvalues);
++
++ bgpmap = bginitfromroot();
++
++ if (bgpmap == None) {
++ XSetForeground(xw.dpy, xw.bggc, dc.col[defaultbg].pixel);
++ XSetFillStyle(xw.dpy, xw.bggc, FillSolid);
++ }
++
++ if (pseudotransparency) {
++ updatexy();
++ MODBIT(xw.attrs.event_mask, 1, PropertyChangeMask);
++ XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, &xw.attrs);
++ }
++
++ xw.stbg_atom = XInternAtom(xw.dpy, "_ST_BACKGROUND_IMAGE", False);
++ xw.netwmstate_atom = XInternAtom(xw.dpy, "_NET_WM_STATE", False);
++ XSelectInput(xw.dpy, DefaultRootWindow(xw.dpy), PropertyChangeMask);
++}
++
+ int
+ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
+ {
+@@ -1482,7 +1576,10 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
+ xclear(winx, winy + win.ch, winx + width, win.h);
+
+ /* Clean up the region we want to draw to. */
+- XftDrawRect(xw.draw, bg, winx, winy, width, win.ch);
++ if (bg == &dc.col[defaultbg])
++ xclear(winx, winy, winx + width, winy + win.ch);
++ else
++ XftDrawRect(xw.draw, bg, winx, winy, width, win.ch);
+
+ /* Set the clip region because Xft is sometimes dirty. */
+ r.x = 0;
+@@ -1914,9 +2011,17 @@ cmessage(XEvent *e)
+ void
+ resize(XEvent *e)
+ {
+- if (e->xconfigure.width == win.w && e->xconfigure.height == win.h)
+- return;
+-
++ if (pseudotransparency) {
++ if (e->xconfigure.width == win.w &&
++ e->xconfigure.height == win.h &&
++ e->xconfigure.x == win.x && e->xconfigure.y == win.y)
++ return;
++ updatexy();
++ } else {
++ if (e->xconfigure.width == win.w &&
++ e->xconfigure.height == win.h)
++ return;
++ }
+ cresize(e->xconfigure.width, e->xconfigure.height);
+ }
+
+@@ -1947,6 +2052,8 @@ run(void)
+ } while (ev.type != MapNotify);
+
+ ttyfd = ttynew(opt_line, shell, opt_io, opt_cmd);
++ if (pseudotransparency)
++ updatexy();
+ cresize(w, h);
+
+ for (timeout = -1, drawing = 0, lastblink = (struct timespec){0};;) {
+@@ -2100,6 +2207,7 @@ run:
+ rows = MAX(rows, 1);
+ tnew(cols, rows);
+ xinit(cols, rows);
++ bginit();
+ xsetenv();
+ selinit();
+ run();
+--
+2.55.0
+