Contains coding, but not narcotic.

XMonad on FreeBSD

April 18th, 2010 5:38:11 pm pst by Sterling Camden

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):

main

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:

mail

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):

web

Workspace number 4 is where I bring up VirtualBox.  Here it’s shown running Windows 7:

vbox

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:

   1: xmonad --recompile

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:

xmonad_keys

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.

download

Posted in Haskell, Ruby, Wildly popular | 6 Comments » RSS 2.0

6 Responses to “XMonad on FreeBSD”

  1. [...] XMonad on FreeBSD — Chip’s Tips for DevelopersPulling it all togetherTags: x11 xmonad haskell xmobar dzen2 [...]

  2. [...] my quest to find more lightweight, componentized, customizable tools for my work, I decided to use when for my calendar.  When uses a simple text file for [...]

  3. [...] resolution. They’ve been reduced to fit here, so click them to see the original size. I use xmonad as my window manager, which tiles windows by default. The status bars on the very top and bottom of each image are [...]

  4. [...] Haskell, so thought of installing XMonad. I use a very simple, highly functional config from Sterling Camden. I configured it for my needs and it fits the bill perfectly. Every workspace is modeled according [...]

  5. Jan says:

    Thanks for sharing. It helped me getting started with xmonad!

Leave a Reply