Recently, I replaced a defunct notebook that I use as my primary workstation. Instead of Windows 7 I decided to go with FreeeBSD 8.0-release. For my window manager I selected XMonad, because it’s lightweight, emphasizes productive use of the keyboard, and it’s highly configurable using Haskell.
Today I’ll give you a guided tour of my Xmonad configuration, which you can download below. First, some screen shots. Here’s my usual setup on the main workspace (click to enlarge):
The screen is divided using the golden ratio, with vim in the larger section and a shell prompt (tcsh) in the smaller one. The shell prompt gives me a scrollable history of commands and their output, unlike issuing shell commands from within vim. The bar across the top is xmobar — the configuration of which I’ll explain later.
Next, here’s my second workspace, dedicated to internet communications:
This workspace is divided 80/20 between my email client (mutt) and chat (pidgin). To keep the contrast between window backgrounds from blinding me, I’ve implemented a dark gtk-2.0 theme (Khali) so that pidgin’s background is almost black.
The third workspace is devoted entirely to the web browser (firefox):
Workspace number 4 is where I bring up VirtualBox. Here it’s shown running Windows 7:
VirtualBox’s main window is behind the full-screen virtual machine, but I can do Win+J or Win-K to swap to the main window (after RightCtrl to release the keyboard from Windows), or switch to a split layout with Win+Space.
XMonad offers five more workspaces that I haven’t dedicated to anything yet, but I can use them as the need arises.
Now let’s examine the configuration that makes this all possible. Xmonad configuration starts with launching X11. Here’s my .xinittrc:
1: xsetroot -solid "#330005"
2: xscreensaver -no-splash &
3: uxterm &
4: exec xmonad
I set the workspace background (which I only see when no windows are open) to a nice, dark wine color. Then I start up xscreensaver and a unicode-capable xterm (thanks to Chad for the tip), and finally start up xmonad. The configuration for uxterm is defined, rather simply, in .Xdefaults:
1: UXTerm*background: grey3
2: UXTerm*foreground: SpringGreen
3: UXTerm*savelines: 512
Yes, I like the old school green on black – but not exactly green and not exactly black. I also like to be able to scroll pretty far back.
xmonad.hs
Now let’s move on to the XMonad-specific stuff. The primary configuration file for Xmonad is ~/.xmonad/xmonad.hs:
1: import XMonad
2: import XMonad.Actions.CycleWS
3: import XMonad.Hooks.DynamicLog
4: import XMonad.Hooks.ManageDocks
5: import XMonad.Layout.NoBorders
6: import XMonad.Layout.PerWorkspace
7: import qualified XMonad.StackSet as W
8: import XMonad.Util.EZConfig(additionalKeys)
9: import XMonad.Util.Run(spawnPipe)
10: import System.IO
11: import Keys(myKeys)
12:
13: main = do
14: xmproc <- spawnPipe "xmobar"
15: xmonad $ defaultConfig
16: { borderWidth = 3
17: , normalBorderColor = "#1e1c10"
18: , focusedBorderColor = "#5d0017"
19: , workspaces = ["1:main", "2:mail", "3:web", "4:vbox", "5", "6", "7", "8", "9"]
20: , layoutHook = avoidStruts $
21: onWorkspace "2:mail" (mailLayout ||| Full) $
22: onWorkspace "4:vbox" (Full ||| Mirror tiled) $
23: smartBorders (tiled ||| Mirror tiled ||| Full )
24: , logHook = dynamicLogWithPP $ xmobarPP
25: { ppOutput = hPutStrLn xmproc
26: , ppTitle = xmobarColor "#cdcd57" "" . shorten 50
27: , ppCurrent = xmobarColor "#cdcd57" ""
28: , ppSep = " <fc=#3d3d07>|</fc> "
29: }
30: , modMask = mod4Mask
31: , terminal = "uxterm"
32: , manageHook = myManageHook
33: } `additionalKeys` myKeys
34: where
35: mailLayout = Tall nmaster delta mailRatio
36: tiled = Tall nmaster delta ratio
37:
38: -- default number of windows in the master pane
39: nmaster = 1
40:
41: -- default propoertion of screen occupied by master pane
42: ratio = toRational (2/(1+sqrt(5)::Double)) -- golden ratio
43: mailRatio = 0.8 -- Pareto ratio, mutt:pidgin
44:
45: -- Percent of screen to increment by when resizing panes
46: delta = 0.05
47:
48: -- How to handle various windows
49: myManageHook = composeAll
50: [ className =? "Firefox" --> doF (W.shift "3:web")
51: , className =? "Gimp" --> doFloat
52: , className =? "Gnubg" --> doFloat
53: , title =? "mutt" --> doF (W.shift "2:mail")
54: , className =? "Pidgin" --> doF (W.shift "2:mail")
55: , title =? "qiv" --> doFloat
56: , className =? "VirtualBox" --> doF (W.shift "4:vbox")
57: , manageDocks
58: ] <+> manageHook defaultConfig
First, we import a bunch of stuff that we’ll need. You may notice at the end of the imports section an import for Keys(myKeys). It turns out you can modularize your XMonad configuration into multiple files, as long as you place them in ~/.xmonad/lib and name the module and the file the same thing. So that import statement looks for ~/xmonad/lib/Keys.hs when recompiling your configuration. That’s done with the shell command:
The “main” function in xmonad.hs defines XMonad’s configuration. The first thing I do is open a pipe to xmobar, a text-based status bar that can populate part of its information from stdin. That’s why I opened it on a pipe, so XMonad can send its status changes to xmobar.
Next, we apply overrides to the default configuration of XMonad. I use a 3-pixel window border, because my 50-year-old eyes can’t easily see a 1-pixel line. But the default light grey and bright red are too much, so I softened those colors. I named all the workspaces (which is optional). Even though I don’t have names for workspaces 5 through 9, I supplied their numbers as names (otherwise you don’t get them). I should note that you aren’t required to put the number in the name, but since their keyboard shortcuts are mod-1 through mod-9, I elected to begin each name with its number.
The layoutHook overrides the layout options for windows. The avoidStruts layout keeps windows from overlapping the xmobar window. The onWorkspace option (imported in XMonad.Layout.PerWorkspace) selects a workspace-specific layout based on the name of the workspace. On “2:mail”, I specify that there are two layouts that the user can cycle through (using mod-space): a layout named mailLayout, or the built-in layout named Full (main window full-screen). The mailLayout is defined later in the “where” clause as the Tall layout (split horizontally) with one main window, a sizing delta of 5%, and a split ratio of 80/20.
The next workspace-specific layout is for “4:vbox”, where we start in Full mode, but allow the user to switch to Mirror tiled, where Mirror is a built-in layout (vertically split, with main window on top), and tiled is defined below as a golden ratio split.
The final layout specified applies to all other workspaces, where we start out with a horizontal golden ratio split, and then cycle through the vertical split and full screen.
The logHook specifies how we’ll log status for xmobar. The dynamicLogWithPP is a “pretty-print” formatter of dynamic status information, which we combine with a built-in configuration for xmobar, overriding the output to go to our pipe to xmobar (xmproc). The active window title is shortened to 50 characters if needed, the color of the active workspace and window title are just a bit brighter than the other text on xmobar, and the separator between sections is overridden to | and colored like the rest of the separators on xmobar so it fits right in. This produces the first three |-delimited sections on the status bar that you can see in the screen shots above.
I override modMask to mod4Mask, so wherever XMonad expects the mod key (Alt by default) I can use the Windows key instead. This frees up Alt for other applications.
My terminal is uxterm, which is what XMonad will launch whenever I press mod-shift-enter.
Next comes the manageHook, which specifies how windows are managed. Look down in the where clause to myManageHook, which specifies that any window with the className matching “Firefox” will be automatically shifted to workspace “3:web”. “Gimp” and “Gnubg” will be allowed to float instead of being tiled. The window titled “mutt” (we’ll see how that happens later) will be routed to workspace “2:mail”, along with windows of class “Pidgin”. A window titled “qiv” (an image viewer) can float, and “VirtualBox” will go in workspace “4:vbox”. Finally, we combine the built-in manageDocks and merge with the manageHook for defaultConfig.
The only other piece of this file is the key mapping. I use the additionalKeys imported from XMonad.Util.EZConfig so I don’t have to respecify the entire key map, only what I want to add or override. That comes from myKeys, which I imported from Keys.hs and looks like this:
1: module Keys where
2: import XMonad
3: import XMonad.Actions.CycleWS
4:
5: -- additional key mappings
6: myKeys =
7: [ ((0, xK_Print), spawn "scrot -q 100") -- capture screen
8: , ((controlMask, xK_Print), spawn "sleep 0.2; scrot -s -q 100") -- choose window to capture
9: , ((mod4Mask, xK_a), spawn "usualapps") -- launch mutt, firefox, and pidgin
10: , ((mod4Mask, xK_f), spawn "/usr/local/lib/firefox3/firefox") -- firefox
11: , ((mod4Mask, xK_p), spawn "exe=`dmenu_path | dmenu -nb '#1c1c0e' -nf '#7d7d37' -sb 'gold' -sf 'grey30'` && eval \"exec $exe\"") -- dmenu
12: , ((mod4Mask, xK_r), spawn "xmonad --recompile && xmonad --restart") -- refresh xmonad config
13: , ((mod4Mask, xK_t), spawn "uxterm") -- uxterm
14: , ((mod4Mask, xK_Right), nextWS) -- next workspace
15: , ((mod4Mask, xK_Left), prevWS) -- previous workspace
16: , ((mod4Mask, xK_F1), spawn "xmonad_keys.sh") -- key help (toggle)
17: , ((mod4Mask, xK_F9), spawn "xscreensaver-command -lock && xset dpms force off") -- lock workstation and turn off display
18: ]
This is pretty easy to decode, and I’ve added comments to describe each action. But a couple of them bear explanation.
The shell script usualapps launches my usual applications if they aren’t launched already:
1: #!/bin/sh
2: if ! xwininfo -name "Pidgin" >/dev/null 2>/dev/null
3: then
4: pidgin &
5: sleep 2
6: fi
7: if ! xwininfo -name "Firefox" >/dev/null 2>/dev/null
8: then
9: /usr/local/lib/firefox3/firefox &
10: fi
11: if ! xwininfo -name "mutt" >/dev/null 2>/dev/null
12: then
13: uxterm -title "mutt" -e mutt &
14: fi
I use xwininfo to see if each window can be located. If not, I launch the associated application (pidgin, firefox, or mutt). Mutt gets launched inside a uxterm for two reasons: (1) it needs a console for I/O, and (2) I can name that window “mutt” for my manageHook to recognize it. I launch Pidgin first and sleep 2 seconds, to make sure that xmonad lays it out before mutt. That puts mutt into the main window by default.
The key help (mod4-F1) is provided by a shell script xmonad_keys.sh, which I adapted from this version. Here’s mine:
1: #!/bin/sh
2: #
3: # Toggles display of dzen window containing key mappings for xmonad
4: #
5: # Adapted from http://snipt.net/doitian/show-xmonad-key-bindings/
6: #
7: # Assumes that keys are defined in ~/.xmonad/lib/Keys.hs, one per line.
8: #
9:
10: if xwininfo -name "dzen slave"
11: then
12: killall dzen2
13: else
14: keysfile=~/.xmonad/lib/Keys.hs
15: fgColor="#0a0a05"
16: bgColor="#f6e6a7"
17: font="-*-fixed-*-*-*-*-10-*-*-*-*-*-*-*"
18:
19: (
20: echo "^fg($bgColor)^bg($fgColor) xmonad keys ^bg()^fg()"
21: xmonad_keys.rb $keysfile
22: ) | dzen2 -fg $fgColor -bg $bgColor -fn $font -x 624 -y 15 -l 11 -w 400 -p \
23: -e 'onstart=uncollapse,scrollhome,grabkeys;enterslave=grabkeys;entertitle=uncollapse,grabkeys;key_Escape=ungrabkeys,exit;key_Next=scrolldown;key_Prior=scrollup'
24: fi
This script toggles the existence of a dzen2 window containing documentation of my additional key bindings, which I filter out of my Keys.hs file using a Ruby script:
1: #!/usr/local/bin/ruby
2: #
3: # Key definitions must be specified one per line
4: #
5: $<.each do |line|
6: if (match = /\(\((.+?),\s*xK_(\w+)\),\s*(.+)\)\s*--\s*(.*)$/.match(line))
7: key = ((match[1] == '0') ? '' :
8: (match[1].gsub('Mask','').gsub(/ *\.\|\. */,'-') + '-')) +
9: match[2]
10: puts ' ' + key.ljust(24) + match[4]
11: end
12: end
This script extracts the key combination and comment, then lines them up in two columns. If you don’t want to document a key, just leave off the comment and the regex will fail to match. Here’s what the final window looks like:
This window floats on the screen until you press mod4-F1 again, or Escape if it’s focused.
.xmobarrc
The configuration of xmobar (the status bar across the top), apart from the information that comes from xmonad itself, is defined in ~/.xmobarrc. Here’s mine:
1: Config { font = "-b&h-lucida-medium-r-normal-sans-10-100-75-75-p-58-iso8859-1"
2: , bgColor = "#1c1c0e"
3: , fgColor = "#7d7d37"
4: , position = Top
5: , lowerOnStart = False
6: , commands = [ Run Weather "KPWT" ["-t","<tempF>F","-L","40","-H","80","--high","red","--low","#3333FF"] 36000
7: , Run Com "echo" ["$USER"] "username" 864000
8: , Run Com "hostname" ["-s"] "hostname" 864000
9: , Run Com "uname" ["-sr"] "os" 864000
10: , Run Date "%a %b %_d" "date" 36000
11: , Run Date "%H:%M:%S" "time" 10
12: , Run Com "mem" ["-tm"] "memtot" 36000
13: , Run Com "mem" ["-um"] "memused" 10
14: , Run Com "mem" ["-p"] "mempct" 10
15: , Run Com "loadavg" [] "loadavg" 10
16: , Run Com "batt" [] "batt" 600
17: , Run CommandReader "ledmon" "LED"
18: , Run StdinReader
19: ]
20: , sepChar = "'"
21: , alignSep = "}{"
22: , template = "'StdinReader' <fc=#3d3d07>|</fc> 'username' <fc=#3d3d07>|</fc> 'hostname' <fc=#3d3d07>|</fc> 'os' <fc=#3d3d07>|</fc> Mem 'memused'/'memtot'mb ('mempct'%) <fc=#3d3d07>|</fc> Load 'loadavg' <fc=#3d3d07>|</fc> Batt 'batt' <fc=#3d3d07>|</fc> <fc=#ffff00>'LED'</fc>}{'date' <fc=#3d3d07>|</fc> 'time' <fc=#3d3d07>|</fc> 'KPWT'"
23: }
I’ve used a proportional font so I can squeeze more text on the bar, and specified colors. The bar is across the top, and starts life not lowered. The “commands” member specifies what commands to run and how often to run them:
- Run Weather polls a NOAA station (id KPWT in Bremerton, WA in my case) for current weather conditions. I’m just showing the temperature in Fahrenheit and coloring it blue if below 40, red if above 80, otherwise inheriting the foreground color of the bar. This updates once an hour (36000 tenths of a second).
- Run Com runs a shell command and captures its output. The arguments are given in a list, followed by the alias to use in the template further down, as well as the interval. So I get the username once a day (it won’t change without restarting X11 anyway), as well as the hostname and operating system (which won’t change without rebooting).
- Run Date gets date/time information and formats it. I do this twice: formatting the date as alias “date” once an hour, and the time as alias “time” once a second.
- The “mem”, “loadavg”, and “batt” commands execute shell scripts to return the memory usage, load averages, and battery information from sysctl. I’ll go into those in more detail below.
- Run CommandReader launches a process that is expected to send status updates to stdout as they happen. The ledmon utility is a little C program by John Goerzen that you can download here. I had to change the Makefile a bit to work on FreeBSD – it wants libX11.so and libX11-xcb.so as inputs instead of –lX11. This little utility monitors changes in the Caps Lock, Num Lock, and Scroll Lock keys and sends you a textual status update of their states. For some reason, it doesn’t seem to detect Scroll Lock on FreeBSD, but the one I really care about is Caps Lock, so it serves my need.
- The final command, Run StdinReader, is what picks up the output from XMonad itself.
Now for the templating. I’ve overridden the sepChar as a single quote, because I wanted to be able to use % as an output character. Thus, in the template, single quotes are used to interpolate each of the aliases of the commands listed above. The alignSep specifies a string that separates left-justified from right-justified text. In the template, I use a | as a visual separator, and I color it a little bit dimmer than the text pieces. You can also see that I apply a bright yellow to the ledmon output, to get my attention before I accidentally join all the lines in my vim session.
Surrogates for xmobar builtins
I think xmobar must have been developed for use under Linux, because some of the built-in commands rely on being able to access Linux-specific features, like /proc/stat for Run Mem. FreeBSD doesn’t have /proc/stat, so I created some shell scripts to extract information from sysctl(8) instead. These are all included in the download below.
- mem provides information on memory. Use mem –h for usage.
- loadavg returns the load average statistics, stripping off the {} included in the sysctl output.
- batt returns the battery’s percent of full charge. If charging or discharging, that’s also noted.
Nonfinal remarks
This configuration is certainly a work in progress, and I can’t help but fiddle with it. So, I may be posting an update at some point in the future. Please let me know if there’s anything I could do more easily, or any other suggestions you might have.