XMonad and Taffybar on NixOS using Stack

2018-02-16

I recently installed NixOS on one of my laptops. I setup XMonad and Taffybar, using stack to compile them. This is somewhat unconventional on NixOS, so I wanted to write a blog post detailing how I did it.

These steps are aimed for people using NixOS, but they should work for any Linux distro with slight modification.

XMonad on NixOS the normal way

The normal way to use XMonad on NixOS is to enable it in /etc/nixos/configuration.nix:

{ config, pkgs, ... }:
{
  imports =
    [ # Include the results of the hardware scan.
      /etc/nixos/hardware-configuration.nix
    ];

  ...

  services.xserver.enable = true;
  services.xserver.layout = "us";
  services.xserver.windowManager.xmonad.enable = true;
  services.xserver.windowManager.xmonad.enableContribAndExtras = true;
}

The downside of this is that you must configure which GHC version and XMonad version you want to use in the configuration.nix file1. This isn't a huge problem, but it makes the solution non-portable to other Linux distros. The method I explain below should be portable to other distros as-is.

XMonad on NixOS the stack way

Before building XMonad, stack needs to be installed on the system. In general, you should install the latest version of stack.2

One easy way to do this is as follows:

  1. Use your package manager to install stack.
  2. Have stack upgrade itself to the latest version.
  3. Uninstall the older version with your package manager.

On NixOS

The previous three steps looks like the following when done on NixOS:

  1. Install stack with nix-env:

    $ nix-env --install stack

    You can check that stack is actually installed with the following command:

    $ stack --version
    Version 1.5.1 x86_64
    Compiled with:
    ...
  2. Have stack upgrade itself to the latest version.

    $ stack upgrade

    stack will download the latest version of itself and install it to ~/.local/bin/.

    Make sure ~/.local/bin/ is on your PATH. This can be done in your current bash session like the following:

    $ export PATH="${HOME}/.local/bin:${PATH}"

    The above statement should also be added to your ~/.bashrc file so that stack will be on your PATH for every bash session.

    You can check to make sure the latest version of stack is being used:

    $ stack --version
    Version 1.6.3, Git revision ...
  3. Use nix-env to uninstall the stack executable installed in step 1.

    $ nix-env --uninstall stack

If you ever want to upgrade to a newer version of stack, you can just run stack upgrade again.

NixOS-specific stack settings

There are a few NixOS-specific settings needed for stack. These should be added to the ~/.stack/config.yaml file. This is not needed for other distros:

# This file contains default non-project-specific settings for 'stack', used
# in all projects.  For more information about stack's configuration, see
# http://docs.haskellstack.org/en/stable/yaml_configuration/

# This enables stack cooperation with nix.
nix:
    enable: true
    # `pure` set to `true` means that environment variables won't get forwarded
    # when running `stack build` and `stack exec`.  This is generally not what
    # we want when doing day-to-day development.
    pure: false
    # These are packages passed to the nix environment when running `stack
    # build` or `stack exec`.  Note that `stack` will NOT be able to see a
    # library that has not been passed in here.
    packages:
        - cairo
        - gcc
        - gnome2.pango
        - gtk2-x11
        - libxml2
        - pkgconfig
        - upower
        - x11
        - xorg.libX11
        - xorg.libXext
        - xorg.libXinerama
        - xorg.libXrandr
        - xorg.libXrender

You must specify the system packages that should be available when running stack build or stack exec. The packages specified above are needed for XMonad and Taffybar.

Changes to /etc/nixos/configuration.nix

Before setting up XMonad, some options must be enabled in /etc/nixos/configuration.nix.

Here is a /etc/nixos/configuration.nix file with the relevant options:

{ config, pkgs, ... }:

{
  ...

  # Packages to install.
  environment.systemPackages = with pkgs; [
    ...
    # I use dmenu from XMonad.
    dmenu
    ...
  ];

  # Enable the X11 windowing system.
  services.xserver.enable = true;

  # Set the keyboard layout to "us".
  services.xserver.layout = "us";

  # Enable touchpad support.
  services.xserver.libinput.enable = true;

  # Enable UPower, which is used by taffybar.
  services.upower.enable = true;
  systemd.services.upower.enable = true;

  ...
}

After modifying this file, you should run sudo nixos-rebuild switch to make sure the changes take effect.

On other distros, this corresponds to the following steps:

  • Install dmenu.
  • Install X11 packages.3
  • Install upower and enable the upower daemon.

Compiling XMonad and Taffybar

Now that we have the latest version of stack and all required system packages, it is time to compile and install XMonad and Taffybar.

xmonad.hs

XMonad is configured with a file ~/.xmonad/xmonad.hs. This is just a normal Haskell file. Writing the xmonad.hs is out of scope for this article, but you should be able to find many examples by googling. My xmonad.hs is here.

When XMonad is launched, it calls GHC to compile this file and produce a new XMonad binary with all the options you have specified. It then re-execs this new binary.

This works well when GHC and XMonad are installed system-wide. However, when you are using stack, this obviously isn't going to work. XMonad won't be able to find GHC.

Recent versions4 of XMonad provide a way around this: a build.sh file.

build.sh

Instead of trying to call GHC directly, recent versions of XMonad will instead execute a ~/.xmonad/build.sh file, if it exists. You are free to write any sort of build steps you want in this build.sh file.

We will be using build.sh to do the following things:

  1. Install the gtk2hs-buildtools Haskell package. This is used by Taffybar.
  2. Install the taffybar Haskell package.
  3. Install the xmonad and xmonad-contrib Haskell packages.
  4. Build the ~/.xmonad/xmonad.hs file.

My build.sh file can be found here.5

In the beginning of the build.sh file, I source a file ~/.xmonad/xmonad_build_vars.sh. This file looks like the following:

#!/usr/bin/env bash
#
# Set config values used for building and running xmonad and taffybar.

# The directory in $HOME with all the XMonad stuff.
export XMONAD_DIR="${HOME}/.xmonad"

# A directory to use to install the taffybar and xmonad binaries.
export XMONAD_LOCAL_BIN_PATH="${XMONAD_DIR}/local-bin"

# The lts resolver we will use for building with stack.
export XMONAD_STACK_RESOLVER="lts-9.14"

These settings will come in handy later, when actually running XMonad and Taffybar.

Now ~/.xmonad/build.sh can be run.

$ chmod +x ~/.xmonad/build.sh
$ ~/.xmonad/build.sh

This will produce a ~/.xmonad/xmonad-x86_64-linux executable. This executable is XMonad compiled with your settings from xmonad.hs. There will also be xmonad and taffybar executables in ~/.xmonad/local-bin/.

Running XMonad and Taffybar

There are many different ways of running XMonad. Two popular ways are to use either startx or xdm.

startx is run from the command line. When run, it executes the file ~/.xinitrc. xdm is a graphical login manager. If you enter the correct username and password, it executes the ~/.xsession file for you.

In general, the contents of these files should be the same. The first part of the script will fork off some background processes. The second part of the script will exec your window manager (which is XMonad in this case).

~/.xsession

NixOS does not currently have support for startx, so the remainder6 of this article will be explaining how to use xdm and ~/.xsession.

If you added the services.xserver.enable = true setting to your /etc/nixos/configuration.nix file, then xdm should start up when you boot your computer.

The following is an example ~/.xsession file. Make sure to make it executable:

#!/bin/sh

# Make sure to add ~/.local/bin to the PATH because that is where `stack` is.
# We use `stack` to run `taffybar`.
export PATH="${HOME}/.local/bin:${PATH}"

# Source xmonad settings for xmonad and taffybar. This reads in the environment
# variables XMONAD_LOCAL_BIN_PATH and XMONAD_STACK_RESOLVER.
# XXX: This must be run using `.` and not `source` on systems where `sh` is
# not `bash`, because xinit always runs this file with `sh` (even if a
# different hashbang is specified).  Some versions of `sh` do know the
# `source` alias for `.`.
. "${HOME}/.xmonad/xmonad_build_vars.sh"

# Start taffybar.
PATH="${XMONAD_LOCAL_BIN_PATH}:$PATH" stack exec \
	--local-bin-path="${XMONAD_LOCAL_BIN_PATH}" \
	--resolver="${XMONAD_STACK_RESOLVER}" \
	-- taffybar &

# Start xmonad.
${XMONAD_LOCAL_BIN_PATH}/xmonad &
windowmanagerpid=$!
wait $windowmanagerpid

Some things to keep in mind:

  • I've seen systems where /bin/sh will always be used to run ~/.xsession and ~/.xinitrc. Some versions of sh do not understand bash's source alias for .. Make sure you don't use any bash-isms in this file.
  • Taffybar is similar to XMonad in that it recompiles a ~/.config/taffybar/taffybar.hs file when it is executed. This means that Taffybar must be run with stack exec, so it gets access to all the correct Haskell libaries. You can see my taffybar.hs file here.

You can see my ~/.xsession, which contains some examples of setting environment variables and running background processes.

xdm

When you type your username and password into xdm, it will run your ~/.xsession file. This should run XMonad. If you are using my xmonad.hs, you should see Taffybar at the top of your screen. You should also be able to use dmenu to run any other program with the key Super + p.

Conclusion

This setup is a little complicated, but it works well across distros, whether you are using startx or xdm.

Footnotes


  1. This could be done using the xmonad.haskellPackages configuration option.↩︎

  2. Older versions of stack will not work with newer versions of lts.↩︎

  3. If you are unsure of which packages are needed, consult your distro's documentation.

    If you can't find anything in your distro's documentation, one easy trick is to install XMonad (or another window manager) through your system's package manager. This should pull in all X11-related dependencies. Then you can just uninstall the XMonad package installed through the package manager.↩︎

  4. This functionality was available starting with xmonad-0.13.↩︎

  5. My build.sh file is somewhat complicated. Since build.sh is run every time XMonad is run, I want build.sh to be able to run as quickly as possible. In order to run only the minimal number of commands necessary, I am using a Makefile-like timestamp-based build process. For instance, if the build.sh file is older than the taffybar binary, then I don't try to reinstall Taffybar. However, if the build.sh file is newer than the taffybar binary, then I run stack install taffybar to reinstall Taffybar.

    In practice, this is very flexible and works reasonably well. However, if I was starting from scratch, I might elect to use a proper build system, like make or shake.

    Another possibility would be to create a Haskell package for your XMonad configuration, complete with a stack.yaml file. Running stack build in this directory would create the XMonad executable for your config.↩︎

  6. If you want to use the startx approach on a different distro, you will need to install a package that provides the startx binary. This package is generally called something like xinit or x11-xinit.

    After installing this package, create the ~/.xinitrc file and run startx. XMonad should start up.↩︎

tags: nixos, haskell