Volume Control in xmonad

What you will need to follow along

The goal

import XMonad
main = xmonad defaultConfig
blank screen
Figure 1: bare bones

I got very jealous of Nicole recently when we were watching some television on her Mac. The commercials came on and blasted us away... so she hit a key, and her volume magically dropped, and a beautiful graphic popped up showing the volume level. When the show came back on, another key put the volume back. Nice!

So, let's take the first step in getting xmonad to do that: let's add some keybindings for volume control and a pop-up showing the current volume level.

We'll start with the barest of bones configuration, shown to the right with a screenshot from a fictitious 320×240 monitor. I'll only put screenshots by configurations that change the visual compared to the last one.

Adding volume control

import XMonad
import XMonad.Actions.Volume
import Data.Map    (fromList)
import Data.Monoid (mappend)

main = xmonad defaultConfig { keys =
    keys defaultConfig `mappend`
    \c -> fromList [
        ((0, xK_F6), lowerVolume 4 >> return ()),
        ((0, xK_F7), raiseVolume 4 >> return ())
    ]
}
Figure 2: raising and lowering the volume

This is the easiest step. The XMonad.Actions.Volume module in xmonad-extras provides a whole glut of actions for controlling the volume and muting. For now, we'll just use these two:

lowerVolume :: MonadIO m => Double -> m Double
raiseVolume :: MonadIO m => Double -> m Double

The Double arguments are how many percentage points to raise the volume; I like 4. When you're doing this yourself, you might also want to bind a key to the toggleMute function, but I personally don't want that very often, so I decided to skip it. If you do this, you will probably also want to change lowerVolume 4 to setMute False >> lowerVolume 4 and raiseVolume 4 to setMute False >> raiseVolume 4.

Now, my keyboard has a great big gap in between the F6 and F7 keys that make those ones super easy to find tactilely, so I chose those keys for volume adjustment (with no modmask at all). If you stare past the fluff of Haskell's record syntax, you'll see that there are really only two load-bearing lines, the ones with xK_F6 and xK_F7. To begin with, I just threw away the return value of the volume change command.

Great, now I could control the volume with F6 and F7! I was pretty happy with this for a while, but after some testing, I decided that visual feedback really mattered. So... let's play with dzen.

Calling dzen to show the current volume

import XMonad
import XMonad.Actions.Volume
import XMonad.Util.Dzen
import Data.Map    (fromList)
import Data.Monoid (mappend)

alert = dzenConfig return . show

main = xmonad defaultConfig { keys =
    keys defaultConfig `mappend`
    \c -> fromList [
        ((0, xK_F6), lowerVolume 4 >>= alert),
        ((0, xK_F7), raiseVolume 4 >>= alert)
    ]
}
dzen shows the volume
Figure 3: displaying the volume

It turns out there's a module for this, too, in xmonad-contrib: XMonad.Util.Dzen. The first thing I wanted to do was make sure that dzen was working properly. That module provides the dzenConfig return :: String -> X () function to just throw some text up on the screen for a few seconds. So it's time to wire things up and stop throwing away the return value from the volume raise/lower actions: let's send it on to dzen instead. Of course, the returned value is a Double, so we have to convert it to a String first. I defined myself an alert function that will display a representation of anything that can be shown.

The result is kind of okay: there's a bar across the entire top of the screen for a few seconds with the new volume setting.

Prettier, pretty please

import XMonad
import XMonad.Actions.Volume
import XMonad.Util.Dzen
import Data.Map    (fromList)
import Data.Monoid (mappend)

alert = dzenConfig centered . show . round
centered =
        onCurr (center 150 66)
    >=> font "-*-helvetica-*-r-*-*-64-*-*-*-*-*-*-*"
    >=> addArgs ["-fg", "#80c0ff"]
    >=> addArgs ["-bg", "#000040"]

main = xmonad defaultConfig { keys =
    keys defaultConfig `mappend`
    \c -> fromList [
        ((0, xK_F6), lowerVolume 4 >>= alert),
        ((0, xK_F7), raiseVolume 4 >>= alert)
    ]
}
prettified dzen output
Figure 4: final product

Okay, that works... but it's pretty ugly. I wanted to make it a little nicer:

  1. I only want to show the first two significant digits. After that is pretty much noise as far as I'm concerned. So I threw a round on the end of my alert function.
  2. I also wanted to center the dzen on the current screen. (My fictitious display has only one screen, but we might as well be general here.) The xmonad-contrib module provides the center function for this purpose. I chose the 150 and 66 (after some experimenting) to match the size of the rendered text; it's a hack, but it's about the best we can do without opening up some fonts within xmonad itself and checking out their metrics.
  3. I fired up xfontsel and picked out a nice big font so that it would be readable.
  4. Just for fun, I set the foreground and background colors, too.

After all that, I finally had a setup that I kind of liked. Perhaps sometime in the future, when I get some more time to hack, I'll learn about ghosd and get a truly beautiful volume indicator going. =)