Dark window decorations for st and Emacs

Since a few weeks, I’m a new happy user of the Pantheon desktop environment (built by the community behind Elementary OS). Thus, I moved from i3 and a border-less tiling window manager to a more classical floating window manager. After many years of using a tiling manager, I realized I never make full use of them: I only have an heavy usage of virtual desktop, with one maximized window on each desktop. My three main applications are Firefox, GNU Emacs and simple terminal with tmux embedded. Emacs and Tmux already gives me a powerfull in-app windowing system and websites are more and more designed for wide screens. That is why, in the end, any window manager works for me, as soon as they allow me to maximize a window.

Pantheon desktop is built on the same root as the Gnome desktop and use recent GTK version behind the scenes. We now have the ability to use a unique theme through the whole desktop, but select between dark and light variant on a per application basis. This is already supported by a lot of Gnome or Elementary applications, but it is a bit more complicated for other apps.

What become very annoying is when you use a dark application, which keeps light window border, because that application does not know how to tell to the window manager that it prefer a dark theme. That was my case with both GNU Emacs and simple terminal. In both of them I use the Dracula dark theme, and thus having light window borders was a bit sad.

Generic way of doing it

The most simple way to do it, is obviously to always use a dark theme, or to set the gtk-application-prefer-dark-theme option to 1 in your .config/gkt-3.0/settings.ini file.

However, sometime you just really want to keep a light theme every where, but in some applications. These ones have to force the window manager to use a dark theme. To do so, they must make their related windows to expose the _GTK_THEME_VARIANT property set to dark. The following command line allow you to do that temporarily on any window:

xprop -id <window_id> -f _GTK_THEME_VARIANT 8u -set _GTK_THEME_VARIANT dark

The <window_id> has first to be found, for example with xdotool and the PID of your application: xdotool search --sync --onlyvisible --pid <PID>. The PID itself can be found with ps, top

The -f _GTK_THEME_VARIANT 8u option tell xprop that the _GTK_THEME_VARIANT property we are about to set is a UTF-8 string.

Finally, the -set _GTK_THEME_VARIANT dark actually set the value dark for that property.

GNU Emacs Specificities

If you already put the gtk-application-prefer-dark-theme option to 1, you have nothing more to do, as Emacs will correctly follow it. However if you do want to keep a light theme, but have a dark one in Emacs, you will have to make thing happen by yourself. Nicolas Petton (but others as well) have already shared by the past a working implementation I based my own proposal on. Thus both share the same unerlying call to the Linux tool xprop. It means this solution will only work on X related desktop manager (not on modern Gnome already using Wayland, nor Mac OS X or MS Windows).

My changes involve using the function start-process instead of call-process-shell-command or other synchronous system calls. The former is asynchronous, meaning it will not block the loading of Emacs. I also bind my function to the after-make-frame-functions hook to ensure every new Emacs window opened during a session will also use a dark window decoration theme.

To use it, you can put the following code snippet in your Emacs configuration file:

(defun ed/set-new-frame-dark (frame)
  "Ensure the given FRAME will be seen as preferring dark theme.
This function only works on X window system."
  (when (string= (window-system) "x")
    (let ((wid (frame-parameter frame 'outer-window-id)))
      (start-process "xprop" nil "xprop" "-id" wid
                     "-f" "_GTK_THEME_VARIANT" "8u"
                     "-set" "_GTK_THEME_VARIANT" "dark"))))
(add-hook 'after-make-frame-functions #'ed/set-new-frame-dark)
;; Call it once as the first created frame will not be handled by the
;; previous hook.
(ed/set-new-frame-dark (selected-frame))

Simple Terminal Specificities

Even if you already put the gtk-application-prefer-dark-theme option to 1, simple terminal will not listen to it. Meaning by default either you need to switch completely dark, or nothing.

That is why I created the little following patch to use when you build simple terminal to make it expose the _GTK_THEME_VARIANT property. This solution is completely built-in, without relying on any other tool.

To use that patch, once you have downloaded and extracted the last st tarball, you just have to change into the st directory and use the following command: patch -i <path_to_the_patch>/prefer_dark.patch -p1

And here is the patch:

--- st-master/x.c       2022-01-07 12:41:35.000000000 +0100
+++ x.c 2022-01-26 15:50:33.003779253 +0100
@@ -93,7 +93,7 @@ typedef struct {
        Window win;
        Drawable buf;
        GlyphFontSpec *specbuf; /* font spec buffer used for rendering */
-       Atom xembed, wmdeletewin, netwmname, netwmiconname, netwmpid;
+       Atom xembed, wmdeletewin, netwmname, netwmiconname, netwmpid, gtkthemevariant;
        struct {
                XIM xim;
                XIC xic;
@@ -161,6 +161,7 @@ static void xunloadfont(Font *);
 static void xunloadfonts(void);
 static void xsetenv(void);
 static void xseturgency(int);
+static void xsetgtkthemevariant(void);
 static int evcol(XEvent *);
 static int evrow(XEvent *);

@@ -1202,12 +1203,15 @@ xinit(int cols, int rows)
        xw.wmdeletewin = XInternAtom(xw.dpy, "WM_DELETE_WINDOW", False);
        xw.netwmname = XInternAtom(xw.dpy, "_NET_WM_NAME", False);
        xw.netwmiconname = XInternAtom(xw.dpy, "_NET_WM_ICON_NAME", False);
+       xw.gtkthemevariant = XInternAtom(xw.dpy, "_GTK_THEME_VARIANT", False);
        XSetWMProtocols(xw.dpy, xw.win, &xw.wmdeletewin, 1);

        xw.netwmpid = XInternAtom(xw.dpy, "_NET_WM_PID", False);
        XChangeProperty(xw.dpy, xw.win, xw.netwmpid, XA_CARDINAL, 32,
                        PropModeReplace, (uchar *)&thispid, 1);

+       xsetgtkthemevariant();
        win.mode = MODE_NUMLOCK;
@@ -1596,6 +1600,16 @@ xsetenv(void)

+       char *variant = "dark";
+       XTextProperty prop;
+       Xutf8TextListToTextProperty(xw.dpy, &variant, 1, XUTF8StringStyle, &prop);
+       XSetTextProperty(xw.dpy, xw.win, &prop, xw.gtkthemevariant);
+       XFree(prop.value);
 xseticontitle(char *p)
        XTextProperty prop;