My new project nix-desktop is out on GitHub now. It is an application and service manager for desktop, currently for Linux.

As the name implies, it relies on the Nix ecosystem. It resides in somewhere between nix-env command and home-manager.

What it is

It is currently capable of the following things:

  • Installing XDG menu entries.
  • Installing user systemd services.

It lets you write configuration in Nix.

Installation and usage

You can install the program by running the following command:

nix-env -if https://github.com/akirak/nix-desktop/archive/master.tar.gz

If you have turned on flakes, you can also run it using nix run command:

nix run 'github:akirak/nix-desktop'

To use it, you first have to create a configuration file. Create a file named desktop.nix in a Git repository and run

nix-desktop DIR

where DIR is the directory containing the desktop.nix file.

You can uninstall the configuration by running

nix-desktop uninstall DIR

Menu entries and service states are updated as you run the command, and it suppports idempotent operation in terms of both installation and uninstallation.

Let’s take a look at an example of running Doom Emacs.

My first Emacs package was playground. I created it to help myself switch from Spacemacs to Emacs. It was a package which spawns Emacs instances from inside Emacs to let the user try out other Emacs configuration repositories. It used a technique of setting HOME environment set to another location. Soon chemacs appeared. It was a better solution for using Emacs with multiple configurations, and I adopted it as well1.

This year, Emacs 27 was officially released. It supports XDG directory conventions, which means you can put your Emacs configuration in $XDG_CONFIG_HOME/emacs which is usually ~/.config/emacs but customizable. The version also supports early-init.el, which has made the initial version of chemacs deprecated. Now there is chemacs2, which is the successor of the Emacs bootloader. To support early-init.el, you have to put chemacs 2 in ~/.config/emacs/ and put your own configuration in somewhere else. This looks like a little regression.

Doom Emacs is now a popular Emacs configuration. It starts up faster than Spacemacs and is contributed by hundreds of people to provide a great out-of-the-box experience. I have my own configuration, but I wanted to try out Doom, and I didn’t want to install Chemacs 2. I just wanted to sideload Doom.

It turns out that Nix can handle this situation very well. With nix-shell, you can create a sandboxed shell environment, which is analogous to virtualenv in Python but for any languages. I have created the following shell.nix in a repository:

with (import <nixpkgs> {});
with builtins;
let
  configDir = "${builtins.getEnv "HOME"}/.config/doom-runner";
in
mkShell {
  XDG_CONFIG_HOME = configDir;

  DOOMDIR = toString ./.;

  buildInputs = [
    emacs
  ];

  # Set up symbolic links from inside XDG_CONFIG_HOME for some
  # applications so they look at existing settings in ~/.config
  initConfigDir = writeShellScript "init-xdg-config" ''
    origin="$HOME/.config"
    cd "''${XDG_CONFIG_HOME}"
    if [[ "$origin" = "$PWD" ]]; then
      exit
    fi
    for f in nix nixpkgs hub; do
      [[ -e $f ]] && continue
      dest="$origin/$f"
      ! [[ -e "$dest" ]] && continue
      ln -sv "$dest"
    done
  '';

  shellHook = ''
    if [[ -f "$HOME/.emacs" || -d "$HOME/.emacs.d" ]]; then
      echo "Non-XDG user-emacs-directory is getting in the way."
      exit 1
    fi

    configDir="''${XDG_CONFIG_HOME}"
    doomDir="$configDir/emacs"
    privateDir="$DOOMDIR"

    if ! [[ -d "$configDir" ]]; then
      mkdir -p "$configDir"
      $initConfigDir
    fi

    if ! [[ -d $doomDir ]]; then
      git clone --depth 1 https://github.com/hlissner/doom-emacs $doomDir
    fi

    export PATH="$doomDir/bin:$PATH"

    if ! [[ -d "$privateDir" ]]; then
      echo
      echo "The installation of Doom has not been completed yet."
      echo
      echo "If you have your own private config, clone it to $privateDir."
      echo
      echo "Run 'doom install' before you start Emacs for the first time."
      echo
    fi
  '';
}

This uses a similar technique as the underdog playground.el, but it sets XDG_CONFIG_HOME and DOOMDIR, and never intact HOME, thanks to the XDG support of Emacs 27. It was painful to change the home, so this is much better.

When you run nix-shell in the directory, it creates ~/.config/doom-runner directory and installs Doom to ~/.config/doom-runner/emacs.

You are now in a subshell where doom command is available. You can create your first configuration by running the following command in the shell:

doom install

Now you can use Doom, but it is tedious to open a terminal and enter the subshell every time you use it. This is where nix-desktop comes in.

Fist create the following desktop.nix in the same directory as shell.nix:

let
  pkgs = import <nixpkgs> {};
in
{
  name = "doom-config";
  xdg.menu.applications.doom-emacs = {
    Name = "Doom Emacs";
    Icon = "emacs";
    TryExec = "${builtins.getEnv "HOME"}/.config/doom-runner/emacs/bin/doom";
    Exec = "${pkgs.nix}/bin/nix-shell ${builtins.toString ./.}/shell.nix --command emacs";
    StartupWMClass = "Emacs";
  };
}

This configuration file defines a XDG menu entry doom-emacs.desktop which starts Doom. Run nix-desktop to install the application:

nix-desktop .

Now you can start Doom directly on your Linux desktop.

For administration of Doom, you still have to open a terminal and enter the shell:

nix-shell

doom command will be available there.

What are the benefits of this installation method over Chemacs 2 and home-manager?

  1. You can sideload Doom (or any other Emacs configuration) while keeping your go-to configuration in the default location in ~/.config/emacs.
  2. You can also use a different version of Emacs for Doom. emacs-overlay provides unstable versions of Emacs.
  3. You can put all of the configuration files for Doom in a single repository. You can put both shell.nix, desktop.nix, and your private Doom configuration in the same repository.
  4. You don’t have to add the temporary configuration for trying out something to your home-manager configuration.

You can install menu entries by performing ad-hoc installation of packages using nix-env, but you have to run xdg-desktop-menu forceupdate to take effect immediately. nix-desktop updates it automatically, which is only a slight improvement in that the configuration is declarative and operation is idempotent. For systemd services, however, this will be of more importance.

Systemd

nix-desktop can also be used to define systemd services. As with the functionality for menu entries, nix-desktop does not only install service files, which is already covered by nix-env, but also automatically enables and (re)restarts individual services, based on your configuration. This feature is inspired by Ansible.

It only supports services at present, and not other unit types such as timers, mounts, etc.

There is no concise example for this feature yet. The overlayfs example in the documentation is rather complex, so maybe I will explain it in a separate post later.

Which tools to use

You have a desktop machine which you want to configure in Nix as much as possible. There are already several options in the Nix world2:

  • If the operating system is NixOS, you can configure the entire system by editing /etc/nixos/configuration.nix.
  • There is home-manager, which lets you configure an entire home directory in Nix.
  • You can perform ad-hoc installation of packages using nix-env.
  • And nix-desktop, introduced in this post.

They are different in many ways, but the following points will be useful for comparison:

  • Does it run on non-NixOS Linux distributions? NixOS is a great operating system even for desktop, but your work environment may not simply allow it.
  • Does it affect tne entire user/system environment (global), or only inside a particular project (local)?
  • Does it allow only a single set of configuration per user/system (centralized) or allow multiple configuration entry points (decentralized)?
  • Is it capable of firing actions to notify updates to the system? When you install applications manually, you have to run commands such as xdg-desktop-menu forceupdate and systemctl daemon-reload to notify updates. I will call this a hook system.

Here is a summary:

Non-NixOS support Scope Configuration Hook system
NixOS No Global Centralized Yes
Docker Yes Global Decentralized No
home-manager Yes Global Centralized Yes
nix-desktop Yes Global Decentralized Yes
nix-env Yes Global Decentralized No
nix-shell Yes Local Decentralized -
  • The standard way of editing configuration.nix for NixOS lets you configure an entire machine. Machines don’t have to be real physical hardware. You can also use Nix to produce tiny virtual machine images and LXC containers. You can run virtual machines built using Nix on non-NixOS systems, but it doesn’t integrate well with your desktop.
  • Alternatively, you can configure Docker containers in Nix. Containers are useful for services. It is not usually a good idea to run graphical applications inside containers.
  • home-manager is useful for user preferences. It is a centralized solution which allows only one entry point per user, so it is best for tracking programs you want to deploy on all machines regardless of your context.
  • nix-env is useful for ad-hoc installation of packages. That is, it lets you install specific applications from Nixpkgs or other repositories for using globally. The downside of this is that you can easily lose track of what you have installed onto your machine, so it is not a good option in terms of configuring a system.

According to the table, nix-desktop sits in somewhere between home-manager and nix-env. It lets you configure applications and services declaratively, but it also allows multiple entry points per user.

I would recommend that you use it to configure domain/context-specific things. You probably have some applications that you use only at work but not for personal life. In this situation, you can create a Git repository for your specific organization or project, put all related notes and configuration files in it, and install applications using nix-desktop. I call this a self-contained repository.

It serves exactly the same way for personal stuffs. Create as many self-contained repositories as your contexts. They integrate seamlessly with your desktop, no matter what the underlying Linux distribution is.

Common stuffs should go into your home-manager configuration. It is about your preferences, and not about your organizations, clients, or projects.

Note that nix-desktop aims to be minimal, unlike home-manager. home-manager is a big project and tries to cover every program its users like to use. nix-desktop is just a wrapper around Nix and Linux, and it won’t support convenience features for specific programs, because it would be too much of work for me. It also doesn’t support generations and rollback. In that sense, the project doesn’t strictly follow the philosophy of Nix. It is a small piece of software which aims to solve a problem in the niche of niche.

Future work

I currently plan on adding a couple of features to nix-desktop:

  • A user-defined hook system, which lets the user to define custom tasks for installation and uninstallation. It should be defined as installHook and uninstallHook attributes in desktop.nix.
  • Supporting other systemd unit types such as timer units, once I find a use case for that.

I don’t plan on adding support for Darwin. Because I have no Macs, I don’t know how they are configured, and I cannot test them either. I will be glad if there were people who would make contribution on it.


  1. Actually, I used only one configuration for most of the time, but I used chemacs to set custom-file. [return]
  2. There is an experimental feature called Nix flakes, but I won’t talk about it in this post. [return]