XMobar with XMonad

XMonad introduces these really neat things called log hooks and startup hooks, which is a way for you to do stuff every time xmonad is restarted. For instance,

myStartupHook = do
  safeSpawn "xmessage" ["Welcome to XMonad"]
  safeSpawn "ibus-daemon" ["-drx"]

displays a welcome xmessage window along with making sure that ibus-daemon is up and running. However, xmonad will not forcibly kill all its children when it restarts, so suppose we have a program called forker that looks something like this,

#include <errno.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
  if (argc < 2) {
    fprintf(stderr, "usage: %s <program> [args]\n", argv[0]);
    return 1;
  } else {
    pid_t pid = fork();
    if (pid == -1) {
      perror("fork");
      return errno;
    } else if (pid == 0) {
      close(0);
      close(1);
      close(2);
      execvp(argv[1], argv + 1);
      perror("exec");
      return errno;
    }
  }
  return 0;
}

which simply forces /sbin/init to adopt the child process if it is created. Then myStartupHook is equivalent to the following lines of an .xinitrc, which is executed every time xmonad is restarted (not to be confused with xinit/x server).

$ forker xmessage "Welcome to XMonad"
$ forker ibus-daemon -drx

This is obviously not the intended behavior because we end up with a bunch of adopted processes because xmonad does not clean up its children. Because of that, the standard way of linking xmonad with xmobar, is quite resource inefficient for users that modify their xmonad.hs and restart xmonad frequently. The default way of joining the two looks something like

xmobar <- spawnPipe "xmobar"
xmonad defaultConfig
{ logHook = do
    dynamicLogWithPP xmobarPP
      { ppOutput = hPutStrLn xmobar
      }
}
hClose xmobar

Okay, so we can do better than this by killing off previous xmobar processes. We try

spawn "pkill xmobar"
xmobar <- spawnPipe "xmobar"

but notice that this only works part of the time. Why? Because the spawn family from XMonad.Run only creates the process. The only sequencing going on here is that the pkill xmobar process is created before the xmobar process. Instead, we can do better, with callProcess from System.Process. So we try

callProcess "pkill" ["xmobar"]
xmobar <- spawnPipe "xmobar"

and upon performing $ xmonad --recompile && xmonad --restart, we discover that we have crashed X! Looking at Xorg.0.log, we determine that callProcess does a createProcess to get a process handle, and then a waitForProcess to wait for it to terminate, but waitForProcess fails and haskell gives up. Digging around the xmonad documentation, I discovered that xmonad installs its own SIGCHLD handler to prevent random zombies, so the process is cleaned up (the signal interrupts wait(2) before wait cleans it up), so instead, we needed something like

uninstallSignalHandlers
callProcess "pkill" ["xmobar"]
installSignalHandlers
xmobar <- spawnPipe "xmobar"

to do what we want. Now this is better, but it is still inefficient. XMobar is a fully fledged window and xmonad --restart does not restart any of your other programs, so the only real way to have one instance of xmobar forever is to run it in your .xinitrc, but doing xmobar & prevents us from writing to stdin of xmobar and exec xmonad | xmobar should work but didn’t for me because I did not properly set up StdinReader, so whenever I quit xmonad, xmobar would keep running and xinitrc would not complete, so X would not shut down. Thus, I decided to use PipeReader. Setting this up was fairly easy. All it took was

# .xinitrc
export XMOBAR_PIPE="$HOME/.local/share/xmobar/xmobar.pipe"
[ -e "$XMOBAR_PIPE" ] || mkfifo "$XMOBAR_PIPE"
xmobar &
exec xmonad
rm "$XMOBAR_PIPE"

and

pipe_file <- getEnv "XMOBAR_PIPE"
xmobar <- fdToHandle =<< openFd pipe_file WriteOnly Nothing defaultFileFlags
hSetBuffering xmobar LineBuffering

Along the way, I encountered some problems such as openFile failing if there are no readers due to opening in non-blocking mode (fixable by using ReadWriteMode), but I wanted to open in write only mode so System.Posix came to the rescue here. And hooray! It works! At this point, I discovered that I actually needed to have Run StdinReader in my xmobarrc for stdin input to work, so I had the great idea of combining both PipeReader and StdinReader, where stdin serves as xmonad messages and PipeReader can do whatever the hell I want it to do with user scripts and what not. So in the end, my configuration ends up looking something like this.

-- xmonad.hs
catchAny = catch :: IO a -> (SomeException -> IO a) -> IO a
main = do
  notification_pipe <- getEnv "XMOBAR_NOTIFICATION_PIPE"
  xmb_pipe <- fdToHandle =<< tryOpenFile notification_pipe
  hSetBuffering xmb_pipe LineBuffering
  hPutStrLn xmb_pipe "starting xmonad..."
  xmonad $ defaults xmb_pipe `additionalKeys` myBindings
  hClose xmb_pipe
  where
    -- xmb is a remnant from using PipeReader
    defaults xmb = defaultConfig
      {
      -- ...
      , logHook = myLogHook stdout
      -- ...
      }
    myLogHook handle = dynamicLogWithPP xmobarPP
                         { ppOutput = hPutStrLn handle
                         }
    tryOpenFile fname = openFile fname `catchAny` \e -> openFile "/dev/null"
      where openFile fname = openFd fname WriteOnly Nothing defaultFileFlags
# .xinitrc
[ "$XMOBAR_NOTIFICATION_PIPE" == "" ] &&
    XMOBAR_NOTIFICATION_PIPE="$HOME/.local/share/xmobar/notifications.pipe"
export XMOBAR_NOTIFICATION_PIPE
[ -e "$XMOBAR_NOTIFICATION_PIPE" ] || mkfifo "$XMOBAR_NOTIFICATION_PIPE"

# start useful services
# xmobar &
stalonetray &
nm-applet &
ibus-daemon -rx &
urxvtd -f -o

# start xmonad
exec xmonad | xmobar
rm "$XMOBAR_NOTIFICATION_PIPE"

which, in my opinion, looks very clean, and does not do any extra work.

You can view my full xmonad configuration files on Github.

This entry was posted in Software. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *