This article is about three things I'm very interested in. I've been a fan of using real programming languages for configuration files for a long time, but haven't written about that recently. I've been using tiling window managers - now in their dynamic version - for a long time as well, and have written about that. Finally, I've been a fan of Haskell for a while, and have written a number of articles about using it.
XMonad is a dynamic, tiling window manager written in Haskell that uses a Haskell module as a configuration file. This has the usual advantages of doing so - you can put values in variables rather than repeating them, and construct new values with Haskell expressions, etc.
One of the features of XMonad is a
Layout, which controls how windows are tiled on the screen. The core of XMonad provides some basic - but very useful -
Layouts, and there are extensions to do things like creating tabbed stacks of windows, nesting
Layouts control how windows are arranged, they are critical components, and changing them is how you change your window managers behavior. I'm going to look at extending the behavior of one of the core
Tall - in a number of ways.
And a credit. The code here was inspired by Devin Mullins, who provided information and code samples while helping me with my XMonad configuration.
Layout needs to be an instance of the
LayoutClass type class. As such, a
Layout needs to do three things: run the layout, handle
Messages from the window manager, and optionally provide a
description. You can find details on that in the API documentation.
description is "a human-readable string used for selecting Layout's." Some tools display them for selection, others use
descriptions to select
Layouts programmatically, say from a list of strings in the configuration. These different uses give rise to different needs, so we'll start by just changing it. This would allow us to have two different
Tall layouts, and tell the difference between them.
First, we need to declare our data type:
data MyTall a = MyTall (Tall a) deriving (Show, Read)
Now we need to make this an instance of
LayoutClass. Running will be forwarded to the wrapped
Tall with the
runLayout method. The same will be done with
Messages by the
pureMessage method. I'll get into the details of those later.
instance LayoutClass MyTall a where runLayout (W.Workspace id (MyTall tall) ms) r = fmap (second (fmap MyTall)) $ runLayout (W.Workspace id tall ms) r pureMessage (MyTall tall) m = fmap MyTall $ pureMessage tall m
And the critical part is to change the description:
description _ = "MyTall"
So now we can create two different
Layouts with different names: one is a regular
Tall layout, and the other a
MyTall layout. Exactly how you do that will depend on your XMonad config file, but you would just add a
Tall layout and wrap it in a
MyTall like one of these examples:
MyTall (Tall *...*) MyTall $ Tall *...*
Instead of just displaying the name, we could display information from the
Layout. One of the features of
Tall is a master pane, which holds a programmable number of client windows - typically the one or two you're working on now, with other windows dynamically sized in a second pane. The wrapped
Tall has the format
Tall n delta frac, where
n is the number of clients in the master pane. We can put that count in the description like so:
description (MyTall (Tall n _ _)) = "Tall " ++ show n
We can still have two
Layouts with different names, but now the second one is distinguished by having the number of client windows in the master pane displayed.
Commands for a
Layout are described by the
Message type class.
Tall has two messages, one to change the size of the master pane, and one to change the number of windows in it. I tend to use either one or two windows in the master pane, and would like the ability to toggle between those two states.
Toggling the master pane
So we'll create a new
ToggleMaster to toggle the number of clients in the master pane:
data ToggleMaster = ToggleMaster deriving Typeable instance Message ToggleMaster
Now, we need to change the existing
pureMessage method to handle this
Message. Let's dissect the current version first:
pureMessage (MyTall tall) m = fmap MyTall $ pureMessage tall m
pureMessage gets a
Layout and a
m. It returns a
Maybe (layout a). Forwarding the message is easy - we just call
pureMessage on the wrapped
Tall, extracted by pattern matching in the function. The returned
Maybe Tall needs to be rewrapped to a
fmap MyTall does that for us.
To handle the
Message ourselves, we need to get the actual message from the
fromMessage will do for us. If that returns
Just ToggleMaster, then we want to handle this
Message. Otherwise, it will return
Nothing, and we pass the message as before. So far we have:
pureMessage (MyTall tall) m = case fromMessage m of Nothing -> fmap MyTall $ pureMessage tall m Just ToggleMaster -> undefined
To handle the
ToggleMaster message, we need to return a
MyTall Tall where
Tall has the new number of client windows we want in the master pane:
pureMessage (MyTall tall@(Tall n delta frac)) m = case fromMessage m of Nothing -> fmap MyTall $ pureMessage tall m Just ToggleMaster -> Just . MyTall $ Tall new delta frac where new = if n /= 1 then 1 else 2
This uses pattern matching to get the values in the
Tall. When we get a
Message, we create the new value
if n /= 1 then 1 else 2 . While I usually toggle between 1 and 2 clients, it handles all other cases by going back to 1 as well. To finish this, we create a new
Tall that we wrap with
Just . MyTall.
We can now bind that in our configuration with:
, ((modm , xK_slash), sendMessage ToggleMaster)
Mod-slash to toggle the master window, which seems to work well with
Mod-period, the defaults for incrementing and decrementing the number of clients in the master pane.
If you also used three client window regularly, you might want a separate toggle for that. We're going to do that in two different ways.
First, we can simply give ToggleMaster an argument and a name change to match:
data ToggleMasterN = ToggleMasterN !Int deriving Typeable
And the changed bindings, including using backslash for the 3-way split:
, ((modm , xK_slash), sendMessage $ ToggleMasterN 2) -- Toggle the master window split 3-way. , ((modm , xK_backslash), sendMessage $ ToggleMasterN 3)
And then we change the handling in
pureMessage to use that argument instead of 2:
Just (ToggleMasterN i) -> Just . MyTall $ Tall new delta frac where new = if n /= 1 then 1 else i
Splits as well.
But suppose we wanted a command that always split the master window, no matter what it currently was? Let's call it
SetMasterN, and the code to handle it is pretty simple:
Just (SetMasterN new) -> Just . MyTall $ Tall new delta frac
I'll omit the bindings. The new
Message is similar to
data SetMasterN = SetMasterN !Int deriving Typeable instance Message SetMasterN
So all we have to do is get the code for
SetMasterN to be run in
pureMessage. We're going to refactor
pureMessage a bit to do that:
pureMessage (MyTall tall@(Tall n delta frac)) m = msum [fmap MyTall $ pureMessage tall m, fmap toggle (fromMessage m), fmap set (fromMessage m)] where toggle ToggleMaster = MyTall $ Tall new delta frac where new = if n /= 1 then 1 else 2 set (SetMasterN new) = MyTall $ Tall new delta frac
Each element of the list passed to
msum handles a different set of messages. The first line passes all of them to
Tall, and it will return
Nothing if it doesn't handle that message. Each element after that will pass the appropriate messages to the function that handles them, or be
msum then returns the
Just value from the list.
It's type is
handleMessage :: layout a -> SomeMessage -> X (Maybe (layout a))
so requires another level of
fmap calls to work with the values in the
Layout. Details can be found in the API documentation.
The last set of functions in a
LayoutClass are the ones that generate the rectangles that windows wind up in. At this point, you're really past extending the
Layout, and are writing a new one. But I'm going to look at a simple case anyway.
The oddity of 0
One of the odder behaviors of the default
Layout is that putting all the windows in the master pane and putting none of the windows in the master pane gets the same layout. Both wind up with all windows stretching the width of the screen. It's just that in one, they're in the master pane, and in the other they're in the other pane.
The difference between them is that you can keep increasing the master pane count until you hit
maxBound, but once you decrease it to 0, it won't go any lower. So you can use
IncMasterN minBound to get to 0 from any value. Once you've got
ToggleMaster, you can use those instead, and use
IncMasterN maxBound to get to the layout that was at 0. Which means 0 can be used for something else.
A full screen mode
Give the above, we can extend
Tall so that putting 0 windows in the master pane makes the first window a full screen window. While I think that this makes as much sense as the current behavior, it makes life a bit difficult if the only message you have is
The layout functions are passed a
Rectangle, and a
Stack of windows, and should return a list of tuples of (window,
Rectangle). The simplest of the layout functions is
pureLayout, which does just that. The value we want to return to get a full screen window is a list with a single tuple consisting of the first window and the initial rectangle:
import XMonad.StackSet as W pureLayout _ r s = map (, r) . take 1 $ W.integrate s
W.integrate returns a list of the windows in the
Stack. While this
pureLayout should never be called with an empty list of windows - there's a method specifically for handling that - using
take instead of
[(head $ W.integrate s, r)] insures that we don't generate an exception should that happen. Instead, we return an empty list, which is the default behavior.
Forwarding the non-zero cases
We want the above code to run when the master pane client count is 0, and otherwise we'll let
Tall handle it. That's done like so:
pureLayout (MyTall tall@(Tall n _ _)) r s = if n != 0 then pureLayout tall r s else map (, r) . take 1 $ W.integrate s
Forwarding simply unwraps the
Layout and passes that and the non-
Layout arguments along to the
And the rest
pureLayout is the simplest of the layout functions. If your layout function isn't quite so pure, you can use
doLayout returns a value in the
X monad, so you can access the
XConf values. The value it returns is a tuple consisting of the list returned by
pureLayout and a
Layout of the same type that was passed in. The
Layout allows the
Layout to be modified, ala the
Message handling examples.
While those two are normally sufficient, there's also
runLayout. Both of these return the same type as
doLayout. The default implementation of
emptyLayout if there are no windows and
doLayout otherwise. You should only need these if you want special handling for the case where there are no windows. Details can be found in the API documentation.