System appearance in Emacs on macOS

More and more applications offer an automatic switch from light mode to dark mode as your plot on Spaceship Earth turns its back on the sun. In “modern” applications, these comforts are commonplace, but a little work is required to upgrade software older than any dog, cat, and most millennials.

Thankfully, Boris Buliga provides patches over on GitHub that we can apply to our Emacs installation.

To apply these via Nix, I have a custom Emacs package in my Home Manager configuration:

{
  programs.emacs = {
    package = pkgs.emacs-unstable-pgtk.overrideAttrs (final: prev: {
      # `emacs-28` patches are compatible with `emacs-29`.
      #
      # Where a compatible path exists, there is a symlink upstream to keep
      # things clean, but GitHub doesn't follow symlinks to generate the
      # responses we need (instead GitHub returns the target of the symlink).
      patches =
        (prev.patches or [])
        ++ [
          # Fix OS window role (needed for window managers like yabai)
          (pkgs.fetchpatch {
            url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-28/fix-window-role.patch";
            sha256 = "0c41rgpi19vr9ai740g09lka3nkjk48ppqyqdnncjrkfgvm2710z";
          })
          # Use poll instead of select to get file descriptors
          (pkgs.fetchpatch {
            url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-29/poll.patch";
            sha256 = "0j26n6yma4n5wh4klikza6bjnzrmz6zihgcsdx36pn3vbfnaqbh5";
          })
          # Enable rounded window with no decoration
          (pkgs.fetchpatch {
            url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-29/round-undecorated-frame.patch";
            sha256 = "0x187xvjakm2730d1wcqbz2sny07238mabh5d97fah4qal7zhlbl";
          })
          # Make Emacs aware of OS-level light/dark mode
          (pkgs.fetchpatch {
            url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-28/system-appearance.patch";
            sha256 = "14ndp2fqqc95s70fwhpxq58y8qqj4gzvvffp77snm2xk76c1bvnn";
          })
        ];
    });
  };
}

With a patched version of Emacs, we can then use a little snippet of Emacs Lisp to change themes when our system appearance changes.

(when (boundp 'ns-system-appearance-change-functions)
  (add-hook! 'ns-system-appearance-change-functions
    (defun +jcf--load-theme-dwim (appearance)
      (mapc #'disable-theme custom-enabled-themes)
      (pcase appearance
        ('dark  (load-theme 'doom-one t))
        ('light (load-theme 'doom-one-light t))))))

If you’re not using Doom, you’ll want to use the regular add-hook macro in Emacs and load-themes or customise fonts or turn on and off your Hue lights as you see fit.

If installed patched versions of Emacs isn’t your thing, there is a technique via Applescript you can rely on, but it’s not as elegant.

(shell-command-to-string
 "printf %s \"$( osascript -e \'tell application \"System Events\" to tell appearance preferences to return dark mode\' )\"")