A GNOME wallpaper build factory

Hi all,

I recently built a small wallpaper “factory” in my NixOS flake and wanted to share the pattern (I can’t share the repo, since it’s something I did for my company).

The goal is simple: drop PNG files into a directory, and let Nix generate the GNOME wallpaper package and metadata automatically.

How it works

The layout looks like this:

pkgs/wallpapers/
  package.nix
  mk-background.nix
  mk-dynamic-background.nix
  walls/
    simple-background.png
    dynamic-background/
      light.png
      dark.png

The convention is:

walls/simple-background.png
  -> wallpapers.simple-background

walls/dynamic-background/light.png
  -> wallpapers.dynamic-background_light

walls/dynamic-background/dark.png
  -> wallpapers.dynamic-background_dark

walls/dynamic-background/{light,dark}.png
  -> wallpapers.dynamic-background_dynamic

Each generated wallpaper installs:

share/backgrounds/<name>.png
share/gnome-background-properties/<name>.xml

For light/dark pairs, the generated GNOME XML uses both:

<filename>...</filename>
<filename-dark>...</filename-dark>

So GNOME can switch wallpapers based on the active light/dark theme.

The aggregate package is built with:

nix build '.#wallpapers'

and can be installed system-wide like any other package:

{ pkgs, ... }:

{
  environment.systemPackages = [
    pkgs.wallpapers
  ];
}

The part I like most is that adding a new wallpaper does not require touching Nix code. For a static wallpaper, add:

walls/foo.png

For a dynamic GNOME wallpaper, add:

walls/bar/
  light.png
  dark.png

The factory discovers it with builtins.readDir, creates the derivations, and exposes them individually through passthru.

This is not a big framework, just a small convention over files. But it has been useful for keeping branding assets declarative, reproducible, and easy to update.

What about the code?

I don’t really like saying that, but here it’s mostly true: the process is quite simple. I have two packages to handle the generation of GNOME XML background properties files:

mk-background.nix

{
  lib,
  stdenv,
  name,
  src,
}:

let
  bgOutdir = "share/backgrounds";
  bgOutfile = "${bgOutdir}/${name}.png";
in
stdenv.mkDerivation (finalAttrs: {
  inherit name src;

  dontUnpack = true;
  dontPatch = true;
  dontConfigure = true;
  dontBuild = true;
  dontFixup = true;

  installPhase = ''
    runHook preInstall

    mkdir -p $out/${bgOutdir}
    cp $src $out/${bgOutfile}

    mkdir -p $out/share/gnome-background-properties
    cat <<EOF > $out/share/gnome-background-properties/${name}.xml
    <?xml version="1.0"?>
    <!DOCTYPE wallpapers SYSTEM "gnome-wp-list.dtd">
    <wallpapers>
      <wallpaper deleted="false">
        <name>${name}</name>
        <filename>$out/${bgOutfile}</filename>
        <options>zoom</options>
        <shade_type>solid</shade_type>
        <pcolor>#ffffff</pcolor>
        <scolor>#000000</scolor>
      </wallpaper>
    </wallpapers>
    EOF

    runHook postInstall
  '';

  passthru = {
    gnomeFilePath = "${finalAttrs.finalPackage}/${bgOutfile}";
  };

  meta = {
    license = lib.licenses.free;
    platforms = lib.platforms.all;
  };
})

mk-dynamic-background

{
  writeTextFile,
  name,
  lightBg,
  darkBg,
}:

writeTextFile {
  name = "${name}-background-info";
  destination = "/share/gnome-background-properties/${name}.xml";

  text = ''
    <?xml version="1.0"?>
    <!DOCTYPE wallpapers SYSTEM "gnome-wp-list.dtd">
    <wallpapers>
      <wallpaper deleted="false">
        <name>${name}</name>
        <filename>${lightBg.gnomeFilePath}</filename>
        <filename-dark>${darkBg.gnomeFilePath}</filename-dark>
        <options>zoom</options>
        <shade_type>solid</shade_type>
        <pcolor>#ffffff</pcolor>
        <scolor>#000000</scolor>
      </wallpaper>
    </wallpapers>
  '';
}

And the main package which aggregates all wallpapers in a single derivation:

package.nix

{
  lib,
  callPackage,
  runCommandLocal,
}:

let
  mkBackground = callPackage ./mk-background.nix;
  mkDynamicBackground = callPackage ./mk-dynamic-background.nix;

  wallpapers = lib.filterAttrs (
    _name: type: type == "directory" || type == "regular"
  ) (builtins.readDir ./walls);
  sanitizeName = name: lib.replaceString "." "-" name;

  wallpaperDrvs = lib.concatMapAttrs (
    name: _:
    let
      sanitizedName = sanitizeName name;
      nameWithoutSuffix = sanitizeName (lib.removeSuffix ".png" name);
      singleBg =
        if
          (lib.pathIsDirectory ./walls/${name} == false) && lib.pathExists ./walls/${name}
        then
          mkBackground {
            name = nameWithoutSuffix;
            src = ./walls/${name};
          }
        else
          null;
      lightBg =
        if lib.pathExists ./walls/${name}/light.png then
          mkBackground {
            name = "${name}-light";
            src = ./walls/${name}/light.png;
          }
        else
          null;
      darkBg =
        if lib.pathExists ./walls/${name}/dark.png then
          mkBackground {
            name = "${name}-dark";
            src = ./walls/${name}/dark.png;
          }
        else
          null;
    in
    lib.optionalAttrs (singleBg != null) { "${nameWithoutSuffix}" = singleBg; }
    // (lib.optionalAttrs (lightBg != null) { "${sanitizedName}_light" = lightBg; })
    // (lib.optionalAttrs (darkBg != null) { "${sanitizedName}_dark" = darkBg; })
    // (lib.optionalAttrs (lightBg != null && darkBg != null) {
      "${sanitizedName}_dynamic" = mkDynamicBackground {
        inherit name lightBg darkBg;
      };
    })
  ) wallpapers;
in

runCommandLocal "wallpapers" { passthru = wallpaperDrvs; } ''
  mkdir -p $out/share/{backgrounds,gnome-background-properties}
  for wall in ${lib.concatStringsSep " " (lib.attrValues wallpaperDrvs)}; do
    cp -r $wall/* $out
  done
''

Small note about the sanitizeName helper function

I created it because I want to be able to name my wallpapers like foo.bar.png or foo.bar/, while still referencing them as wallpapers.foo-bar instead of wallpapers."foo.bar". This is unnecessary if you don’t have dots in your wallpaper names.

Optional: Replacing the default NixOS GNOME wallpaper

I also use the generated package to set the default GNOME wallpaper through nixos-gsettings-overrides:

environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = lib.mkForce "${
  pkgs.gnome.nixos-gsettings-overrides.override {
    nixos-background-light = pkgs.wallpapers.dynamic-background_light;
    nixos-background-dark = pkgs.wallpapers.dynamic-background_dark;
  }
}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";

Optional: Setting a custom GDM login screen

For GDM, I use the same generated wallpaper derivation and patch gnome-shell with the final store path exposed by the wallpaper package:

Overlay

{ pkgs, ... }:

{
  nixpkgs.overlays = [
    (
      final: prev:
      let
        # This is exposed from creating a `gdm.png` file in `walls/`!
        wallpaper = pkgs.wallpapers.gdm;
      in
      {
        gnome-shell = prev.gnome-shell.overrideAttrs (oldAttrs: {
          buildInputs = (oldAttrs.buildInputs or [ ]) ++ [ wallpaper ];

          patches = (oldAttrs.patches or [ ]) ++ [
            (final.replaceVars ./gdm-background.patch {
              backgroundPath = wallpaper.gnomeFilePath;
            })
          ];
        });
      }
    )
  ];
}

Patch

diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss
index b8f5ae9..9560976 100644
--- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss
+++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss
@@ -231,7 +231,10 @@ $_gdm_dialog_width: 25em;
 }

 #lockDialogGroup {
-  background-color: $_gdm_bg;
+  background: $_gdm_bg url(file://@backgroundPath@);
+  background-repeat: no-repeat;
+  background-size: cover;
+  background-position: center;
 }

 // Clock

The custom GDM login screen doesn’t work very well on multi-screen setups, since the image covers all monitors.

I didn’t find a solution (yet) about this issue. If anybody has pointers though, I’m all ears!

Cheers!

4 Likes

Wow, this is excellent. I appreciate the level of care taken to make a clean interface to this functionality.

1 Like